packages feed

tala-frontend-0.1.0: src/Progmasoft/Tala/Template/MemberReachability.hs

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

{- |
Semantic reachability for members of one template declaration.

Template specialization is lazy at member granularity.  A request for one
member must nevertheless retain every same-declaration member reached by a
resolved call from its body.  The call graph is keyed by @SymbolId@, never by
spelling: overloads can share a source name while remaining distinct semantic
definitions.

Calls whose target is not a direct member of the declaration are deliberately
classified as external.  This module does not guess inheritance, extension,
dynamic dispatch, or another declaration's ownership from a name.  Those
decisions belong to name and call resolution before specialization.
-}
module Progmasoft.Tala.Template.MemberReachability
    ( TemplateMemberCall (..)
    , TemplateMemberNode (..)
    , TemplateMemberGraph (..)
    , TemplateMemberReachabilityStatistics (..)
    , TemplateMemberReachability (..)
    , TemplateMemberReachabilityError (..)
    , TemplateMemberReachabilityIssue (..)
    , buildTemplateMemberGraph
    , selectReachableMembersByName
    , selectReachableMembersBySymbol
    , verifyTemplateMemberReachability
    , renderTemplateMemberReachabilityError
    , renderTemplateMemberReachabilityIssue
    ) where

import Data.List (intercalate)
import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Progmasoft.Tala.AST

{- | One direct call expression.  Repeated calls remain distinct evidence even
when they later collapse to one graph edge.
-}
data TemplateMemberCall = TemplateMemberCall
    { templateCallOwner :: SymbolId
    -- ^ Caller member identity.
    , templateCallTarget :: SymbolId
    -- ^ Resolved direct callee identity.
    , templateCallSpan :: SourceSpan
    -- ^ Source location of the call expression.
    }
    deriving (Eq, Ord, Read, Show)

{- | A node retains source order and declaration identity.  Source order is
used only to make output deterministic; it never chooses an overload.
-}
data TemplateMemberNode = TemplateMemberNode
    { templateMemberIndex :: Int
    -- ^ Source-order index within the declaration.
    , templateMemberSymbol :: SymbolId
    -- ^ Unique resolved identity, not spelling.
    , templateMemberName :: Identifier
    -- ^ Source name; overloads may share it.
    , templateMemberCallable :: Bool
    -- ^ Whether the member has an executable body.
    , templateMemberCalls :: [TemplateMemberCall]
    -- ^ Direct calls found in its body.
    }
    deriving (Eq, Ord, Read, Show)

-- | Direct-member call graph partitioned into internal and external calls.
data TemplateMemberGraph = TemplateMemberGraph
    { templateGraphNodes :: [TemplateMemberNode]
    -- ^ Nodes in declaration source order.
    , templateGraphInternalCalls :: [TemplateMemberCall]
    -- ^ Calls whose target is another direct member.
    , templateGraphExternalCalls :: [TemplateMemberCall]
    -- ^ Calls leaving this declaration's member set.
    }
    deriving (Eq, Ord, Read, Show)

-- | Counts describing graph construction and selected member closure.
data TemplateMemberReachabilityStatistics = TemplateMemberReachabilityStatistics
    { totalTemplateMembers :: Int
    -- ^ All direct members in the declaration.
    , callableTemplateMembers :: Int
    -- ^ Members with callable bodies.
    , requestedTemplateMemberRoots :: Int
    -- ^ Unique selected root symbols.
    , reachableTemplateMembers :: Int
    -- ^ Members retained by transitive closure.
    , observedTemplateMemberCalls :: Int
    -- ^ Internal and external call occurrences.
    , internalTemplateMemberCalls :: Int
    -- ^ Calls with same-declaration targets.
    , externalTemplateMemberCalls :: Int
    -- ^ Calls whose targets are outside the graph.
    , uniqueTemplateMemberEdges :: Int
    -- ^ Distinct internal caller/callee pairs.
    }
    deriving (Eq, Ord, Read, Show)

-- | Root requests, transitively retained members, edges, and graph metrics.
data TemplateMemberReachability = TemplateMemberReachability
    { memberReachabilityRoots :: [SymbolId]
    -- ^ Selected root identities in source order.
    , memberReachabilitySymbols :: [SymbolId]
    -- ^ Roots and all transitively reached members.
    , memberReachabilityMembers :: [Declaration ResolvedName Type]
    -- ^ Original member declarations retained.
    , memberReachabilityEdges :: [(SymbolId, SymbolId)]
    -- ^ Unique internal call edges among selected members.
    , memberReachabilityStatistics :: TemplateMemberReachabilityStatistics
    -- ^ Counts for the analyzed graph.
    }
    deriving (Eq, Ord, Read, Show)

-- | Invalid root graph or request that cannot be resolved to a direct member.
data TemplateMemberReachabilityError
    = -- | Member identity is reserved or invalid.
      InvalidTemplateMemberSymbol Identifier SymbolId
    | -- | Multiple member declarations share an identity.
      DuplicateTemplateMemberSymbol SymbolId [Identifier]
    | -- | Requested spelling has no member.
      MissingTemplateMemberName Identifier
    | -- | Requested semantic identity has no member.
      MissingTemplateMemberSymbol SymbolId
    deriving (Eq, Ord, Read, Show)

-- | Independently checkable inconsistency in a claimed reachability result.
data TemplateMemberReachabilityIssue
    = -- | A requested root is not declared directly.
      ReachabilityRootIsNotMember SymbolId
    | -- | A selected identity is not in the graph.
      ReachabilitySelectionIsNotMember SymbolId
    | -- | The root list repeats a symbol.
      DuplicateReachabilityRoot SymbolId
    | -- | The selection repeats a symbol.
      DuplicateReachabilitySelection SymbolId
    | -- | A root is absent from the closure.
      ReachabilityRootNotSelected SymbolId
    | -- | A selected call omits its in-graph callee.
      ReachabilityDependencyNotSelected SymbolId SymbolId
    | -- | An edge is not supported by a recorded call.
      UnexpectedReachabilityEdge SymbolId SymbolId
    | -- | A recorded internal call has no reported edge.
      MissingReachabilityEdge SymbolId SymbolId
    | -- | Declaration and symbol lists disagree.
      ReachabilityMembersDiffer [SymbolId] [SymbolId]
    | -- | Reported counts differ from recomputation.
      IncorrectReachabilityStatistics TemplateMemberReachabilityStatistics TemplateMemberReachabilityStatistics
    deriving (Eq, Ord, Read, Show)

{- | Construct a declaration-local graph after semantic resolution.  Invalid
or duplicated symbols are rejected here even though the general template
verifier normally diagnoses them earlier; this keeps the API safe for
compiler tests and independently constructed TypedAST values.
-}
buildTemplateMemberGraph ::
    [Declaration ResolvedName Type] ->
    Either [TemplateMemberReachabilityError] TemplateMemberGraph
buildTemplateMemberGraph members = case validationProblems of
    [] -> Right graph
    problems -> Left problems
    where
        nodes = zipWith memberNode [0 ..] members
        memberSymbols = map templateMemberSymbol nodes
        memberSet = Set.fromList memberSymbols
        calls = concatMap templateMemberCalls nodes
        graph =
            TemplateMemberGraph
                nodes
                [call | call <- calls, templateCallTarget call `Set.member` memberSet]
                [call | call <- calls, templateCallTarget call `Set.notMember` memberSet]
        invalid =
            [ InvalidTemplateMemberSymbol (templateMemberName node) (templateMemberSymbol node)
            | node <- nodes
            , symbolIdValue (templateMemberSymbol node) <= 0
            ]
        duplicated =
            [ DuplicateTemplateMemberSymbol symbol [templateMemberName node | node <- nodes, templateMemberSymbol node == symbol]
            | symbol <- duplicates memberSymbols
            ]
        validationProblems = invalid ++ duplicated

-- | Select every overload with a requested spelling and follow internal calls.
selectReachableMembersByName ::
    [Identifier] ->
    [Declaration ResolvedName Type] ->
    Either [TemplateMemberReachabilityError] TemplateMemberReachability
selectReachableMembersByName requested members = do
    graph <- buildTemplateMemberGraph members
    let names = unique requested
        nodes = templateGraphNodes graph
        missing = [name | name <- names, name `notElem` map templateMemberName nodes]
        roots =
            [ templateMemberSymbol node
            | node <- nodes
            , templateMemberName node `elem` names
            ]
    if null missing
        then Right (selectFromGraph roots members graph)
        else Left (map MissingTemplateMemberName missing)

-- | Select exact semantic member identities and follow internal calls.
selectReachableMembersBySymbol ::
    [SymbolId] ->
    [Declaration ResolvedName Type] ->
    Either [TemplateMemberReachabilityError] TemplateMemberReachability
selectReachableMembersBySymbol requested members = do
    graph <- buildTemplateMemberGraph members
    let roots = unique requested
        available = map templateMemberSymbol (templateGraphNodes graph)
        missing = [symbol | symbol <- roots, symbol `notElem` available]
    if null missing
        then Right (selectFromGraph roots members graph)
        else Left (map MissingTemplateMemberSymbol missing)

selectFromGraph ::
    [SymbolId] ->
    [Declaration ResolvedName Type] ->
    TemplateMemberGraph ->
    TemplateMemberReachability
selectFromGraph roots members graph =
    TemplateMemberReachability
        orderedRoots
        selectedSymbols
        [member | member <- members, declarationSymbol member `Set.member` selectedSet]
        selectedEdges
        statistics
    where
        nodes = templateGraphNodes graph
        sourceOrder = map templateMemberSymbol nodes
        orderedRoots = [symbol | symbol <- sourceOrder, symbol `elem` roots]
        adjacency = adjacencyTable (templateGraphInternalCalls graph)
        selectedSet = closeReachability adjacency Set.empty orderedRoots
        selectedSymbols = [symbol | symbol <- sourceOrder, symbol `Set.member` selectedSet]
        selectedEdges =
            unique
                [ (templateCallOwner call, templateCallTarget call)
                | call <- templateGraphInternalCalls graph
                , templateCallOwner call `Set.member` selectedSet
                ]
        calls = templateGraphInternalCalls graph ++ templateGraphExternalCalls graph
        statistics =
            TemplateMemberReachabilityStatistics
                { totalTemplateMembers = length nodes
                , callableTemplateMembers = length [() | node <- nodes, templateMemberCallable node]
                , requestedTemplateMemberRoots = length orderedRoots
                , reachableTemplateMembers = length selectedSymbols
                , observedTemplateMemberCalls = length calls
                , internalTemplateMemberCalls = length (templateGraphInternalCalls graph)
                , externalTemplateMemberCalls = length (templateGraphExternalCalls graph)
                , uniqueTemplateMemberEdges = length (unique (map callEdge (templateGraphInternalCalls graph)))
                }

adjacencyTable :: [TemplateMemberCall] -> Map SymbolId [SymbolId]
adjacencyTable = foldl' insert Map.empty
    where
        insert table call =
            Map.insertWith
                (\new old -> unique (old ++ new))
                (templateCallOwner call)
                [templateCallTarget call]
                table

closeReachability :: Map SymbolId [SymbolId] -> Set SymbolId -> [SymbolId] -> Set SymbolId
closeReachability _ visited [] = visited
closeReachability adjacency visited (symbol : pending)
    | symbol `Set.member` visited = closeReachability adjacency visited pending
    | otherwise =
        let dependencies = Map.findWithDefault [] symbol adjacency
         in closeReachability adjacency (Set.insert symbol visited) (pending ++ dependencies)

{- | Recompute graph facts from declarations and compare them with a retained
result.  The specialization-plan verifier uses the selected declaration for
broader invariants; this verifier protects the reachability object itself.
-}
verifyTemplateMemberReachability ::
    [Declaration ResolvedName Type] ->
    TemplateMemberReachability ->
    Either [TemplateMemberReachabilityError] [TemplateMemberReachabilityIssue]
verifyTemplateMemberReachability members reachability = do
    graph <- buildTemplateMemberGraph members
    let available = map templateMemberSymbol (templateGraphNodes graph)
        roots = memberReachabilityRoots reachability
        selected = memberReachabilitySymbols reachability
        actualMembers = map declarationSymbol (memberReachabilityMembers reachability)
        expected = selectFromGraph roots members graph
        expectedEdges = memberReachabilityEdges expected
        actualEdges = memberReachabilityEdges reachability
        rootProblems =
            [ReachabilityRootIsNotMember symbol | symbol <- roots, symbol `notElem` available]
                ++ [DuplicateReachabilityRoot symbol | symbol <- duplicates roots]
                ++ [ReachabilityRootNotSelected symbol | symbol <- roots, symbol `notElem` selected]
        selectionProblems =
            [ReachabilitySelectionIsNotMember symbol | symbol <- selected, symbol `notElem` available]
                ++ [DuplicateReachabilitySelection symbol | symbol <- duplicates selected]
        closureProblems =
            [ ReachabilityDependencyNotSelected owner target
            | (owner, target) <- expectedEdges
            , owner `elem` selected
            , target `notElem` selected
            ]
        edgeProblems =
            [UnexpectedReachabilityEdge owner target | (owner, target) <- actualEdges, (owner, target) `notElem` expectedEdges]
                ++ [MissingReachabilityEdge owner target | (owner, target) <- expectedEdges, (owner, target) `notElem` actualEdges]
        memberProblems =
            [ReachabilityMembersDiffer selected actualMembers | selected /= actualMembers]
        statisticProblems =
            [ IncorrectReachabilityStatistics
                (memberReachabilityStatistics expected)
                (memberReachabilityStatistics reachability)
            | memberReachabilityStatistics expected /= memberReachabilityStatistics reachability
            ]
    pure (rootProblems ++ selectionProblems ++ closureProblems ++ edgeProblems ++ memberProblems ++ statisticProblems)

memberNode :: Int -> Declaration ResolvedName Type -> TemplateMemberNode
memberNode index member =
    TemplateMemberNode
        index
        (declarationSymbol member)
        (resolvedSpelling (declarationName member))
        (isFunction member)
        (memberCalls (declarationSymbol member) member)

isFunction :: Declaration name annotation -> Bool
isFunction FunctionDeclaration {} = True
isFunction _ = False

memberCalls :: SymbolId -> Declaration ResolvedName Type -> [TemplateMemberCall]
memberCalls owner declaration = case declaration of
    FunctionDeclaration {declarationBody = body} -> blockCalls owner body
    -- Nested types have their own member ownership domain.  Their bodies must
    -- not accidentally pull siblings into the enclosing template closure.
    TypeDeclaration {} -> []
    TemplateTypeDeclaration {} -> []
    EnumDeclaration {} -> []

blockCalls :: SymbolId -> Block ResolvedName Type -> [TemplateMemberCall]
blockCalls owner (Block statements) = concatMap (statementCalls owner) statements

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

expressionCalls :: SymbolId -> Expression ResolvedName Type -> [TemplateMemberCall]
expressionCalls owner expression = case expression of
    NameExpression {} -> []
    LiteralExpression {} -> []
    MemberAccessExpression _ receiver _ _ -> expressionCalls owner receiver
    MethodReferenceExpression _ receiver _ _ -> expressionCalls owner receiver
    CallExpression spanValue callee arguments _ ->
        directCall spanValue callee
            ++ expressionCalls owner callee
            ++ concatMap (expressionCalls owner) arguments
    UnaryExpression _ _ value _ -> expressionCalls owner value
    BinaryExpression _ _ left right _ -> expressionCalls owner left ++ expressionCalls owner right
    IsPatternExpression _ subject _ _ -> expressionCalls owner subject
    -- Reachability is path-insensitive: a call in either arm may execute.
    ConditionalExpression _ condition first second _ ->
        concatMap (expressionCalls owner) [condition, first, second]
    CoalesceExpression _ left fallback _ -> expressionCalls owner left ++ expressionCalls owner fallback
    AssignmentExpression _ _ _ value _ -> expressionCalls owner value
    IncrementExpression {} -> []
    LoopExpression _ loop _ -> statementCalls owner loop
    BlockExpression _ block _ -> blockCalls owner block
    -- Reachability is path-insensitive: a call in any arm may execute.
    MatchExpression _ subjects arms _ ->
        concatMap (expressionCalls owner) (subjects ++ concatMap matchArmExpressions arms)
    CallableExpression _ _ captures _ body _ ->
        concatMap (maybe [] (expressionCalls owner) . captureInitializer) captures
            ++ callableBodyCalls owner body
    where
        directCall spanValue callee = case callee of
            NameExpression _ target _ -> [TemplateMemberCall owner (resolvedSymbol target) spanValue]
            _ -> []

callableBodyCalls :: SymbolId -> CallableBody ResolvedName Type -> [TemplateMemberCall]
callableBodyCalls owner body = case body of
    CallableExpressionBody value -> expressionCalls owner value
    CallableBlockBody block -> blockCalls owner block

callEdge :: TemplateMemberCall -> (SymbolId, SymbolId)
callEdge call = (templateCallOwner call, templateCallTarget call)

declarationSymbol :: Declaration ResolvedName annotation -> SymbolId
declarationSymbol = resolvedSymbol . declarationName

-- | Render a graph-construction or root-selection failure.
renderTemplateMemberReachabilityError :: TemplateMemberReachabilityError -> String
renderTemplateMemberReachabilityError problem = case problem of
    InvalidTemplateMemberSymbol name symbol ->
        "template member " ++ identifierText name ++ " has invalid " ++ renderSymbol symbol
    DuplicateTemplateMemberSymbol symbol names ->
        renderSymbol symbol
            ++ " is shared by template members "
            ++ intercalate ", " (map identifierText names)
    MissingTemplateMemberName name -> "template declaration has no member named " ++ identifierText name
    MissingTemplateMemberSymbol symbol -> "template declaration has no member with " ++ renderSymbol symbol

-- | Render a verifier issue from an inconsistent reachability result.
renderTemplateMemberReachabilityIssue :: TemplateMemberReachabilityIssue -> String
renderTemplateMemberReachabilityIssue problem = case problem of
    ReachabilityRootIsNotMember symbol -> "reachability root is not a declaration member: " ++ renderSymbol symbol
    ReachabilitySelectionIsNotMember symbol -> "selected reachability symbol is not a declaration member: " ++ renderSymbol symbol
    DuplicateReachabilityRoot symbol -> "reachability root occurs more than once: " ++ renderSymbol symbol
    DuplicateReachabilitySelection symbol -> "selected reachability symbol occurs more than once: " ++ renderSymbol symbol
    ReachabilityRootNotSelected symbol -> "reachability root is absent from its selection: " ++ renderSymbol symbol
    ReachabilityDependencyNotSelected owner target ->
        "selected " ++ renderSymbol owner ++ " calls unselected " ++ renderSymbol target
    UnexpectedReachabilityEdge owner target ->
        "reachability contains an unobserved edge " ++ renderEdge owner target
    MissingReachabilityEdge owner target ->
        "reachability omits observed edge " ++ renderEdge owner target
    ReachabilityMembersDiffer expected actual ->
        "reachability member declarations differ: expected "
            ++ renderSymbols expected
            ++ ", found "
            ++ renderSymbols actual
    IncorrectReachabilityStatistics expected actual ->
        "reachability statistics differ: expected " ++ show expected ++ ", found " ++ show actual

renderSymbol :: SymbolId -> String
renderSymbol (SymbolId value) = "SymbolId " ++ show value

renderEdge :: SymbolId -> SymbolId -> String
renderEdge owner target = renderSymbol owner ++ " -> " ++ renderSymbol target

renderSymbols :: [SymbolId] -> String
renderSymbols values = "[" ++ intercalate ", " (map renderSymbol values) ++ "]"

duplicates :: (Ord value) => [value] -> [value]
duplicates values = Set.toList (go Set.empty Set.empty values)
    where
        go _ repeated [] = repeated
        go seen repeated (value : remaining)
            | value `Set.member` seen = go seen (Set.insert value repeated) remaining
            | otherwise = go (Set.insert value seen) repeated remaining

unique :: (Ord value) => [value] -> [value]
unique = go Set.empty
    where
        go _ [] = []
        go seen (value : remaining)
            | value `Set.member` seen = go seen remaining
            | otherwise = value : go (Set.insert value seen) remaining