tala-compiler-0.1.0: test/StaticMemberOverloadTests.hs
-- SPDX-FileCopyrightText: 2026 Progmasoft <support@progmasoft.com>
-- SPDX-License-Identifier: MPL-2.0 WITH AdditionRef-Progmasoft-Exception-1.1
{- | Arity and ordered parameter-vector tests for static member overloads.
The scalar cross-product is deliberately resolved through the real frontend,
not a test-only ranking function. Every selected target is checked against
the typed declaration that owns its SymbolId. This catches a category of bug
that a successful parse or a return-type-only assertion would miss.
-}
module StaticMemberOverloadTests (staticMemberOverloadTests) where
import Data.List (find)
import Progmasoft.Tala.AST
import Progmasoft.Tala.BuiltinTypes
import Progmasoft.Tala.Compiler
import Progmasoft.Tala.Diagnostic
import Progmasoft.Tala.Frontend
staticMemberOverloadTests :: [(String, Bool)]
staticMemberOverloadTests =
[ ("a two-parameter call selects by its ordered type vector", orderedPairSelection)
, ("reversing overload declaration order does not change the selected vector", reversedPairOrderSelection)
, ("an overload may differ at the first parameter position", firstPositionSelection)
, ("an overload may differ at the second parameter position", secondPositionSelection)
, ("a three-parameter call checks all positions", threePositionSelection)
, ("a mismatch in each parameter position is rejected", everyPositionMismatchRejected)
, ("two-parameter duplicate signatures are rejected", duplicateTwoParameterSignature)
, ("wrong arity is diagnosed when no overload has the supplied count", wrongArityAmongOverloads)
, ("matching arity with a wrong ordered type vector reports type mismatch", wrongVectorReportsTypeMismatch)
, ("the selected return type comes from the matching declaration", selectedResultTypeIsPreserved)
, ("multi-argument resolution preserves method identity in the typed tree", selectedMethodIdentityIsStable)
]
++ pairSelectionMatrix
++ triplePositionMatrix
++ scalarPositionMismatchMatrix
orderedPairSelection :: Bool
orderedPairSelection =
selectedParameterTypes (pairProgram IntScalar LongScalar False)
== Just [scalarTypeToType IntScalar, scalarTypeToType LongScalar]
reversedPairOrderSelection :: Bool
reversedPairOrderSelection =
selectedParameterTypes (pairProgram IntScalar LongScalar True)
== Just [scalarTypeToType IntScalar, scalarTypeToType LongScalar]
firstPositionSelection :: Bool
firstPositionSelection =
selectedParameterTypes (pairProgram BooleanScalar IntScalar False) == Just [boolType, intType]
secondPositionSelection :: Bool
secondPositionSelection =
selectedParameterTypes (pairProgram IntScalar BooleanScalar False) == Just [intType, boolType]
threePositionSelection :: Bool
threePositionSelection =
selectedParameterTypes (threeParameterProgram [ByteScalar, IntScalar, DoubleScalar])
== Just (map scalarTypeToType [ByteScalar, IntScalar, DoubleScalar])
everyPositionMismatchRejected :: Bool
everyPositionMismatchRejected =
and
[ hasDiagnostic "TAT0009" (threeParameterMismatchProgram position)
| position <- [0 .. 2]
]
duplicateTwoParameterSignature :: Bool
duplicateTwoParameterSignature =
hasDiagnostic
"TAT0028"
"class Catalog { public static int Select(int first, long second) { return 1; } public static long Select(int left, long right) { return 2; } }"
wrongArityAmongOverloads :: Bool
wrongArityAmongOverloads =
hasDiagnostic
"TAT0008"
"class Catalog { public static int Select(int value) { return value; } public static int Select(int first, int second) { return first; } } class Caller { int Invoke(int value) { return Catalog.Select(value, value, value); } }"
wrongVectorReportsTypeMismatch :: Bool
wrongVectorReportsTypeMismatch =
hasDiagnostic
"TAT0009"
"class Catalog { public static int Select(int first, long second) { return first; } public static int Select(long first, int second) { return second; } } class Caller { int Invoke(long first, long second) { return Catalog.Select(first, second); } }"
selectedResultTypeIsPreserved :: Bool
selectedResultTypeIsPreserved =
case selectedFunctionType resultTypeProgram of
Just (FunctionType [first, second] result) ->
first == scalarTypeToType IntScalar
&& second == scalarTypeToType LongScalar
&& result == stringType
_ -> False
selectedMethodIdentityIsStable :: Bool
selectedMethodIdentityIsStable =
case selectedDeclaration (pairProgram LongScalar IntScalar False) of
Just (target, parametersValue) ->
resolvedSpelling target == Identifier "Select"
&& map parameterAnnotation parametersValue == map scalarTypeToType [LongScalar, IntScalar]
&& resolvedSymbol target /= SymbolId 0
Nothing -> False
pairSelectionMatrix :: [(String, Bool)]
pairSelectionMatrix =
[ ( "two-parameter vector " ++ vectorName [first, second] ++ " wins in " ++ orderName reversed
, selectedParameterTypes (pairProgram first second reversed)
== Just (map scalarTypeToType [first, second])
)
| first <- scalarTypes
, second <- scalarTypes
, first /= second
, reversed <- [False, True]
]
triplePositionMatrix :: [(String, Bool)]
triplePositionMatrix =
[ ( "three-parameter vector " ++ vectorName expected ++ " wins against a mismatch at position " ++ show position
, selectedParameterTypes (threeOverloadProgram expected distractor)
== Just (map scalarTypeToType expected)
)
| selected <- scalarTypes
, selected /= IntScalar
, position <- [0 .. 2]
, let expected = replaceAt position selected [IntScalar, IntScalar, IntScalar]
, let otherPosition = (position + 1) `mod` 3
, let distractor = replaceAt otherPosition selected expected
]
scalarPositionMismatchMatrix :: [(String, Bool)]
scalarPositionMismatchMatrix =
[ ( "three-parameter candidate rejects "
++ scalarTypeName target
++ " versus "
++ scalarTypeName actual
++ " at position "
++ show position
, hasDiagnostic "TAT0009" (typedThreeParameterProgram target actual position)
)
| target <- scalarTypes
, let actual = nextScalar target
, position <- [0 .. 2]
]
pairProgram :: ScalarType -> ScalarType -> Bool -> String
pairProgram first second reverseDeclarations =
"class Catalog { "
++ method (if reverseDeclarations then second else first) (if reverseDeclarations then first else second) "first" "second"
++ method (if reverseDeclarations then first else second) (if reverseDeclarations then second else first) "left" "right"
++ " } class Caller { "
++ "int Invoke("
++ scalarTypeName first
++ " first, "
++ scalarTypeName second
++ " second) { return Catalog.Select(first, second); } }"
threeParameterProgram :: [ScalarType] -> String
threeParameterProgram signature =
"class Catalog { public static int Select("
++ parameters signature ["first", "second", "third"]
++ ") { return 1; } } class Caller { int Invoke("
++ parameters signature ["first", "second", "third"]
++ ") { return Catalog.Select(first, second, third); } }"
threeOverloadProgram :: [ScalarType] -> [ScalarType] -> String
threeOverloadProgram selected distractor =
"class Catalog { public static int Select("
++ parameters selected ["first", "second", "third"]
++ ") { return 1; } public static int Select("
++ parameters distractor ["left", "middle", "right"]
++ ") { return 2; } } class Caller { int Invoke("
++ parameters selected ["first", "second", "third"]
++ ") { return Catalog.Select(first, second, third); } }"
typedThreeParameterProgram :: ScalarType -> ScalarType -> Int -> String
typedThreeParameterProgram target actual changedPosition =
let expected = [IntScalar, IntScalar, IntScalar]
candidate = replaceAt changedPosition target expected
callerTypes = replaceAt changedPosition actual candidate
in "class Catalog { public static int Select("
++ parameters candidate ["first", "second", "third"]
++ ") { return 1; } } class Caller { int Invoke("
++ parameters callerTypes ["first", "second", "third"]
++ ") { return Catalog.Select(first, second, third); } }"
threeParameterMismatchProgram :: Int -> String
threeParameterMismatchProgram position = typedThreeParameterProgram IntScalar LongScalar position
resultTypeProgram :: String
resultTypeProgram =
"class Catalog { public static String Select(int first, long second) { return \"selected\"; } public static String Select(long first, int second) { return \"other\"; } } class Caller { String Invoke(int first, long second) { return Catalog.Select(first, second); } }"
method :: ScalarType -> ScalarType -> String -> String -> String
method first second firstName secondName =
"public static int Select("
++ scalarTypeName first
++ " "
++ firstName
++ ", "
++ scalarTypeName second
++ " "
++ secondName
++ ") { return 1; } "
parameters :: [ScalarType] -> [String] -> String
parameters types names =
joinComma [scalarTypeName valueType ++ " " ++ name | (valueType, name) <- zip types names]
joinComma :: [String] -> String
joinComma [] = ""
joinComma [value] = value
joinComma (value : remaining) = value ++ ", " ++ joinComma remaining
replaceAt :: Int -> value -> [value] -> [value]
replaceAt position replacement values =
[if index == position then replacement else value | (index, value) <- zip [0 ..] values]
nextScalar :: ScalarType -> ScalarType
nextScalar value = case dropWhile (/= value) scalarTypes of
_ : following : _ -> following
_ -> case scalarTypes of
first : _ -> first
[] -> value
vectorName :: [ScalarType] -> String
vectorName = joinComma . map scalarTypeName
orderName :: Bool -> String
orderName False = "forward declaration order"
orderName True = "reverse declaration order"
selectedParameterTypes :: String -> Maybe [Type]
selectedParameterTypes source = do
(_, parametersValue) <- selectedDeclaration source
pure (map parameterAnnotation parametersValue)
selectedFunctionType :: String -> Maybe Type
selectedFunctionType source = do
(target, _) <- selectedDeclaration source
result <- either (const Nothing) Just (analyze source)
call <- findCall (semanticTypedAST result)
case call of
CallExpression _ (NameExpression _ name functionType) _ _
| resolvedSymbol name == resolvedSymbol target -> Just functionType
_ -> Nothing
selectedDeclaration :: String -> Maybe (ResolvedName, [Parameter ResolvedName Type])
selectedDeclaration source = do
result <- either (const Nothing) Just (analyze source)
call <- findCall (semanticTypedAST result)
target <- case call of
CallExpression _ (NameExpression _ name _) _ _ -> Just name
_ -> Nothing
declaration <- find (sameIdentity target) (methodDeclarations (semanticTypedAST result))
case declaration of
FunctionDeclaration {declarationName = name, declarationParameters = parametersValue} -> Just (name, parametersValue)
_ -> Nothing
where
sameIdentity target FunctionDeclaration {declarationName = candidate} = resolvedSymbol target == resolvedSymbol candidate
sameIdentity _ _ = False
methodDeclarations :: TypedAST -> [Declaration ResolvedName Type]
methodDeclarations (TypedAST (SyntaxTree _ declarations)) = concatMap membersOf declarations
where
membersOf declaration@TypeDeclaration {} = declaration : concatMap membersOf (typeMembers declaration)
membersOf declaration@TemplateTypeDeclaration {} = declaration : concatMap membersOf (typeMembers declaration)
membersOf declaration@FunctionDeclaration {} = [declaration]
membersOf EnumDeclaration {} = []
findCall :: TypedAST -> Maybe (Expression ResolvedName Type)
findCall (TypedAST (SyntaxTree _ declarations)) = find isSelected (concatMap callsInDeclaration declarations)
where
isSelected (CallExpression _ (NameExpression _ name _) _ _) = resolvedSpelling name == Identifier "Select"
isSelected _ = False
callsInDeclaration :: Declaration ResolvedName Type -> [Expression ResolvedName Type]
callsInDeclaration declaration = case declaration of
TypeDeclaration {typeMembers = members} -> concatMap callsInDeclaration members
TemplateTypeDeclaration {typeMembers = members} -> concatMap callsInDeclaration members
FunctionDeclaration {declarationBody = body} -> callsInBlock body
EnumDeclaration {} -> []
callsInBlock :: Block ResolvedName Type -> [Expression ResolvedName Type]
callsInBlock (Block statements) = concatMap callsInStatement statements
callsInStatement :: Statement ResolvedName Type -> [Expression ResolvedName Type]
callsInStatement statement = case statement of
BindingStatement _ _ _ _ _ value -> callsInExpression value
AssignmentStatement _ _ _ value -> callsInExpression value
ReturnStatement _ value -> maybe [] callsInExpression value
IfStatement _ condition yes no -> callsInExpression condition ++ callsInBlock yes ++ maybe [] callsInBlock no
WhileStatement _ condition body -> callsInExpression condition ++ callsInBlock body
DoWhileStatement _ body condition -> callsInBlock body ++ callsInExpression condition
ForStatement _ initializer condition updates body ->
maybe [] callsInStatement initializer
++ maybe [] callsInExpression condition
++ concatMap callsInStatement updates
++ callsInBlock body
ForEachStatement _ _ _ _ _ source body -> callsInExpression source ++ callsInBlock body
IncrementStatement {} -> []
CompoundAssignmentStatement _ _ _ _ value -> callsInExpression value
DiscardStatement _ value -> callsInExpression value
BreakStatement _ value -> maybe [] callsInExpression value
ContinueStatement {} -> []
GuardStatement _ condition block -> callsInExpression condition ++ callsInBlock block
BlockStatement _ block -> callsInBlock block
ExpressionStatement _ value _ -> callsInExpression value
callsInExpression :: Expression ResolvedName Type -> [Expression ResolvedName Type]
callsInExpression expression = case expression of
NameExpression {} -> []
LiteralExpression {} -> []
MemberAccessExpression _ receiver _ _ -> callsInExpression receiver
MethodReferenceExpression _ receiver _ _ -> callsInExpression receiver
CallExpression _ callee arguments _ ->
[expression | isSelectedCall expression]
++ callsInExpression callee
++ concatMap callsInExpression arguments
UnaryExpression _ _ value _ -> callsInExpression value
BinaryExpression _ _ left right _ -> callsInExpression left ++ callsInExpression right
IsPatternExpression _ subject _ _ -> callsInExpression subject
ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap callsInExpression [condition, whenTrue, whenFalse]
CoalesceExpression _ left fallback _ -> callsInExpression left ++ callsInExpression fallback
AssignmentExpression _ _ _ value _ -> callsInExpression value
IncrementExpression {} -> []
LoopExpression _ loop _ -> callsInStatement loop
BlockExpression _ block _ -> callsInBlock block
MatchExpression _ subjects arms _ -> concatMap callsInExpression (subjects ++ concatMap matchArmExpressions arms)
CallableExpression _ _ _ _ body _ -> callsInCallableBody body
where
isSelectedCall (CallExpression _ (NameExpression _ name _) _ _) = resolvedSpelling name == Identifier "Select"
isSelectedCall _ = False
callsInCallableBody :: CallableBody ResolvedName Type -> [Expression ResolvedName Type]
callsInCallableBody body = case body of
CallableExpressionBody expression -> callsInExpression expression
CallableBlockBody block -> callsInBlock block
analyze :: String -> Either [Diagnostic] SemanticArtifacts
analyze = analyzeSemantics . CompilerInput "static-member-overloads.tala"
hasDiagnostic :: String -> String -> Bool
hasDiagnostic code source = case analyze source of
Left diagnostics -> any ((== code) . diagnosticCode) diagnostics
Right _ -> False