packages feed

tala-compiler-0.1.0: test/TemplatePlanVerifierTests.hs

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

module TemplatePlanVerifierTests (templatePlanVerifierTests) where

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.Mangling
import Progmasoft.Tala.Template.Specialization
import Progmasoft.Tala.Template.Specialization.Verifier

templatePlanVerifierTests :: [(String, Bool)]
templatePlanVerifierTests =
    [ ("plan verifier accepts planner output", acceptsPlannerOutput)
    , ("plan verifier accepts the empty plan", acceptsEmptyPlan)
    , ("plan verifier rejects a zero specialization id", rejectsZeroId)
    , ("plan verifier rejects a negative specialization id", rejectsNegativeId)
    , ("plan verifier rejects duplicate specialization ids", rejectsDuplicateIds)
    , ("plan verifier rejects an empty canonical identity", rejectsEmptyIdentity)
    , ("plan verifier rejects duplicate canonical identities", rejectsDuplicateIdentities)
    , ("plan verifier rejects an open specialization type", rejectsOpenType)
    , ("plan verifier rejects ErrorType specialization", rejectsErrorType)
    , ("plan verifier requires an ordinary type declaration", rejectsFunctionDeclaration)
    , ("plan verifier checks declaration annotation", rejectsDeclarationMismatch)
    , ("plan verifier rejects an empty origin", rejectsEmptyOrigin)
    , ("plan verifier rejects duplicate origins", rejectsDuplicateOrigins)
    , ("plan verifier rejects a missing dependency", rejectsMissingDependency)
    , ("plan verifier rejects duplicate dependencies", rejectsDuplicateDependencies)
    , ("plan verifier rejects direct self dependency", rejectsSelfDependency)
    , ("plan verifier rejects zero fresh source symbols", rejectsZeroFreshSource)
    , ("plan verifier rejects zero fresh target symbols", rejectsZeroFreshTarget)
    , ("plan verifier rejects identity fresh mappings", rejectsIdentityFreshMap)
    , ("plan verifier rejects duplicate fresh sources", rejectsDuplicateFreshSources)
    , ("plan verifier rejects duplicate fresh targets", rejectsDuplicateFreshTargets)
    , ("plan verifier requires every declaration symbol in the fresh map", rejectsMissingDeclarationSymbol)
    , ("plan verifier rejects layout plans containing members", rejectsLayoutWithMembers)
    , ("plan verifier checks requested member presence", rejectsMissingRequestedMember)
    , ("plan verifier checks unrequested member presence", rejectsUnexpectedMember)
    , ("plan verifier rejects an invalid mangled type", rejectsInvalidMangledType)
    , ("plan verifier checks recomputed mangled types", rejectsIncorrectMangledType)
    , ("plan verifier requires a mangled name per member", rejectsMissingMangledMember)
    , ("plan verifier rejects an invalid mangled member", rejectsInvalidMangledMember)
    , ("plan verifier rejects duplicate mangled members", rejectsDuplicateMangledMember)
    , ("plan verifier rejects an unknown mangled member", rejectsUnexpectedMangledMember)
    , ("plan verifier checks recomputed mangled members", rejectsIncorrectMangledMember)
    , ("plan verifier checks aggregate statistics", rejectsIncorrectStatistics)
    , ("plan verifier detects incomplete emission identity coverage", rejectsIncompleteEmissionCoverage)
    , ("issue rendering names the specialization id", renderingNamesSpecialization)
    , ("issue rendering explains mangled symbol mismatch", renderingExplainsMangledMismatch)
    , ("issue rendering explains fresh symbol ownership", renderingExplainsSharedFreshSymbol)
    ]

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

resolved :: Int -> String -> ResolvedName
resolved unique spelling = ResolvedName (SymbolId unique) (Identifier spelling)

typeArgument :: String -> TemplateArgument
typeArgument name = TypeTemplateArgument (namedType name)

request :: TemplateSpecializationDemand
request =
    TemplateSpecializationDemand
        (TemplateApplication (qualified ["Box"]) [typeArgument "String"])
        (TemplateMemberDemand [Identifier "Identity"])
        "verifier-test:request"

source :: String
source =
    unlines
        [ "template<typename T> class Box {"
        , "    T Identity(_ T value) { T copy = value; return copy; }"
        , "    int Size() { return 1; }"
        , "}"
        ]

validPlan :: Maybe TemplateSpecializationPlan
validPlan = case analyzeSemantics (CompilerInput "template-plan-verifier-test.tala" source) of
    Right artifacts -> case planTemplateSpecializations defaultTemplateSpecializationLimits (semanticTypedAST artifacts) [request] of
        Right plan -> Just plan
        Left _ -> Nothing
    Left _ -> Nothing

emptyPlan :: TemplateSpecializationPlan
emptyPlan = TemplateSpecializationPlan Nothing [] (TemplateSpecializationStatistics 0 0 0 0 0 0)

onlySpecialization :: TemplateSpecializationPlan -> Maybe TemplateSpecialization
onlySpecialization plan = case plannedTemplateSpecializations plan of
    [specialization] -> Just specialization
    _ -> Nothing

rewriteOnly ::
    (TemplateSpecialization -> TemplateSpecialization) -> TemplateSpecializationPlan -> TemplateSpecializationPlan
rewriteOnly rewrite plan = plan {plannedTemplateSpecializations = map rewrite (plannedTemplateSpecializations plan)}

issuesAfter :: (TemplateSpecialization -> TemplateSpecialization) -> [TemplatePlanIssue]
issuesAfter rewrite = maybe [] (templateSpecializationPlanIssues . rewriteOnly rewrite) validPlan

hasIssue :: (TemplatePlanIssue -> Bool) -> [TemplatePlanIssue] -> Bool
hasIssue predicate = any predicate

acceptsPlannerOutput :: Bool
acceptsPlannerOutput = case validPlan of
    Just plan -> verifyTemplateSpecializationPlan plan == Right plan
    Nothing -> False

acceptsEmptyPlan :: Bool
acceptsEmptyPlan = verifyTemplateSpecializationPlan emptyPlan == Right emptyPlan

rejectsZeroId :: Bool
rejectsZeroId = hasIssue isInvalid (issuesAfter (\value -> value {templateSpecializationId = TemplateSpecializationId 0}))
    where
        isInvalid (InvalidPlanSpecializationId (TemplateSpecializationId 0)) = True
        isInvalid _ = False

rejectsNegativeId :: Bool
rejectsNegativeId = hasIssue isInvalid (issuesAfter (\value -> value {templateSpecializationId = TemplateSpecializationId (-7)}))
    where
        isInvalid (InvalidPlanSpecializationId (TemplateSpecializationId (-7))) = True
        isInvalid _ = False

rejectsDuplicateIds :: Bool
rejectsDuplicateIds = case validPlan >>= onlySpecialization of
    Just specialization ->
        let plan = maybe emptyPlan id validPlan
            duplicate = specialization {templateSpecializationIdentity = templateSpecializationIdentity specialization ++ ":copy"}
         in hasIssue
                isDuplicate
                (templateSpecializationPlanIssues plan {plannedTemplateSpecializations = [specialization, duplicate]})
    Nothing -> False
    where
        isDuplicate (DuplicatePlanSpecializationId _) = True
        isDuplicate _ = False

rejectsEmptyIdentity :: Bool
rejectsEmptyIdentity = hasIssue isEmpty (issuesAfter (\value -> value {templateSpecializationIdentity = ""}))
    where
        isEmpty (EmptyPlanSpecializationIdentity _) = True
        isEmpty _ = False

rejectsDuplicateIdentities :: Bool
rejectsDuplicateIdentities = case validPlan >>= onlySpecialization of
    Just specialization ->
        let plan = maybe emptyPlan id validPlan
            duplicate = specialization {templateSpecializationId = TemplateSpecializationId 2}
         in hasIssue
                isDuplicate
                (templateSpecializationPlanIssues plan {plannedTemplateSpecializations = [specialization, duplicate]})
    Nothing -> False
    where
        isDuplicate (DuplicatePlanSpecializationIdentity _) = True
        isDuplicate _ = False

rejectsOpenType :: Bool
rejectsOpenType = hasIssue isOpen (issuesAfter rewrite)
    where
        open = TypeVariable (resolved 500 "T")
        rewrite value = value {templateSpecializationType = open}
        isOpen (PlanSpecializationTypeIsOpen _ symbols) = SymbolId 500 `elem` symbols
        isOpen _ = False

rejectsErrorType :: Bool
rejectsErrorType = hasIssue isError (issuesAfter (\value -> value {templateSpecializationType = ErrorType}))
    where
        isError (PlanSpecializationTypeHasError _) = True
        isError _ = False

rejectsFunctionDeclaration :: Bool
rejectsFunctionDeclaration = case validPlan >>= onlySpecialization of
    Just specialization -> case templateSpecializationDeclaration specialization of
        TypeDeclaration {typeMembers = member : _} ->
            hasIssue isWrong (issuesAfter (\value -> value {templateSpecializationDeclaration = member}))
        _ -> False
    Nothing -> False
    where
        isWrong (PlanDeclarationIsNotClosedType _) = True
        isWrong _ = False

rejectsDeclarationMismatch :: Bool
rejectsDeclarationMismatch = hasIssue isMismatch (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationDeclaration = setAnnotation intType (templateSpecializationDeclaration value)}
        isMismatch (PlanDeclarationTypeMismatch _ _ _) = True
        isMismatch _ = False

setAnnotation :: Type -> Declaration ResolvedName Type -> Declaration ResolvedName Type
setAnnotation annotation declaration = case declaration of
    TypeDeclaration spanValue name _ members -> TypeDeclaration spanValue name annotation members
    other -> other

rejectsEmptyOrigin :: Bool
rejectsEmptyOrigin = hasIssue isEmpty (issuesAfter (\value -> value {templateSpecializationOrigins = [""]}))
    where
        isEmpty (EmptyPlanOrigin _) = True
        isEmpty _ = False

rejectsDuplicateOrigins :: Bool
rejectsDuplicateOrigins = hasIssue isDuplicate (issuesAfter (\value -> value {templateSpecializationOrigins = ["same", "same"]}))
    where
        isDuplicate (DuplicatePlanOrigin _ "same") = True
        isDuplicate _ = False

rejectsMissingDependency :: Bool
rejectsMissingDependency =
    hasIssue isMissing (issuesAfter (\value -> value {templateSpecializationDependencies = [TemplateSpecializationId 99]}))
    where
        isMissing (MissingPlanDependency _ (TemplateSpecializationId 99)) = True
        isMissing _ = False

rejectsDuplicateDependencies :: Bool
rejectsDuplicateDependencies = hasIssue isDuplicate (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationDependencies = [TemplateSpecializationId 99, TemplateSpecializationId 99]}
        isDuplicate (DuplicatePlanDependency _ (TemplateSpecializationId 99)) = True
        isDuplicate _ = False

rejectsSelfDependency :: Bool
rejectsSelfDependency = hasIssue isSelf (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationDependencies = [templateSpecializationId value]}
        isSelf (SelfPlanDependency _) = True
        isSelf _ = False

rejectsZeroFreshSource :: Bool
rejectsZeroFreshSource = hasIssue isInvalid (issuesAfter replaceFirst)
    where
        replaceFirst value = value {templateSpecializationSymbolMap = replaceSource (SymbolId 0) (templateSpecializationSymbolMap value)}
        isInvalid (InvalidFreshSymbol _ (SymbolId 0) _) = True
        isInvalid _ = False

rejectsZeroFreshTarget :: Bool
rejectsZeroFreshTarget = hasIssue isInvalid (issuesAfter replaceFirst)
    where
        replaceFirst value = value {templateSpecializationSymbolMap = replaceTarget (SymbolId 0) (templateSpecializationSymbolMap value)}
        isInvalid (InvalidFreshSymbol _ _ (SymbolId 0)) = True
        isInvalid _ = False

rejectsIdentityFreshMap :: Bool
rejectsIdentityFreshMap = hasIssue isInvalid (issuesAfter replaceFirst)
    where
        replaceFirst value = case templateSpecializationSymbolMap value of
            (old, _) : rest -> value {templateSpecializationSymbolMap = (old, old) : rest}
            [] -> value
        isInvalid (InvalidFreshSymbol _ old new) = old == new
        isInvalid _ = False

replaceSource :: SymbolId -> [(SymbolId, SymbolId)] -> [(SymbolId, SymbolId)]
replaceSource replacement mappings = case mappings of
    (_, target) : rest -> (replacement, target) : rest
    [] -> []

replaceTarget :: SymbolId -> [(SymbolId, SymbolId)] -> [(SymbolId, SymbolId)]
replaceTarget replacement mappings = case mappings of
    (sourceSymbol, _) : rest -> (sourceSymbol, replacement) : rest
    [] -> []

rejectsDuplicateFreshSources :: Bool
rejectsDuplicateFreshSources = hasIssue isDuplicate (issuesAfter duplicateFirst)
    where
        duplicateFirst value = case templateSpecializationSymbolMap value of
            first : second : rest -> value {templateSpecializationSymbolMap = first : (fst first, snd second) : rest}
            _ -> value
        isDuplicate (DuplicateFreshSourceSymbol _ _) = True
        isDuplicate _ = False

rejectsDuplicateFreshTargets :: Bool
rejectsDuplicateFreshTargets = hasIssue isDuplicate (issuesAfter duplicateFirst)
    where
        duplicateFirst value = case templateSpecializationSymbolMap value of
            first : second : rest -> value {templateSpecializationSymbolMap = first : (fst second, snd first) : rest}
            _ -> value
        isDuplicate (DuplicateFreshTargetSymbol _ _) = True
        isDuplicate _ = False

rejectsMissingDeclarationSymbol :: Bool
rejectsMissingDeclarationSymbol = hasIssue isMissing (issuesAfter (\value -> value {templateSpecializationSymbolMap = []}))
    where
        isMissing (DeclarationSymbolMissingFromFreshMap _ _) = True
        isMissing _ = False

rejectsLayoutWithMembers :: Bool
rejectsLayoutWithMembers = hasIssue isLayout (issuesAfter (\value -> value {templateSpecializationScope = TemplateLayoutDemand}))
    where
        isLayout (LayoutPlanContainsMembers _ count) = count > 0
        isLayout _ = False

rejectsMissingRequestedMember :: Bool
rejectsMissingRequestedMember = hasIssue isMissing (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationScope = TemplateMemberDemand [Identifier "Missing"]}
        isMissing (RequestedPlanMemberMissing _ (Identifier "Missing")) = True
        isMissing _ = False

rejectsUnexpectedMember :: Bool
rejectsUnexpectedMember = hasIssue isUnexpected (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationScope = TemplateMemberDemand []}
        isUnexpected (UnexpectedPlanMember _ (Identifier "Identity")) = True
        isUnexpected _ = False

rejectsInvalidMangledType :: Bool
rejectsInvalidMangledType = hasIssue isInvalid (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationMangledType = MangledTemplateType "invalid!"}
        isInvalid (InvalidPlanMangledType _ "invalid!") = True
        isInvalid _ = False

rejectsIncorrectMangledType :: Bool
rejectsIncorrectMangledType = hasIssue isIncorrect (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationMangledType = MangledTemplateType "_TAT1_wrong"}
        isIncorrect (IncorrectPlanMangledType _ _ "_TAT1_wrong") = True
        isIncorrect _ = False

rejectsMissingMangledMember :: Bool
rejectsMissingMangledMember = hasIssue isMissing (issuesAfter (\value -> value {templateSpecializationMangledMembers = []}))
    where
        isMissing (MissingPlanMangledMember _ _) = True
        isMissing _ = False

rejectsInvalidMangledMember :: Bool
rejectsInvalidMangledMember = hasIssue isInvalid (issuesAfter rewrite)
    where
        rewrite value = value {templateSpecializationMangledMembers = map corrupt (templateSpecializationMangledMembers value)}
        corrupt member = member {mangledMemberText = "invalid!"}
        isInvalid (InvalidPlanMangledMember _ _ "invalid!") = True
        isInvalid _ = False

rejectsDuplicateMangledMember :: Bool
rejectsDuplicateMangledMember = hasIssue isDuplicate (issuesAfter rewrite)
    where
        rewrite value = case templateSpecializationMangledMembers value of
            first : rest -> value {templateSpecializationMangledMembers = first : first : rest}
            [] -> value
        isDuplicate (DuplicatePlanMangledMember _ _) = True
        isDuplicate _ = False

rejectsUnexpectedMangledMember :: Bool
rejectsUnexpectedMangledMember = hasIssue isUnexpected (issuesAfter rewrite)
    where
        rewrite value = case templateSpecializationMangledMembers value of
            first : rest -> value {templateSpecializationMangledMembers = first {mangledMemberSourceSymbol = SymbolId 9999} : rest}
            [] -> value
        isUnexpected (UnexpectedPlanMangledMember _ (SymbolId 9999)) = True
        isUnexpected _ = False

rejectsIncorrectMangledMember :: Bool
rejectsIncorrectMangledMember = hasIssue isInvalid (issuesAfter rewrite)
    where
        rewrite value = case templateSpecializationMangledMembers value of
            first : rest -> value {templateSpecializationMangledMembers = first {mangledMemberText = "_TAT1_wrong"} : rest}
            [] -> value
        isInvalid (InvalidPlanMangledMember _ _ "_TAT1_wrong") = True
        isInvalid _ = False

rejectsIncorrectStatistics :: Bool
rejectsIncorrectStatistics = case validPlan of
    Just plan ->
        hasIssue
            isIncorrect
            (templateSpecializationPlanIssues plan {plannedTemplateStatistics = TemplateSpecializationStatistics 99 98 97 96 95 94})
    Nothing -> False
    where
        isIncorrect (IncorrectPlanStatistics _ _) = True
        isIncorrect _ = False

rejectsIncompleteEmissionCoverage :: Bool
rejectsIncompleteEmissionCoverage = case validPlan >>= onlySpecialization of
    Just specialization ->
        let plan = maybe emptyPlan id validPlan
            duplicate = specialization {templateSpecializationIdentity = templateSpecializationIdentity specialization ++ ":duplicate"}
         in hasIssue
                isIncomplete
                (templateSpecializationPlanIssues plan {plannedTemplateSpecializations = [specialization, duplicate]})
    Nothing -> False
    where
        isIncomplete (IncompletePlanEmissionOrder _ _) = True
        isIncomplete _ = False

renderingNamesSpecialization :: Bool
renderingNamesSpecialization = "specialization 7" `isInfixOf` renderTemplatePlanIssue (InvalidPlanSpecializationId (TemplateSpecializationId 7))

renderingExplainsMangledMismatch :: Bool
renderingExplainsMangledMismatch =
    "mangled type differs"
        `isInfixOf` renderTemplatePlanIssue (IncorrectPlanMangledType (TemplateSpecializationId 1) "expected" "actual")

renderingExplainsSharedFreshSymbol :: Bool
renderingExplainsSharedFreshSymbol =
    "fresh symbol 44 is shared"
        `isInfixOf` renderTemplatePlanIssue (SharedFreshTargetSymbol (SymbolId 44) [TemplateSpecializationId 1, TemplateSpecializationId 2])