tala-compiler-0.1.0: test/ShortCircuitTests.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
module ShortCircuitTests (shortCircuitTests) where
import Data.List (isPrefixOf, nub)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Core
import Progmasoft.Tala.Core.CorePrep
import Progmasoft.Tala.Core.CorePrep.Verifier (verifyCorePrep)
import Progmasoft.Tala.Diagnostic (Diagnostic)
shortCircuitTests :: [(String, Bool)]
shortCircuitTests =
[ ("logical AND lowers to a branch and join", andHasExpectedShape)
, ("logical OR reverses the short-circuit edge", orHasExpectedShape)
, ("logical primitives never survive CorePrep", noEagerLogicalPrimitives)
, ("AND result storage starts as false", andSeedIsFalse)
, ("OR result storage starts as true", orSeedIsTrue)
, ("short-circuit result storage is mutable Bool", resultStorageIsMutableBool)
, ("right operand assigns the short-circuit result", rightOperandAssignsResult)
, ("numeric left operands become Bool before branching", numericLeftIsBooleanized)
, ("numeric right operands become Bool before assignment", numericRightIsBooleanized)
, ("Boolean operands do not acquire redundant comparisons", booleanOperandsAvoidComparisons)
, ("nested conjunction creates two conditional regions", nestedAndCreatesTwoRegions)
, ("nested disjunction creates two conditional regions", nestedOrCreatesTwoRegions)
, ("mixed nested logical expressions create three regions", mixedTreeCreatesThreeRegions)
, ("logical expressions in bindings resume at their join", bindingContinuesAfterJoin)
, ("logical expressions in assignments resume at their join", assignmentContinuesAfterJoin)
, ("logical expressions in returns terminate the join", returnTerminatesJoin)
, ("logical expressions used as call arguments preserve order", callArgumentContinuesAtJoin)
, ("logical expressions used as a callee preserve order", calleeContinuesAtJoin)
, ("logical expressions used as if conditions compose CFG", conditionComposesControlFlow)
, ("logical expressions used as closure captures compose CFG", captureComposesControlFlow)
, ("every generated block id is unique", generatedBlockIdsAreUnique)
, ("every generated branch target exists", generatedTargetsExist)
, ("every short-circuit result has exactly one seed", eachResultHasOneSeed)
, ("every short-circuit result has exactly one conditional assignment", eachResultHasOneAssignment)
, ("temporary symbols remain globally unique", generatedSymbolsAreUnique)
, ("the CorePrep verifier accepts simple AND", verifierAcceptsAnd)
, ("the CorePrep verifier accepts simple OR", verifierAcceptsOr)
, ("the CorePrep verifier accepts nested logical control flow", verifierAcceptsNested)
, ("false AND keeps its right call behind a branch", andCallIsConditional)
, ("true OR keeps its right call behind a branch", orCallIsConditional)
]
andHasExpectedShape :: Bool
andHasExpectedShape = case preparedFunction (returning (logical CoreLogicalAnd boolTrue boolFalse)) of
Just function ->
length (corePrepFunctionBlocks function) == 3
&& branchTargets function == [(1, 2)]
&& jumpTargets function == [2]
Nothing -> False
orHasExpectedShape :: Bool
orHasExpectedShape = case preparedFunction (returning (logical CoreLogicalOr boolFalse boolTrue)) of
Just function -> branchTargets function == [(2, 1)] && jumpTargets function == [2]
Nothing -> False
noEagerLogicalPrimitives :: Bool
noEagerLogicalPrimitives = all (not . containsEagerLogical) (allInstructions mixedPrepared)
andSeedIsFalse :: Bool
andSeedIsFalse = seedValues andPrepared == [False]
orSeedIsTrue :: Bool
orSeedIsTrue = seedValues orPrepared == [True]
resultStorageIsMutableBool :: Bool
resultStorageIsMutableBool = case shortCircuitSeeds andPrepared of
[CorePrepBind _ valueType mutable _] -> valueType == boolType && mutable
_ -> False
rightOperandAssignsResult :: Bool
rightOperandAssignsResult = case (shortCircuitNames andPrepared, shortCircuitAssignments andPrepared) of
([seed], [assigned]) -> seed == assigned
_ -> False
numericLeftIsBooleanized :: Bool
numericLeftIsBooleanized =
hasComparisonToZero intType numericPrepared
&& all ((== boolType) . atomType) (branchConditions numericPrepared)
numericRightIsBooleanized :: Bool
numericRightIsBooleanized =
hasComparisonToZero floatType numericPrepared
&& all ((== boolType) . atomType) (assignmentValues numericPrepared)
booleanOperandsAvoidComparisons :: Bool
booleanOperandsAvoidComparisons = not (any isNotEqual (allInstructions andPrepared))
nestedAndCreatesTwoRegions :: Bool
nestedAndCreatesTwoRegions = length (branchConditions nestedAndPrepared) == 2
nestedOrCreatesTwoRegions :: Bool
nestedOrCreatesTwoRegions = length (branchConditions nestedOrPrepared) == 2
mixedTreeCreatesThreeRegions :: Bool
mixedTreeCreatesThreeRegions =
length (branchConditions mixedPrepared) == 3
&& length (shortCircuitSeeds mixedPrepared) == 3
&& length (shortCircuitAssignments mixedPrepared) == 3
bindingContinuesAfterJoin :: Bool
bindingContinuesAfterJoin = case preparedFunction statements of
Just function -> any joinContainsBinding (corePrepFunctionBlocks function)
Nothing -> False
where
value = name 20 "value"
statements =
[ CoreBind (CoreBinding value boolType False (logical CoreLogicalAnd boolTrue boolFalse))
, CoreReturn unitLiteral
]
joinContainsBinding block = any bindsValue (corePrepBlockInstructions block)
bindsValue (CorePrepBind bound _ _ _) = bound == value
bindsValue _ = False
assignmentContinuesAfterJoin :: Bool
assignmentContinuesAfterJoin = case preparedFunction statements of
Just function -> any joinContainsAssignment (corePrepFunctionBlocks function)
Nothing -> False
where
value = name 20 "value"
statements =
[ CoreBind (CoreBinding value boolType True boolFalse)
, CoreAssign value (logical CoreLogicalOr boolFalse boolTrue)
, CoreReturn unitLiteral
]
joinContainsAssignment block = any assignsValue (corePrepBlockInstructions block)
assignsValue (CorePrepAssign assigned _) = assigned == value
assignsValue _ = False
returnTerminatesJoin :: Bool
returnTerminatesJoin = case preparedFunction (returning (logical CoreLogicalAnd boolTrue boolFalse)) of
Just function -> case reverse (corePrepFunctionBlocks function) of
block : _ -> case corePrepBlockTerminator block of
CorePrepReturn value -> atomType value == boolType
_ -> False
[] -> False
Nothing -> False
callArgumentContinuesAtJoin :: Bool
callArgumentContinuesAtJoin = case preparedFunction (returning call) of
Just function -> exactlyOneCall function && callBlockIsJoin function
Nothing -> False
where
call = CoreApply functionVariable [logical CoreLogicalAnd boolTrue boolFalse] boolType
calleeContinuesAtJoin :: Bool
calleeContinuesAtJoin = case preparedFunction (returning call) of
Just function -> exactlyOneCall function && length (branchConditions (Just function)) == 1
Nothing -> False
where
argument = name 51 "argument"
callableType = FunctionType [boolType] boolType
callee =
CoreClosure
[]
[(argument, boolType)]
boolType
[CoreReturn (CoreVariable argument boolType)]
callableType
call = CoreApply callee [logical CoreLogicalOr boolFalse boolTrue] boolType
conditionComposesControlFlow :: Bool
conditionComposesControlFlow = case preparedFunction statements of
Just function ->
length (branchConditions (Just function)) == 2
&& allTargetsExist function
Nothing -> False
where
statements =
[ CoreIf
(logical CoreLogicalAnd boolTrue boolFalse)
[CoreReturn unitLiteral]
[CoreReturn unitLiteral]
]
captureComposesControlFlow :: Bool
captureComposesControlFlow = case preparedModule statements of
Right moduleValue ->
length (corePrepModuleFunctions moduleValue) == 2
&& length (branchConditions (preparedFunctionFromModule moduleValue)) == 1
Left _ -> False
where
capture = CoreCapture StrongCapture (name 40 "enabled") boolType (logical CoreLogicalOr boolFalse boolTrue)
closure = CoreClosure [capture] [] boolType [CoreReturn (CoreVariable (name 40 "enabled") boolType)] nullaryBoolFunction
statements = [CoreEvaluate closure, CoreReturn unitLiteral]
generatedBlockIdsAreUnique :: Bool
generatedBlockIdsAreUnique = case mixedPrepared of
Just function -> unique (map corePrepBlockId (corePrepFunctionBlocks function))
Nothing -> False
generatedTargetsExist :: Bool
generatedTargetsExist = maybe False allTargetsExist mixedPrepared
eachResultHasOneSeed :: Bool
eachResultHasOneSeed =
let names = shortCircuitNames mixedPrepared
in unique names && length names == 3
eachResultHasOneAssignment :: Bool
eachResultHasOneAssignment =
let seeds = shortCircuitNames mixedPrepared
assignments = shortCircuitAssignments mixedPrepared
in all (\seed -> count seed assignments == 1) seeds
generatedSymbolsAreUnique :: Bool
generatedSymbolsAreUnique = unique (generatedBindingSymbols mixedPrepared)
verifierAcceptsAnd :: Bool
verifierAcceptsAnd = maybe False (verifierAccepts . singletonModule) andPrepared
verifierAcceptsOr :: Bool
verifierAcceptsOr = maybe False (verifierAccepts . singletonModule) orPrepared
verifierAcceptsNested :: Bool
verifierAcceptsNested = maybe False (verifierAccepts . singletonModule) mixedPrepared
andCallIsConditional :: Bool
andCallIsConditional = callIsSeparatedFromEntry CoreLogicalAnd boolFalse
orCallIsConditional :: Bool
orCallIsConditional = callIsSeparatedFromEntry CoreLogicalOr boolTrue
andPrepared :: Maybe CorePrepFunction
andPrepared = preparedFunction (returning (logical CoreLogicalAnd boolTrue boolFalse))
orPrepared :: Maybe CorePrepFunction
orPrepared = preparedFunction (returning (logical CoreLogicalOr boolFalse boolTrue))
numericPrepared :: Maybe CorePrepFunction
numericPrepared =
preparedFunction
( returning
( logical
CoreLogicalAnd
(CoreVariable (name 2 "integer") intType)
(CoreVariable (name 3 "floating") floatType)
)
)
nestedAndPrepared :: Maybe CorePrepFunction
nestedAndPrepared =
preparedFunction
(returning (logical CoreLogicalAnd boolTrue (logical CoreLogicalAnd boolFalse boolTrue)))
nestedOrPrepared :: Maybe CorePrepFunction
nestedOrPrepared =
preparedFunction
(returning (logical CoreLogicalOr (logical CoreLogicalOr boolFalse boolTrue) boolFalse))
mixedPrepared :: Maybe CorePrepFunction
mixedPrepared =
preparedFunction
( returning
( logical
CoreLogicalAnd
(logical CoreLogicalOr boolFalse boolTrue)
(logical CoreLogicalAnd boolTrue boolFalse)
)
)
preparedFunction :: [CoreStatement] -> Maybe CorePrepFunction
preparedFunction statements = case preparedModule statements of
Right moduleValue -> case corePrepModuleFunctions moduleValue of
function : _ -> Just function
[] -> Nothing
Left _ -> Nothing
preparedModule :: [CoreStatement] -> Either [Diagnostic] CorePrepModule
preparedModule statements = case prepareCore moduleValue of
Right prepared -> Right prepared
Left _ -> error "prepareCore currently cannot fail after verified Core"
where
moduleValue = CoreModuleWithSources (QualifiedName [Identifier "ShortCircuit"]) [testFunction statements] [] []
preparedFunctionFromModule :: CorePrepModule -> Maybe CorePrepFunction
preparedFunctionFromModule moduleValue = case corePrepModuleFunctions moduleValue of
function : _ -> Just function
[] -> Nothing
testFunction :: [CoreStatement] -> CoreFunction
testFunction statements = CoreFunction (name 1 "Evaluate") parameters returnType statements
where
parameters =
[ (name 2 "integer", intType)
, (name 3 "floating", floatType)
, (name 31 "leftFunction", nullaryBoolFunction)
, (name 32 "rightFunction", nullaryBoolFunction)
, (name 50 "Predicate", FunctionType [boolType] boolType)
]
returnType = case reverse statements of
CoreReturn expression : _ -> expressionType expression
_ -> unitType
returning :: CoreExpression -> [CoreStatement]
returning expression = [CoreReturn expression]
logical :: CorePrimitive -> CoreExpression -> CoreExpression -> CoreExpression
logical primitive left right = logicalWithType primitive left right boolType
logicalWithType :: CorePrimitive -> CoreExpression -> CoreExpression -> Type -> CoreExpression
logicalWithType primitive left right resultType = CorePrimitive primitive [left, right] resultType
boolTrue :: CoreExpression
boolTrue = CoreLiteral (CoreBoolean True) boolType
boolFalse :: CoreExpression
boolFalse = CoreLiteral (CoreBoolean False) boolType
unitLiteral :: CoreExpression
unitLiteral = CoreLiteral CoreUnit unitType
floatType :: Type
floatType = namedType "float"
nullaryBoolFunction :: Type
nullaryBoolFunction = FunctionType [] boolType
functionVariable :: CoreExpression
functionVariable = CoreVariable (name 50 "Predicate") (FunctionType [boolType] boolType)
name :: Int -> String -> ResolvedName
name symbol spelling = ResolvedName (SymbolId symbol) (Identifier spelling)
allBlocks :: Maybe CorePrepFunction -> [CorePrepBlock]
allBlocks = maybe [] corePrepFunctionBlocks
allInstructions :: Maybe CorePrepFunction -> [CorePrepInstruction]
allInstructions = concatMap corePrepBlockInstructions . allBlocks
branchConditions :: Maybe CorePrepFunction -> [CorePrepAtom]
branchConditions = foldMap condition . allBlocks
where
condition block = case corePrepBlockTerminator block of
CorePrepBranch value _ _ -> [value]
_ -> []
branchTargets :: CorePrepFunction -> [(Int, Int)]
branchTargets = foldMap targets . corePrepFunctionBlocks
where
targets block = case corePrepBlockTerminator block of
CorePrepBranch _ trueTarget falseTarget -> [(trueTarget, falseTarget)]
_ -> []
jumpTargets :: CorePrepFunction -> [Int]
jumpTargets = foldMap targets . corePrepFunctionBlocks
where
targets block = case corePrepBlockTerminator block of
CorePrepJump target -> [target]
_ -> []
shortCircuitSeeds :: Maybe CorePrepFunction -> [CorePrepInstruction]
shortCircuitSeeds = filter isSeed . allInstructions
where
isSeed (CorePrepBind bound valueType mutable (CorePrepCopy (CorePrepLiteral (CoreBoolean _) literalType))) =
isShortCircuitName bound && valueType == boolType && mutable && literalType == boolType
isSeed _ = False
seedValues :: Maybe CorePrepFunction -> [Bool]
seedValues = foldMap seed . shortCircuitSeeds
where
seed (CorePrepBind _ _ _ (CorePrepCopy (CorePrepLiteral (CoreBoolean value) _))) = [value]
seed _ = []
shortCircuitNames :: Maybe CorePrepFunction -> [ResolvedName]
shortCircuitNames = foldMap bound . shortCircuitSeeds
where
bound (CorePrepBind result _ _ _) = [result]
bound _ = []
shortCircuitAssignments :: Maybe CorePrepFunction -> [ResolvedName]
shortCircuitAssignments = foldMap assigned . allInstructions
where
assigned (CorePrepAssign result _) | isShortCircuitName result = [result]
assigned _ = []
assignmentValues :: Maybe CorePrepFunction -> [CorePrepAtom]
assignmentValues = foldMap assigned . allInstructions
where
assigned (CorePrepAssign result value) | isShortCircuitName result = [value]
assigned _ = []
generatedBindingSymbols :: Maybe CorePrepFunction -> [SymbolId]
generatedBindingSymbols = foldMap symbol . allInstructions
where
symbol (CorePrepBind bound _ _ _) | "$" `isPrefixOf` identifierText (resolvedSpelling bound) = [resolvedSymbol bound]
symbol _ = []
isShortCircuitName :: ResolvedName -> Bool
isShortCircuitName = ("$shortcircuit" `isPrefixOf`) . identifierText . resolvedSpelling
containsEagerLogical :: CorePrepInstruction -> Bool
containsEagerLogical instruction = case instruction of
CorePrepBind _ _ _ (CorePrepPrimitive primitive _) -> primitive `elem` [CoreLogicalAnd, CoreLogicalOr]
CorePrepEvaluate (CorePrepPrimitive primitive _) -> primitive `elem` [CoreLogicalAnd, CoreLogicalOr]
_ -> False
isNotEqual :: CorePrepInstruction -> Bool
isNotEqual (CorePrepBind _ _ _ (CorePrepPrimitive CoreNotEqual _)) = True
isNotEqual _ = False
hasComparisonToZero :: Type -> Maybe CorePrepFunction -> Bool
hasComparisonToZero valueType = any comparison . allInstructions
where
comparison (CorePrepBind _ resultType _ (CorePrepPrimitive CoreNotEqual operands)) =
resultType == boolType
&& any ((== valueType) . atomType) operands
&& any isZero operands
comparison _ = False
-- The zero has the literal form of the operand type, as the native
-- adapter spells it: a floating operand is compared with a floating
-- zero, never with an integer payload under a floating type.
isZero (CorePrepLiteral literal literalType) =
literalType == valueType
&& literal == (if valueType == floatType then CoreFloating "0" else CoreInteger 0)
isZero _ = False
atomType :: CorePrepAtom -> Type
atomType atom = case atom of
CorePrepVariable _ valueType -> valueType
CorePrepLiteral _ valueType -> valueType
allTargetsExist :: CorePrepFunction -> Bool
allTargetsExist function = all (`elem` ids) targets
where
blocks = corePrepFunctionBlocks function
ids = map corePrepBlockId blocks
targets = foldMap blockTargets blocks
blockTargets block = case corePrepBlockTerminator block of
CorePrepBranch _ trueTarget falseTarget -> [trueTarget, falseTarget]
CorePrepJump target -> [target]
_ -> []
exactlyOneCall :: CorePrepFunction -> Bool
exactlyOneCall function = count True (map isCall (concatMap corePrepBlockInstructions (corePrepFunctionBlocks function))) == 1
where
isCall (CorePrepBind _ _ _ (CorePrepCall _ _)) = True
isCall (CorePrepEvaluate (CorePrepCall _ _)) = True
isCall _ = False
callBlockIsJoin :: CorePrepFunction -> Bool
callBlockIsJoin function = case jumpTargets function of
target : _ ->
any
(\block -> corePrepBlockId block == target && any isCall (corePrepBlockInstructions block))
(corePrepFunctionBlocks function)
[] -> False
where
isCall (CorePrepBind _ _ _ (CorePrepCall _ _)) = True
isCall (CorePrepEvaluate (CorePrepCall _ _)) = True
isCall _ = False
callIsSeparatedFromEntry :: CorePrimitive -> CoreExpression -> Bool
callIsSeparatedFromEntry primitive left = case preparedFunction (returning expression) of
Just function ->
let entry = corePrepFunctionEntry function
callBlocks = [corePrepBlockId block | block <- corePrepFunctionBlocks function, any isCall (corePrepBlockInstructions block)]
in length callBlocks == 1 && callBlocks /= [entry]
Nothing -> False
where
expression = logical primitive left (CoreApply functionVariable [boolTrue] boolType)
isCall (CorePrepBind _ _ _ (CorePrepCall _ _)) = True
isCall _ = False
singletonModule :: CorePrepFunction -> CorePrepModule
singletonModule function = CorePrepModule (QualifiedName [Identifier "ShortCircuit"]) [function] []
verifierAccepts :: CorePrepModule -> Bool
verifierAccepts moduleValue = case verifyCorePrep moduleValue of
Right _ -> True
Left _ -> False
unique :: (Eq a) => [a] -> Bool
unique values = length values == length (nub values)
count :: (Eq a) => a -> [a] -> Int
count needle = length . filter (== needle)