packages feed

tala-compiler-0.1.0: test/TemplateDiscoveryTests.hs

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

module TemplateDiscoveryTests (templateDiscoveryTests) where

import Data.Functor.Identity (runIdentity)
import Data.List (isInfixOf)
import Progmasoft.Tala.AST
import Progmasoft.Tala.Compiler
import Progmasoft.Tala.Frontend
import Progmasoft.Tala.Template.Application
import Progmasoft.Tala.Template.Discovery
import Progmasoft.Tala.Template.Specialization

templateDiscoveryTests :: [(String, Bool)]
templateDiscoveryTests =
    [ ("discovery finds a concrete template parameter type", discoversParameterType)
    , ("discovery emits layout-only demand", discoveryIsLayoutOnly)
    , ("discovery retains concrete type arguments", discoveryRetainsTypeArguments)
    , ("discovery retains concrete value arguments", discoveryRetainsValueArguments)
    , ("discovery resolves a namespace-relative template", resolvesNamespaceRelativeTemplate)
    , ("discovery retains a qualified template target", retainsQualifiedTemplateTarget)
    , ("discovery finds a function result template", discoversFunctionResult)
    , ("discovery finds a local binding template", discoversBindingType)
    , ("discovery finds nested template applications", discoversNestedApplications)
    , ("nested applications are ordered outer before inner", nestedApplicationOrderIsStable)
    , ("discovery records distinct source uses", repeatedUsesRemainDiagnosticEvidence)
    , ("open template declaration bodies are skipped", skipsOpenTemplateBodies)
    , ("ordinary named types do not create demands", ignoresOrdinaryTypes)
    , ("unrelated generic spelling does not create a demand", ignoresUnknownGenericNames)
    , ("discovery records visited type nodes", countsVisitedTypes)
    , ("discovery counts emitted applications", countsApplications)
    , ("discovery counts ordinary named types", countsIgnoredTypes)
    , ("discovery counts skipped open templates", countsSkippedTemplates)
    , ("origins identify the owning class", originNamesOwner)
    , ("origins identify the member", originNamesMember)
    , ("origins identify the parameter site", originNamesParameterSite)
    , ("origins include the source file", originNamesSourceFile)
    , ("origins include positive source coordinates", originHasCoordinates)
    , ("normal compilation stores discovery artifacts", compilerStoresDiscovery)
    , ("normal compilation plans discovered layouts", compilerPlansLayouts)
    , ("automatically planned layout omits template methods", automaticLayoutOmitsMethods)
    , ("automatically planned template type is closed", automaticPlanIsClosed)
    , ("ordinary programs retain an empty template plan", ordinaryProgramHasEmptyPlan)
    , ("default template arguments are materialized by planning", defaultArgumentsAreMaterialized)
    , ("type and value applications become separate plans", mixedApplicationsStaySeparate)
    , ("duplicate uses coalesce in the specialization plan", duplicateUsesCoalesceInPlan)
    , ("duplicate uses retain both discovery origins", duplicateUsesRetainOrigins)
    , ("discovery itself remains independent from coalescing", discoveryDoesNotCoalesce)
    , ("nested layout plans retain dependency order", nestedPlansHaveDependencyOrder)
    , ("template-only source reaches CorePrep with no emitted function", templateOnlyPipelineIsEmpty)
    ]

analyze :: String -> Maybe TypedAST
analyze source = case analyzeSemantics (CompilerInput "template-discovery-test.tala" source) of
    Right artifacts -> Just (semanticTypedAST artifacts)
    Left _ -> Nothing

discover :: String -> Maybe TemplateDemandDiscovery
discover source = discoverTemplateDemands <$> analyze source

compile :: String -> Maybe FrontendArtifacts
compile source = case compileToCorePrep (CompilerInput "template-discovery-test.tala" source) of
    Right artifacts -> Just artifacts
    Left _ -> Nothing

qualified :: [String] -> QualifiedName
qualified = QualifiedName . map Identifier

applicationTarget :: TemplateSpecializationDemand -> QualifiedName
applicationTarget = templateApplicationTarget . specializationDemandApplication

applicationArguments :: TemplateSpecializationDemand -> [TemplateArgument]
applicationArguments = templateApplicationArguments . specializationDemandApplication

targetTexts :: TemplateDemandDiscovery -> [[String]]
targetTexts =
    map (map identifierText . qualifiedNameParts . applicationTarget)
        . discoveredTemplateDemands

onlyDemand :: TemplateDemandDiscovery -> Maybe TemplateSpecializationDemand
onlyDemand discovery = case discoveredTemplateDemands discovery of
    [value] -> Just value
    _ -> Nothing

onlyOrigin :: TemplateDemandDiscovery -> Maybe TemplateDiscoveryOrigin
onlyOrigin discovery = case discoveredTemplateOrigins discovery of
    [value] -> Just value
    _ -> Nothing

onlySpecialization :: FrontendArtifacts -> Maybe TemplateSpecialization
onlySpecialization artifacts = case plannedTemplateSpecializations (artifactTemplateSpecializations artifacts) of
    [value] -> Just value
    _ -> Nothing

boxParameterSource :: String
boxParameterSource =
    unlines
        [ "template<typename T> class Box {"
        , "    T Read(_ T value) { return value; }"
        , "}"
        , "class App {"
        , "    void Use(_ Box<int> value) { return; }"
        , "}"
        ]

bufferParameterSource :: String
bufferParameterSource =
    unlines
        [ "template<typename T, int N> class Buffer {}"
        , "class App {"
        , "    void Use(_ Buffer<int, 64> value) { return; }"
        , "}"
        ]

namespacedSource :: String
namespacedSource =
    unlines
        [ "namespace Example;"
        , "template<typename T> class Box {}"
        , "class App { void Use(_ Box<String> value) { return; } }"
        ]

nestedSource :: String
nestedSource =
    unlines
        [ "template<typename T> class Box {}"
        , "template<typename T> class Holder {}"
        , "class App { void Use(_ Holder<Box<int>> value) { return; } }"
        ]

duplicateSource :: String
duplicateSource =
    unlines
        [ "template<typename T> class Box {}"
        , "class App {"
        , "    void First(_ Box<int> value) { return; }"
        , "    void Second(_ Box<int> value) { return; }"
        , "}"
        ]

discoversParameterType :: Bool
discoversParameterType = case discover boxParameterSource >>= onlyDemand of
    Just demand -> applicationTarget demand == qualified ["Box"]
    Nothing -> False

discoveryIsLayoutOnly :: Bool
discoveryIsLayoutOnly = case discover boxParameterSource >>= onlyDemand of
    Just demand -> specializationDemandScope demand == TemplateLayoutDemand
    Nothing -> False

discoveryRetainsTypeArguments :: Bool
discoveryRetainsTypeArguments = case discover boxParameterSource >>= onlyDemand of
    Just demand -> applicationArguments demand == [TypeTemplateArgument intType]
    Nothing -> False

discoveryRetainsValueArguments :: Bool
discoveryRetainsValueArguments = case discover bufferParameterSource >>= onlyDemand of
    Just demand ->
        applicationArguments demand
            == [TypeTemplateArgument intType, ValueTemplateArgument (IntegerTemplateValue 64)]
    Nothing -> False

resolvesNamespaceRelativeTemplate :: Bool
resolvesNamespaceRelativeTemplate = case discover namespacedSource >>= onlyDemand of
    Just demand -> applicationTarget demand == qualified ["Example", "Box"]
    Nothing -> False

retainsQualifiedTemplateTarget :: Bool
retainsQualifiedTemplateTarget = case discover source >>= onlyDemand of
    Just demand -> applicationTarget demand == qualified ["Example", "Box"]
    Nothing -> False
    where
        source =
            "namespace Example; template<typename T> class Box {} class App { void Use(_ Example.Box<int> value) { return; } }"

discoversFunctionResult :: Bool
discoversFunctionResult = case discover source of
    Just result ->
        any
            (\(demand, origin) -> applicationTarget demand == qualified ["Box"] && FunctionSignatureSite `elem` discoverySites origin)
            (zip (discoveredTemplateDemands result) (discoveredTemplateOrigins result))
    Nothing -> False
    where
        source =
            "template<typename T> class Box {} class App { Box<int> Identity(_ Box<int> value) { return value; } }"

discoversBindingType :: Bool
discoversBindingType = case discover source of
    Just result -> ["Box"] `elem` targetTexts result
    Nothing -> False
    where
        source =
            "template<typename T> class Box {} class App { void Use(_ Box<int> input) { Box<int> copy = input; return; } }"

discoversNestedApplications :: Bool
discoversNestedApplications = case discover nestedSource of
    Just result -> targetTexts result == [["Holder"], ["Box"]]
    Nothing -> False

nestedApplicationOrderIsStable :: Bool
nestedApplicationOrderIsStable = case (discover nestedSource, discover nestedSource) of
    (Just first, Just second) -> discoveredTemplateDemands first == discoveredTemplateDemands second
    _ -> False

repeatedUsesRemainDiagnosticEvidence :: Bool
repeatedUsesRemainDiagnosticEvidence = case discover duplicateSource of
    Just result -> length (discoveredTemplateDemands result) == 2 && length (discoveredTemplateOrigins result) == 2
    Nothing -> False

skipsOpenTemplateBodies :: Bool
skipsOpenTemplateBodies = case discover source of
    Just result -> null (discoveredTemplateDemands result) && skippedOpenTemplateBodies (templateDiscoveryStatistics result) == 1
    Nothing -> False
    where
        source = "template<typename T> class Box { T Read(_ T value) { return value; } }"

ignoresOrdinaryTypes :: Bool
ignoresOrdinaryTypes = case discover "class App { String Name() { return \"x\"; } }" of
    Just result -> null (discoveredTemplateDemands result)
    Nothing -> False

ignoresUnknownGenericNames :: Bool
ignoresUnknownGenericNames = case analyze boxParameterSource of
    Just typed ->
        let result = discoverTemplateDemands (renameBoxApplication "Missing" typed)
         in null (discoveredTemplateDemands result)
    Nothing -> False

renameBoxApplication :: String -> TypedAST -> TypedAST
renameBoxApplication replacement (TypedAST tree) = TypedAST tree {syntaxDeclarations = map rewriteDeclaration (syntaxDeclarations tree)}
    where
        rewriteDeclaration declaration = case declaration of
            TypeDeclaration spanValue name annotation members ->
                TypeDeclaration spanValue name (rewriteType annotation) (map rewriteDeclaration members)
            FunctionDeclaration spanValue name annotation syntax parameters body isStatic access ->
                FunctionDeclaration
                    spanValue
                    name
                    (rewriteType annotation)
                    syntax
                    (map rewriteParameter parameters)
                    (rewriteBlock body)
                    isStatic
                    access
            TemplateTypeDeclaration {} -> declaration
            EnumDeclaration {} -> declaration
        rewriteParameter parameter = parameter {parameterAnnotation = rewriteType (parameterAnnotation parameter)}
        rewriteBlock (Block statements) = Block (map rewriteStatement statements)
        rewriteStatement statement = case statement of
            BindingStatement spanValue kind syntax name annotation value ->
                BindingStatement spanValue kind syntax name (rewriteType annotation) (rewriteExpression value)
            AssignmentStatement spanValue name annotation value ->
                AssignmentStatement spanValue name (rewriteType annotation) (rewriteExpression value)
            ReturnStatement spanValue value -> ReturnStatement spanValue (rewriteExpression <$> value)
            IfStatement spanValue condition yes no ->
                IfStatement spanValue (rewriteExpression condition) (rewriteBlock yes) (rewriteBlock <$> no)
            WhileStatement spanValue condition body ->
                WhileStatement spanValue (rewriteExpression condition) (rewriteBlock body)
            DoWhileStatement spanValue body condition ->
                DoWhileStatement spanValue (rewriteBlock body) (rewriteExpression condition)
            ForStatement spanValue initializer condition updates body ->
                ForStatement
                    spanValue
                    (rewriteStatement <$> initializer)
                    (rewriteExpression <$> condition)
                    (map rewriteStatement updates)
                    (rewriteBlock body)
            ForEachStatement spanValue kind syntax name annotation source body ->
                ForEachStatement
                    spanValue
                    kind
                    syntax
                    name
                    (rewriteType annotation)
                    (rewriteExpression source)
                    (rewriteBlock body)
            IncrementStatement spanValue name annotation ->
                IncrementStatement spanValue name (rewriteType annotation)
            CompoundAssignmentStatement spanValue operator name annotation value ->
                CompoundAssignmentStatement spanValue operator name (rewriteType annotation) (rewriteExpression value)
            DiscardStatement spanValue value -> DiscardStatement spanValue (rewriteExpression value)
            BreakStatement spanValue value -> BreakStatement spanValue (rewriteExpression <$> value)
            ContinueStatement {} -> statement
            GuardStatement spanValue condition block ->
                GuardStatement spanValue (rewriteExpression condition) (rewriteBlock block)
            BlockStatement spanValue block -> BlockStatement spanValue (rewriteBlock block)
            ExpressionStatement spanValue value terminated -> ExpressionStatement spanValue (rewriteExpression value) terminated
        rewriteExpression expression = case expression of
            NameExpression spanValue name annotation -> NameExpression spanValue name (rewriteType annotation)
            LiteralExpression spanValue literal annotation -> LiteralExpression spanValue literal (rewriteType annotation)
            MemberAccessExpression spanValue receiver member annotation ->
                MemberAccessExpression spanValue (rewriteExpression receiver) member (rewriteType annotation)
            MethodReferenceExpression spanValue receiver member annotation ->
                MethodReferenceExpression spanValue (rewriteExpression receiver) member (rewriteType annotation)
            CallExpression spanValue callee arguments annotation ->
                CallExpression spanValue (rewriteExpression callee) (map rewriteExpression arguments) (rewriteType annotation)
            UnaryExpression spanValue operator value annotation ->
                UnaryExpression spanValue operator (rewriteExpression value) (rewriteType annotation)
            BinaryExpression spanValue operator left right annotation ->
                BinaryExpression spanValue operator (rewriteExpression left) (rewriteExpression right) (rewriteType annotation)
            IsPatternExpression spanValue value patternValue annotation ->
                IsPatternExpression
                    spanValue
                    (rewriteExpression value)
                    (rewritePattern patternValue)
                    (rewriteType annotation)
            ConditionalExpression spanValue condition whenTrue whenFalse annotation ->
                ConditionalExpression
                    spanValue
                    (rewriteExpression condition)
                    (rewriteExpression whenTrue)
                    (rewriteExpression whenFalse)
                    (rewriteType annotation)
            CoalesceExpression spanValue left fallback annotation ->
                CoalesceExpression spanValue (rewriteExpression left) (rewriteExpression fallback) (rewriteType annotation)
            AssignmentExpression spanValue operator name value annotation ->
                AssignmentExpression spanValue operator name (rewriteExpression value) (rewriteType annotation)
            IncrementExpression spanValue isPrefix name annotation ->
                IncrementExpression spanValue isPrefix name (rewriteType annotation)
            LoopExpression spanValue loop annotation ->
                LoopExpression spanValue (rewriteStatement loop) (rewriteType annotation)
            BlockExpression spanValue block annotation ->
                BlockExpression spanValue (rewriteBlock block) (rewriteType annotation)
            MatchExpression spanValue subjects arms annotation ->
                MatchExpression
                    spanValue
                    (map rewriteExpression subjects)
                    [ arm
                        { matchArmPatterns =
                            [ runIdentity (traverseMatchPattern pure (pure . rewriteType) patternValue)
                            | patternValue <- matchArmPatterns arm
                            ]
                        , matchArmGuard = rewriteExpression <$> matchArmGuard arm
                        , matchArmBody = rewriteExpression (matchArmBody arm)
                        }
                    | arm <- arms
                    ]
                    (rewriteType annotation)
            CallableExpression spanValue isStatic captures parameters body annotation ->
                CallableExpression spanValue isStatic captures parameters body (rewriteType annotation)
        rewritePattern patternValue = case patternValue of
            WildcardPattern spanValue annotation -> WildcardPattern spanValue (rewriteType annotation)
            NullPattern spanValue annotation -> NullPattern spanValue (rewriteType annotation)
            LiteralPattern spanValue literal annotation -> LiteralPattern spanValue literal (rewriteType annotation)
            TypePattern spanValue syntax annotation -> TypePattern spanValue syntax (rewriteType annotation)
            RelationalPattern spanValue operator literal annotation ->
                RelationalPattern spanValue operator literal (rewriteType annotation)
            NotPattern spanValue nested annotation ->
                NotPattern spanValue (rewritePattern nested) (rewriteType annotation)
            AndPattern spanValue left right annotation ->
                AndPattern spanValue (rewritePattern left) (rewritePattern right) (rewriteType annotation)
            OrPattern spanValue left right annotation ->
                OrPattern spanValue (rewritePattern left) (rewritePattern right) (rewriteType annotation)
        rewriteType valueType = case valueType of
            NamedType (QualifiedName [Identifier "Box"]) arguments -> NamedType (qualified [replacement]) arguments
            NamedType name arguments -> NamedType name (map rewriteArgument arguments)
            FunctionType parameters result -> FunctionType (map rewriteType parameters) (rewriteType result)
            _ -> valueType
        rewriteArgument (TypeTemplateArgument nested) = TypeTemplateArgument (rewriteType nested)
        rewriteArgument value = value

countsVisitedTypes :: Bool
countsVisitedTypes = maybe False ((> 0) . visitedTemplateTypeNodes . templateDiscoveryStatistics) (discover boxParameterSource)

countsApplications :: Bool
countsApplications = maybe False ((== 2) . discoveredTemplateApplications . templateDiscoveryStatistics) (discover duplicateSource)

countsIgnoredTypes :: Bool
countsIgnoredTypes = maybe False ((> 0) . ignoredOrdinaryNamedTypes . templateDiscoveryStatistics) (discover boxParameterSource)

countsSkippedTemplates :: Bool
countsSkippedTemplates = maybe False ((== 2) . skippedOpenTemplateBodies . templateDiscoveryStatistics) (discover nestedSource)

originNamesOwner :: Bool
originNamesOwner = case discover boxParameterSource >>= onlyOrigin of
    Just origin -> resolvedSpelling (discoveryDeclaration origin) == Identifier "App"
    Nothing -> False

originNamesMember :: Bool
originNamesMember = case discover boxParameterSource >>= onlyOrigin of
    Just origin -> fmap resolvedSpelling (discoveryMember origin) == Just (Identifier "Use")
    Nothing -> False

originNamesParameterSite :: Bool
originNamesParameterSite = case discover boxParameterSource >>= onlyOrigin of
    Just origin -> ParameterTypeSite 0 `elem` discoverySites origin
    Nothing -> False

originNamesSourceFile :: Bool
originNamesSourceFile = case discover boxParameterSource >>= onlyOrigin of
    Just origin -> "template-discovery-test.tala" `isInfixOf` renderTemplateDiscoveryOrigin origin
    Nothing -> False

originHasCoordinates :: Bool
originHasCoordinates = case discover boxParameterSource >>= onlyOrigin of
    Just origin -> sourceLine (sourceStart (discoverySpan origin)) > 0 && sourceColumn (sourceStart (discoverySpan origin)) > 0
    Nothing -> False

compilerStoresDiscovery :: Bool
compilerStoresDiscovery = case compile boxParameterSource of
    Just artifacts -> not (null (discoveredTemplateDemands (artifactTemplateDemandDiscovery artifacts)))
    Nothing -> False

compilerPlansLayouts :: Bool
compilerPlansLayouts = case compile boxParameterSource of
    Just artifacts -> length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 1
    Nothing -> False

automaticLayoutOmitsMethods :: Bool
automaticLayoutOmitsMethods = case compile boxParameterSource >>= onlySpecialization of
    Just specialization -> case templateSpecializationDeclaration specialization of
        TypeDeclaration {typeMembers = members} -> null members
        _ -> False
    Nothing -> False

automaticPlanIsClosed :: Bool
automaticPlanIsClosed = case compile boxParameterSource >>= onlySpecialization of
    Just specialization -> templateSpecializationType specialization == NamedType (qualified ["Box"]) [TypeTemplateArgument intType]
    Nothing -> False

ordinaryProgramHasEmptyPlan :: Bool
ordinaryProgramHasEmptyPlan = case compile "class App { void Run() { return; } }" of
    Just artifacts -> null (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts))
    Nothing -> False

defaultArgumentsAreMaterialized :: Bool
defaultArgumentsAreMaterialized = case compile source >>= onlySpecialization of
    Just specialization -> templateSpecializationType specialization == NamedType (qualified ["Box"]) [TypeTemplateArgument intType]
    Nothing -> False
    where
        source = "template<typename T = int> class Box {} class App { void Use(_ Box value) { return; } }"

mixedApplicationsStaySeparate :: Bool
mixedApplicationsStaySeparate = case compile source of
    Just artifacts -> length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 2
    Nothing -> False
    where
        source =
            "template<typename T, int N> class Buffer {} class App { void A(_ Buffer<int, 8> x) { return; } void B(_ Buffer<int, 16> x) { return; } }"

duplicateUsesCoalesceInPlan :: Bool
duplicateUsesCoalesceInPlan = case compile duplicateSource of
    Just artifacts -> length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 1
    Nothing -> False

duplicateUsesRetainOrigins :: Bool
duplicateUsesRetainOrigins = case compile duplicateSource >>= onlySpecialization of
    Just specialization -> length (templateSpecializationOrigins specialization) == 2
    Nothing -> False

discoveryDoesNotCoalesce :: Bool
discoveryDoesNotCoalesce = case compile duplicateSource of
    Just artifacts ->
        length (discoveredTemplateDemands (artifactTemplateDemandDiscovery artifacts)) == 2
            && length (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts)) == 1
    Nothing -> False

nestedPlansHaveDependencyOrder :: Bool
nestedPlansHaveDependencyOrder = case compile nestedSource of
    Just artifacts ->
        let plan = artifactTemplateSpecializations artifacts
         in length (plannedTemplateSpecializations plan) == 2
                && length (specializationEmissionOrder plan) == 2
    Nothing -> False

templateOnlyPipelineIsEmpty :: Bool
templateOnlyPipelineIsEmpty = case compile "template<typename T> class Box { T Read(_ T value) { return value; } }" of
    Just artifacts -> null (plannedTemplateSpecializations (artifactTemplateSpecializations artifacts))
    Nothing -> False