packages feed

tala-compiler-0.1.0: test/StaticMemberSemanticTests.hs

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

{- | Semantic conformance tests for type-qualified static method calls.

The source fragments intentionally use only declaration and call forms already
present in Spec/Language/Decls.tala. These tests pin down candidate collection,
exact parameter matching, access filtering, diagnostics, and the stable symbol
that must survive from semantic analysis into Core.
-}
module StaticMemberSemanticTests (staticMemberSemanticTests) where

import Progmasoft.Tala.AST
import Progmasoft.Tala.BuiltinTypes
import Progmasoft.Tala.Compiler
import Progmasoft.Tala.Core
import Progmasoft.Tala.Diagnostic
import Progmasoft.Tala.Frontend

staticMemberSemanticTests :: [(String, Bool)]
staticMemberSemanticTests =
    [ ("a single public static method resolves", singlePublicMethod)
    , ("a default-access static method resolves in its source unit", defaultAccessMethod)
    , ("an internal static method resolves in its source unit", internalMethod)
    , ("a private static method resolves in its declaring class", privateMethodWithinOwner)
    , ("a protected static method resolves in its declaring class", protectedMethodWithinOwner)
    , ("a private static method is hidden from another class", privateMethodOutsideOwner)
    , ("a protected static method is hidden from an unrelated class", protectedMethodOutsideOwner)
    , ("an instance method is not callable through the type surface", instanceMethodThroughType)
    , ("a static method is not callable through a value receiver", staticMethodThroughValue)
    , ("a local identifier that shadows a type is not a type receiver", localShadowsType)
    , ("a missing member reports the member-not-found diagnostic", missingMethod)
    , ("a case-mismatched member reports the member-not-found diagnostic", caseSensitiveMember)
    , ("a bare member selector is rejected until it denotes a method call", bareMethodSelector)
    , ("a method reference is the method and not its result", methodReferenceAsResult)
    , ("a member selected on a value is rejected until members of values exist", valueMemberSelector)
    , ("an overload call with too few arguments reports arity", tooFewArguments)
    , ("an overload call with too many arguments reports arity", tooManyArguments)
    , ("an overload call rejects a mismatched argument type", mismatchedArgumentType)
    , ("an overload call rejects an implicit numeric conversion", implicitNumericConversionRejected)
    , ("an overload call rejects a String-to-scalar conversion", stringConversionRejected)
    , ("an overload call rejects a scalar-to-String conversion", scalarConversionRejected)
    , ("distinct parameter lists form a valid overload set", distinctParameterLists)
    , ("same-name methods on different owners remain independent", overloadFamiliesAreOwnerScoped)
    , ("the selected overload is stored as a stable symbol reference", selectedSymbolIsRecorded)
    , ("each overload declaration keeps its own SymbolId", overloadDeclarationsHaveDistinctIds)
    , ("method symbols remain distinct across type declarations", methodsAcrossOwnersHaveDistinctIds)
    , ("the Core call target agrees with the selected overload symbol", coreCallTargetAgrees)
    , ("overload selection is independent of declaration order", declarationOrderDoesNotSelect)
    , ("equal arity does not imply equal overload viability", equalArityUsesTypes)
    , ("a call can select the second overload after rejecting the first", secondCandidateWins)
    , ("a call can select the first overload after rejecting the second", firstCandidateWins)
    , ("an overloaded method remains callable from its owner", ownerCallUsesOverloadSet)
    , ("an overload selected by a typed local preserves its annotation", selectedAnnotationIsKept)
    , ("a method returning void remains a valid call statement", voidOverloadCall)
    , ("an overload returning a value can be used as a return expression", valueOverloadReturn)
    , ("an overload returning a value can initialize a typed local", valueOverloadInitializer)
    , ("a selector receiver is checked before member lookup", receiverMustNameDeclaredType)
    , ("a local value receiver does not become a dynamic dispatch", valueReceiverIsNotDynamic)
    , ("a nested call receiver is not silently treated as a type", nestedCallReceiverNotType)
    , ("a qualified selector is rejected when the type is absent", unknownTypeReceiver)
    , ("a public overload remains selectable beside an inaccessible overload", publicCandidateSurvivesPrivateSibling)
    , ("a private-only overload family reports access failure", privateOnlyFamilyReportsAccess)
    , ("access modifiers do not create a second overload identity", accessDoesNotDistinguishSignature)
    , ("return types do not create a second overload identity", returnDoesNotDistinguishSignature)
    , ("static and instance declarations do not create distinct signatures", staticnessDoesNotDistinguishSignature)
    , ("parameter order contributes to overload identity", parameterOrderIsSignificant)
    , ("different owners may reuse a member spelling and signature", ownerScopePermitsSameSignature)
    , ("a type name is case-sensitive", typeNameIsCaseSensitive)
    , ("a returned call has the selected method result type", returnedCallHasResultType)
    , ("a value receiver's own type is not guessed from its spelling", valueReceiverTypeIsNotGuessed)
    , ("unqualified owner calls share the declared overload family", ownerUnqualifiedCall)
    , ("unqualified calls do not resolve methods on other classes", foreignUnqualifiedCallIsRejected)
    , ("selector diagnostics retain a source span", methodDiagnosticHasSpan)
    , ("selector diagnostics belong to the type-checker stage", methodDiagnosticHasStage)
    , ("selector diagnostics are errors rather than warnings", methodDiagnosticSeverity)
    ]
        ++ scalarPairSelectionTests
        ++ scalarDuplicateSignatureTests
        ++ scalarArityTests

singlePublicMethod :: Bool
singlePublicMethod = succeeds (staticProgram "public static int Read() { return 7; }" "int Use() { return Catalog.Read(); }")

defaultAccessMethod :: Bool
defaultAccessMethod = succeeds (staticProgram "static int Read() { return 7; }" "int Use() { return Catalog.Read(); }")

internalMethod :: Bool
internalMethod = succeeds (staticProgram "internal static int Read() { return 7; }" "int Use() { return Catalog.Read(); }")

privateMethodWithinOwner :: Bool
privateMethodWithinOwner =
    succeeds ("class Catalog { private static int Read() { return 7; } int Use() { return Catalog.Read(); } }")

protectedMethodWithinOwner :: Bool
protectedMethodWithinOwner =
    succeeds ("class Catalog { protected static int Read() { return 7; } int Use() { return Catalog.Read(); } }")

privateMethodOutsideOwner :: Bool
privateMethodOutsideOwner =
    hasCode
        "TAT0033"
        "class Catalog { private static int Read() { return 7; } } class Caller { int Use() { return Catalog.Read(); } }"

protectedMethodOutsideOwner :: Bool
protectedMethodOutsideOwner =
    hasCode
        "TAT0033"
        "class Catalog { protected static int Read() { return 7; } } class Caller { int Use() { return Catalog.Read(); } }"

instanceMethodThroughType :: Bool
instanceMethodThroughType = hasCode "TAT0031" "class Catalog { int Read() { return 7; } } class Caller { int Use() { return Catalog.Read(); } }"

staticMethodThroughValue :: Bool
staticMethodThroughValue =
    hasCode
        "TAT0032"
        "class Catalog { public static int Read() { return 7; } } class Caller { int Use(Catalog value) { return value.Read(); } }"

localShadowsType :: Bool
localShadowsType =
    hasCode
        "TAT0032"
        "class Catalog { public static int Read() { return 7; } } class Caller { int Use(int Catalog) { return Catalog.Read(); } }"

missingMethod :: Bool
missingMethod = hasCode "TAT0029" "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"

caseSensitiveMember :: Bool
caseSensitiveMember =
    hasCode
        "TAT0029"
        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { return Catalog.read(); } }"

bareMethodSelector :: Bool
bareMethodSelector =
    hasCode
        "TAT0034"
        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { return Catalog.Read; } }"

-- The reference is the method itself, so returning it as an int is a
-- mismatch of types.
methodReferenceAsResult :: Bool
methodReferenceAsResult =
    hasCode
        "TAT0005"
        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { return Catalog::Read; } }"

valueMemberSelector :: Bool
valueMemberSelector =
    hasCode
        "TAT0034"
        "class Catalog { public static int Read() { return 7; } } class Caller { int Use() { int value = 1; return value.Read; } }"

tooFewArguments :: Bool
tooFewArguments =
    hasCode
        "TAT0008"
        "class Catalog { public static int Read(int first, int second) { return first; } } class Caller { int Use() { return Catalog.Read(1); } }"

tooManyArguments :: Bool
tooManyArguments =
    hasCode
        "TAT0008"
        "class Catalog { public static int Read(int first) { return first; } } class Caller { int Use() { return Catalog.Read(1, 2); } }"

mismatchedArgumentType :: Bool
mismatchedArgumentType =
    hasCode
        "TAT0009"
        "class Catalog { public static int Read(String text) { return 1; } } class Caller { int Use() { return Catalog.Read(1); } }"

implicitNumericConversionRejected :: Bool
implicitNumericConversionRejected =
    hasCode
        "TAT0009"
        "class Catalog { public static int Read(long value) { return 1; } } class Caller { int Use(int value) { return Catalog.Read(value); } }"

stringConversionRejected :: Bool
stringConversionRejected =
    hasCode
        "TAT0009"
        "class Catalog { public static int Read(String text) { return 1; } } class Caller { int Use(char value) { return Catalog.Read(value); } }"

scalarConversionRejected :: Bool
scalarConversionRejected =
    hasCode
        "TAT0009"
        "class Catalog { public static int Read(int value) { return value; } } class Caller { int Use(String value) { return Catalog.Read(value); } }"

distinctParameterLists :: Bool
distinctParameterLists =
    succeeds
        "class Catalog { public static int Read(int value) { return value; } public static int Read(String value) { return 1; } } class Caller { int Use(int value) { return Catalog.Read(value); } }"

overloadFamiliesAreOwnerScoped :: Bool
overloadFamiliesAreOwnerScoped =
    succeeds
        "class First { public static int Read(int value) { return value; } } class Second { public static int Read(int value) { return value; } } class Caller { int Use() { return First.Read(1) + Second.Read(2); } }"

selectedSymbolIsRecorded :: Bool
selectedSymbolIsRecorded =
    sameSuccessfulSymbol
        (selectedMethodSymbol "int" selectedIntLongProgram)
        (expectedSelectedMethodSymbol "int" selectedIntLongProgram)

overloadDeclarationsHaveDistinctIds :: Bool
overloadDeclarationsHaveDistinctIds = case methodDeclarationSymbols "Select" selectedIntLongProgram of
    [first, second] -> first /= second
    _ -> False

methodsAcrossOwnersHaveDistinctIds :: Bool
methodsAcrossOwnersHaveDistinctIds = case methodDeclarationSymbols "Read" source of
    [first, second] -> first /= second
    _ -> False
    where
        source =
            "class First { public static int Read(int value) { return value; } } class Second { public static int Read(int value) { return value; } }"

coreCallTargetAgrees :: Bool
coreCallTargetAgrees = case compileToCorePrep (CompilerInput "static-member.tala" selectedIntLongProgram) of
    Right artifacts ->
        case [ resolvedSymbol target
             | target <- coreCallTargets (artifactCore artifacts)
             , resolvedSpelling target == Identifier "Select"
             ] of
            selected : _ -> Just selected == expectedSelectedMethodSymbol "int" selectedIntLongProgram
            [] -> False
    Left _ -> False

declarationOrderDoesNotSelect :: Bool
declarationOrderDoesNotSelect =
    sameSuccessfulSymbol
        (selectedMethodSymbol "int" selectedIntLongProgram)
        (expectedSelectedMethodSymbol "int" selectedIntLongProgram)
        && sameSuccessfulSymbol
            (selectedMethodSymbol "long" selectedLongIntProgram)
            (expectedSelectedMethodSymbol "long" selectedLongIntProgram)

equalArityUsesTypes :: Bool
equalArityUsesTypes = succeeds selectedIntLongProgram && succeeds selectedLongIntProgram

secondCandidateWins :: Bool
secondCandidateWins =
    sameSuccessfulSymbol
        (selectedMethodSymbol "int" selectedIntLongProgram)
        (expectedSelectedMethodSymbol "int" selectedIntLongProgram)

firstCandidateWins :: Bool
firstCandidateWins =
    sameSuccessfulSymbol
        (selectedMethodSymbol "long" selectedLongIntProgram)
        (expectedSelectedMethodSymbol "long" selectedLongIntProgram)

ownerCallUsesOverloadSet :: Bool
ownerCallUsesOverloadSet =
    sameSuccessfulSymbol
        (selectedMethodSymbol "int" source)
        (expectedSelectedMethodSymbol "int" source)
    where
        source =
            "class Catalog { public static int Select(long value) { return 1; } public static int Select(int value) { return value; } int Use(int value) { return Select(value); } }"

selectedAnnotationIsKept :: Bool
selectedAnnotationIsKept = case analyze
    "class Catalog { public static int Select(int value) { return value; } int Use(int value) { Select(value); return 0; } }" of
    Right result -> case callsNamed "Select" (typedTree result) of
        CallExpression _ (NameExpression _ target (FunctionType [parameter] resultType)) [NameExpression _ _ argumentType] _ : _ ->
            parameter == intType
                && resultType == intType
                && argumentType == intType
                && Just (resolvedSymbol target)
                    == expectedSelectedMethodSymbol
                        "int"
                        "class Catalog { public static int Select(int value) { return value; } int Use(int value) { Select(value); return 0; } }"
        _ -> False
    Left _ -> False

voidOverloadCall :: Bool
voidOverloadCall =
    succeeds
        "class Catalog { public static void Select(int value) { return; } public static void Select(String value) { return; } } class Caller { void Use(int value) { Catalog.Select(value); return; } }"

valueOverloadReturn :: Bool
valueOverloadReturn = succeeds selectedIntLongProgram

valueOverloadInitializer :: Bool
valueOverloadInitializer =
    succeeds
        "class Catalog { public static int Select(int value) { return value; } public static long Select(long value) { return value; } } class Caller { int Use(int value) { int chosen = Catalog.Select(value); return chosen; } }"

receiverMustNameDeclaredType :: Bool
receiverMustNameDeclaredType = hasCode "TAN0001" "class Caller { int Use() { return Missing.Read(); } }"

valueReceiverIsNotDynamic :: Bool
valueReceiverIsNotDynamic = staticMethodThroughValue

nestedCallReceiverNotType :: Bool
nestedCallReceiverNotType =
    hasCode
        "TAT0032"
        "class Catalog { public static Catalog Create(Catalog value) { return value; } public static int Read() { return 1; } } class Caller { int Use(Catalog value) { return Catalog.Create(value).Read(); } }"

unknownTypeReceiver :: Bool
unknownTypeReceiver = receiverMustNameDeclaredType

publicCandidateSurvivesPrivateSibling :: Bool
publicCandidateSurvivesPrivateSibling =
    succeeds
        "class Catalog { private static int Select(String value) { return 1; } public static int Select(int value) { return value; } } class Caller { int Use(int value) { return Catalog.Select(value); } }"

privateOnlyFamilyReportsAccess :: Bool
privateOnlyFamilyReportsAccess = privateMethodOutsideOwner

accessDoesNotDistinguishSignature :: Bool
accessDoesNotDistinguishSignature =
    hasCode
        "TAT0028"
        "class Catalog { public static int Select(int value) { return value; } private static int Select(int other) { return other; } }"

returnDoesNotDistinguishSignature :: Bool
returnDoesNotDistinguishSignature =
    hasCode
        "TAT0028"
        "class Catalog { public static int Select(int value) { return value; } public static long Select(int other) { return other; } }"

staticnessDoesNotDistinguishSignature :: Bool
staticnessDoesNotDistinguishSignature =
    hasCode
        "TAT0028"
        "class Catalog { public static int Select(int value) { return value; } public int Select(int other) { return other; } }"

parameterOrderIsSignificant :: Bool
parameterOrderIsSignificant =
    succeeds
        "class Catalog { public static int Select(int left, long right) { return left; } public static long Select(long left, int right) { return left; } } class Caller { int Use(int left, long right) { return Catalog.Select(left, right); } }"

ownerScopePermitsSameSignature :: Bool
ownerScopePermitsSameSignature = overloadFamiliesAreOwnerScoped

typeNameIsCaseSensitive :: Bool
typeNameIsCaseSensitive =
    hasCode
        "TAN0001"
        "class Catalog { public static int Read() { return 1; } } class Caller { int Use() { return catalog.Read(); } }"

returnedCallHasResultType :: Bool
returnedCallHasResultType = selectedAnnotationIsKept

valueReceiverTypeIsNotGuessed :: Bool
valueReceiverTypeIsNotGuessed = staticMethodThroughValue

ownerUnqualifiedCall :: Bool
ownerUnqualifiedCall = ownerCallUsesOverloadSet

foreignUnqualifiedCallIsRejected :: Bool
foreignUnqualifiedCallIsRejected =
    hasCode
        "TAN0001"
        "class Catalog { public static int Select(int value) { return value; } } class Caller { int Use(int value) { return Select(value); } }"

methodDiagnosticHasSpan :: Bool
methodDiagnosticHasSpan =
    hasDiagnostic
        "TAT0029"
        (\diagnostic -> diagnosticSpan diagnostic /= Nothing)
        "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"

methodDiagnosticHasStage :: Bool
methodDiagnosticHasStage =
    hasDiagnostic
        "TAT0029"
        ((== TypeCheckerStage) . diagnosticStage)
        "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"

methodDiagnosticSeverity :: Bool
methodDiagnosticSeverity =
    hasDiagnostic
        "TAT0029"
        ((== Error) . diagnosticSeverity)
        "class Catalog {} class Caller { int Use() { return Catalog.Read(); } }"

selectedIntLongProgram :: String
selectedIntLongProgram =
    "class Catalog { public static int Select(long value) { return 1; } public static int Select(int value) { return value; } } class Caller { int Use(int value) { return Catalog.Select(value); } }"

selectedLongIntProgram :: String
selectedLongIntProgram =
    "class Catalog { public static long Select(int value) { return 1; } public static long Select(long value) { return value; } } class Caller { long Use(long value) { return Catalog.Select(value); } }"

sameSuccessfulSymbol :: Maybe SymbolId -> Maybe SymbolId -> Bool
sameSuccessfulSymbol (Just first) (Just second) = first == second
sameSuccessfulSymbol _ _ = False

methodDeclarationSymbols :: String -> String -> [SymbolId]
methodDeclarationSymbols name = map (resolvedSymbol . declarationName) . filter isSelected . typedDeclarations
    where
        isSelected FunctionDeclaration {declarationName = candidate} = resolvedSpelling candidate == Identifier name
        isSelected _ = False

scalarPairSelectionTests :: [(String, Bool)]
scalarPairSelectionTests =
    [ ( "exact overload resolution selects " ++ scalarTypeName requested ++ " over " ++ scalarTypeName distractor
      , sameSuccessfulSymbol
            (selectedMethodSymbol (scalarTypeName requested) source)
            (expectedSelectedMethodSymbol (scalarTypeName requested) source)
      )
    | requested <- scalarTypes
    , distractor <- scalarTypes
    , requested /= distractor
    , let source = scalarPairProgram (scalarTypeName requested) (scalarTypeName distractor)
    ]

scalarDuplicateSignatureTests :: [(String, Bool)]
scalarDuplicateSignatureTests =
    [ ( "duplicate " ++ scalarTypeName valueType ++ " parameter signatures are rejected"
      , hasCode "TAT0028" (scalarDuplicateProgram (scalarTypeName valueType))
      )
    | valueType <- scalarTypes
    ]

scalarArityTests :: [(String, Bool)]
scalarArityTests =
    [ ( "an " ++ scalarTypeName valueType ++ " overload preserves its wrong-arity diagnostic"
      , hasCode "TAT0008" (scalarArityProgram (scalarTypeName valueType))
      )
    | valueType <- scalarTypes
    ]

scalarPairProgram :: String -> String -> String
scalarPairProgram requested distractor =
    "class Catalog { public static void Select("
        ++ distractor
        ++ " other) { return; } public static void Select("
        ++ requested
        ++ " other) { return; } public static void Invoke("
        ++ requested
        ++ " value) { Select(value); return; } }"

scalarDuplicateProgram :: String -> String
scalarDuplicateProgram valueType =
    "class Catalog { public static void Select("
        ++ valueType
        ++ " value) { return; } public static void Select("
        ++ valueType
        ++ " other) { return; } }"

scalarArityProgram :: String -> String
scalarArityProgram valueType =
    "class Catalog { public static void Select("
        ++ valueType
        ++ " value) { return; } } class Caller { void Invoke() { Catalog.Select(); return; } }"

selectedMethodSymbol :: String -> String -> Maybe SymbolId
selectedMethodSymbol selectedType source = do
    result <- either (const Nothing) Just (analyze source)
    selectedCall <- firstMatch (callsNamed "Select" (typedTree result))
    functionName <- case selectedCall of
        CallExpression _ (NameExpression _ name _) _ _ -> Just name
        _ -> Nothing
    pure (resolvedSymbol functionName)
    where
        firstMatch [] = Nothing
        firstMatch (call : remaining) = case call of
            CallExpression _ (NameExpression _ name _) _ _
                | any (selectedDeclaration name) (typedDeclarations source) -> Just call
            _ -> firstMatch remaining
        selectedDeclaration name declaration = case declaration of
            FunctionDeclaration {declarationName = candidate, declarationParameters = [parameter]} ->
                resolvedSymbol candidate == resolvedSymbol name
                    && parameterAnnotation parameter == scalarTypeToType (scalarForName selectedType)
                    && resolvedSpelling candidate == Identifier "Select"
            _ -> False

expectedSelectedMethodSymbol :: String -> String -> Maybe SymbolId
expectedSelectedMethodSymbol selectedType source = do
    declaration <- firstMatch (typedDeclarations source)
    pure (resolvedSymbol (declarationName declaration))
    where
        firstMatch [] = Nothing
        firstMatch (declaration : remaining) = case declaration of
            FunctionDeclaration {declarationName = name, declarationParameters = [parameter]}
                | resolvedSpelling name == Identifier "Select"
                , parameterAnnotation parameter == scalarTypeToType (scalarForName selectedType) ->
                    Just declaration
            _ -> firstMatch remaining

scalarForName :: String -> ScalarType
scalarForName spelling = case [scalar | scalar <- scalarTypes, scalarTypeName scalar == spelling] of
    scalar : _ -> scalar
    [] -> IntScalar

typedDeclarations :: String -> [Declaration ResolvedName Type]
typedDeclarations source = case analyze source of
    Right result -> flattenDeclarations (syntaxDeclarations (typedSyntaxTree (typedTree result)))
    Left _ -> []

flattenDeclarations :: [Declaration name annotation] -> [Declaration name annotation]
flattenDeclarations declarations = concatMap flatten declarations
    where
        flatten declaration@TypeDeclaration {} = declaration : flattenDeclarations (typeMembers declaration)
        flatten declaration@TemplateTypeDeclaration {} = declaration : flattenDeclarations (typeMembers declaration)
        flatten declaration = [declaration]

analyze :: String -> Either [Diagnostic] SemanticArtifacts
analyze = analyzeSemantics . CompilerInput "static-member.tala"

typedTree :: SemanticArtifacts -> TypedAST
typedTree = semanticTypedAST

succeeds :: String -> Bool
succeeds = either (const False) (const True) . analyze

hasCode :: String -> String -> Bool
hasCode code = hasDiagnostic code (const True)

hasDiagnostic :: String -> (Diagnostic -> Bool) -> String -> Bool
hasDiagnostic code predicate source = case analyze source of
    Left diagnostics -> any (\diagnostic -> diagnosticCode diagnostic == code && predicate diagnostic) diagnostics
    Right _ -> False

staticProgram :: String -> String -> String
staticProgram method caller = "class Catalog { " ++ method ++ " } class Caller { " ++ caller ++ " }"

callsNamed :: String -> TypedAST -> [Expression ResolvedName Type]
callsNamed name (TypedAST (SyntaxTree _ declarations)) = filter isNamedCall (concatMap declarationCalls declarations)
    where
        isNamedCall (CallExpression _ (NameExpression _ target _) _ _) = resolvedSpelling target == Identifier name
        isNamedCall _ = False

declarationCalls :: Declaration ResolvedName Type -> [Expression ResolvedName Type]
declarationCalls declaration = case declaration of
    TypeDeclaration {typeMembers = members} -> concatMap declarationCalls members
    TemplateTypeDeclaration {typeMembers = members} -> concatMap declarationCalls members
    FunctionDeclaration {declarationBody = body} -> blockCalls body
    EnumDeclaration {} -> []

blockCalls :: Block ResolvedName Type -> [Expression ResolvedName Type]
blockCalls (Block statements) = concatMap statementCalls statements

statementCalls :: Statement ResolvedName Type -> [Expression ResolvedName Type]
statementCalls statement = case statement of
    BindingStatement _ _ _ _ _ value -> expressionCalls value
    AssignmentStatement _ _ _ value -> expressionCalls value
    ReturnStatement _ value -> maybe [] expressionCalls value
    IfStatement _ condition yes no -> expressionCalls condition ++ blockCalls yes ++ maybe [] blockCalls no
    WhileStatement _ condition body -> expressionCalls condition ++ blockCalls body
    DoWhileStatement _ body condition -> blockCalls body ++ expressionCalls condition
    ForStatement _ initializer condition updates body ->
        maybe [] statementCalls initializer
            ++ maybe [] expressionCalls condition
            ++ concatMap statementCalls updates
            ++ blockCalls body
    ForEachStatement _ _ _ _ _ source body -> expressionCalls source ++ blockCalls body
    IncrementStatement {} -> []
    CompoundAssignmentStatement _ _ _ _ value -> expressionCalls value
    DiscardStatement _ value -> expressionCalls value
    BreakStatement _ value -> maybe [] expressionCalls value
    ContinueStatement {} -> []
    GuardStatement _ condition block -> expressionCalls condition ++ blockCalls block
    BlockStatement _ block -> blockCalls block
    ExpressionStatement _ value _ -> expressionCalls value

expressionCalls :: Expression ResolvedName Type -> [Expression ResolvedName Type]
expressionCalls expression = case expression of
    NameExpression {} -> []
    LiteralExpression {} -> []
    MemberAccessExpression _ receiver _ _ -> expressionCalls receiver
    MethodReferenceExpression _ receiver _ _ -> expressionCalls receiver
    CallExpression _ callee arguments _ ->
        [expression | isCall expression] ++ expressionCalls callee ++ concatMap expressionCalls arguments
    UnaryExpression _ _ value _ -> expressionCalls value
    BinaryExpression _ _ left right _ -> expressionCalls left ++ expressionCalls right
    IsPatternExpression _ subject _ _ -> expressionCalls subject
    ConditionalExpression _ condition whenTrue whenFalse _ -> concatMap expressionCalls [condition, whenTrue, whenFalse]
    CoalesceExpression _ left fallback _ -> expressionCalls left ++ expressionCalls fallback
    AssignmentExpression _ _ _ value _ -> expressionCalls value
    IncrementExpression {} -> []
    LoopExpression _ loop _ -> statementCalls loop
    BlockExpression _ block _ -> blockCalls block
    MatchExpression _ subjects arms _ -> concatMap expressionCalls (subjects ++ concatMap matchArmExpressions arms)
    CallableExpression _ _ _ _ body _ -> callableBodyCalls body
    where
        isCall CallExpression {} = True
        isCall _ = False

callableBodyCalls :: CallableBody ResolvedName Type -> [Expression ResolvedName Type]
callableBodyCalls body = case body of
    CallableExpressionBody expression -> expressionCalls expression
    CallableBlockBody block -> blockCalls block

coreCallTargets :: CoreModule -> [ResolvedName]
coreCallTargets moduleValue = concatMap (concatMap statementTargets . coreFunctionBody) (coreModuleFunctions moduleValue)

statementTargets :: CoreStatement -> [ResolvedName]
statementTargets statement = case statement of
    CoreBind binding -> expressionTargets (coreBindingValue binding)
    CoreAssign _ value -> expressionTargets value
    CoreReturn value -> expressionTargets value
    CoreIf condition yes no -> expressionTargets condition ++ concatMap statementTargets yes ++ concatMap statementTargets no
    CoreEvaluate value -> expressionTargets value
    CoreWhile condition body -> expressionTargets condition ++ concatMap statementTargets body
    CoreDoWhile body condition -> concatMap statementTargets body ++ expressionTargets condition
    CoreFor condition updates body -> expressionTargets condition ++ concatMap statementTargets updates ++ concatMap statementTargets body
    CoreBreak -> []
    CoreContinue -> []

expressionTargets :: CoreExpression -> [ResolvedName]
expressionTargets expression = case expression of
    CoreVariable {} -> []
    CoreLiteral {} -> []
    CoreApply callee arguments _ -> calledTarget callee ++ expressionTargets callee ++ concatMap expressionTargets arguments
    CorePrimitive _ arguments _ -> concatMap expressionTargets arguments
    CoreLet _ _ value body _ -> expressionTargets value ++ expressionTargets body
    CoreConditional condition whenTrue whenFalse _ -> concatMap expressionTargets [condition, whenTrue, whenFalse]
    CoreClosure _ _ _ statements _ -> concatMap statementTargets statements
    where
        calledTarget (CoreVariable name _) = [name]
        calledTarget _ = []