packages feed

tala-core-0.1.0: src/Progmasoft/Tala/Core/Monomorphization/Graph.hs

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

{- | Queries and invariants for a completed specialization-demand graph.

Instantiation needs children before parents when concrete layout depends on a
nested specialization.  The discovery queue is intentionally root-first for
good diagnostics, so emission order is derived here instead of overloading the
meaning of demand identifiers.
-}
module Progmasoft.Tala.Core.Monomorphization.Graph
    ( DemandGraphIssue (..)
    , demandLeaves
    , demandRoots
    , dependentDemands
    , reachableDependencies
    , renderDemandTrace
    , specializationEmissionOrder
    , validateDemandGraph
    ) where

import Data.List (intercalate, sort)
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.Core.Monomorphization.Types
import Progmasoft.Tala.Core.Specialization

-- | Structural or accounting invariant violated by a completed demand graph.
data DemandGraphIssue
    = DuplicateDemandIdentifier DemandId
    | NonPositiveDemandIdentifier DemandId
    | MissingDependency DemandId DemandId
    | DuplicateDependency DemandId DemandId
    | SelfDependency DemandId
    | IncompleteDemand DemandId
    | CyclicDemandPath [DemandId]
    | StatisticsDemandCountMismatch Int Int
    | StatisticsEdgeCountMismatch Int Int
    | StatisticsDepthMismatch Int Int
    deriving (Eq, Ord, Read, Show)

-- | Check identifiers, edges, completion state, acyclicity, and statistics.
validateDemandGraph :: MonomorphizationPlan -> [DemandGraphIssue]
validateDemandGraph plan =
    identifierIssues
        ++ dependencyIssues
        ++ stateIssues
        ++ cycleIssues
        ++ statisticsIssues
    where
        demands = monomorphizationDemands plan
        demandIds = map monomorphizationDemandId demands
        known = Set.fromList demandIds
        identifierIssues =
            [ NonPositiveDemandIdentifier demandId
            | demandId@(DemandId value) <- demandIds
            , value <= 0
            ]
                ++ [ DuplicateDemandIdentifier demandId
                   | groupIds@(demandId : _) <- grouped demandIds
                   , length groupIds > 1
                   ]
        dependencyIssues = concatMap (validateDependencies known) demands
        stateIssues =
            [ IncompleteDemand (monomorphizationDemandId demand)
            | demand <- demands
            , monomorphizationState demand /= DemandComplete
            ]
        cycleIssues = case specializationEmissionOrder plan of
            Left cyclePath -> [CyclicDemandPath cyclePath]
            Right _ -> []
        statistics = monomorphizationStatistics plan
        actualCount = length demands
        actualEdges = sum (map (length . monomorphizationDependencies) demands)
        actualDepth = foldl max 0 (map monomorphizationDepth demands)
        statisticsIssues =
            [ StatisticsDemandCountMismatch (uniqueSpecializationDemands statistics) actualCount
            | uniqueSpecializationDemands statistics /= actualCount
            ]
                ++ [ StatisticsEdgeCountMismatch (specializationDependencyEdges statistics) actualEdges
                   | specializationDependencyEdges statistics /= actualEdges
                   ]
                ++ [ StatisticsDepthMismatch (deepestSpecializationDemand statistics) actualDepth
                   | deepestSpecializationDemand statistics /= actualDepth
                   ]

validateDependencies :: Set DemandId -> MonomorphizationDemand -> [DemandGraphIssue]
validateDependencies known demand =
    [ MissingDependency owner dependency
    | dependency <- dependencies
    , dependency `Set.notMember` known
    ]
        ++ [ DuplicateDependency owner dependency
           | duplicate@(dependency : _) <- grouped dependencies
           , length duplicate > 1
           ]
        ++ [SelfDependency owner | owner `elem` dependencies]
    where
        owner = monomorphizationDemandId demand
        dependencies = monomorphizationDependencies demand

-- | Select demands that have at least one source/Core root occurrence.
demandRoots :: MonomorphizationPlan -> [MonomorphizationDemand]
demandRoots = filter hasRootOrigin . monomorphizationDemands
    where
        hasRootOrigin = any isRoot . monomorphizationOrigins
        isRoot (RootDemand _) = True
        isRoot (DependencyDemand _ _) = False

-- | Select demands that introduce no further specialization dependencies.
demandLeaves :: MonomorphizationPlan -> [MonomorphizationDemand]
demandLeaves = filter (null . monomorphizationDependencies) . monomorphizationDemands

-- | Find direct parents whose dependency list contains the requested demand.
dependentDemands :: DemandId -> MonomorphizationPlan -> [MonomorphizationDemand]
dependentDemands dependency =
    filter (elem dependency . monomorphizationDependencies) . monomorphizationDemands

-- | Return the transitive dependencies reachable from one demand, excluding it.
reachableDependencies :: DemandId -> MonomorphizationPlan -> [MonomorphizationDemand]
reachableDependencies root plan =
    let table = demandTable plan
        ids = walk table Set.empty [root]
     in [demand | demandId <- ids, Just demand <- [Map.lookup demandId table], demandId /= root]
    where
        walk _ _ [] = []
        walk table visited (current : remaining)
            | current `Set.member` visited = walk table visited remaining
            | otherwise =
                let dependencies = maybe [] monomorphizationDependencies (Map.lookup current table)
                 in current : walk table (Set.insert current visited) (dependencies ++ remaining)

{- | Produce a stable postorder. Sibling order follows demand identifiers,
which reflect first discovery order within one deterministic compilation.
-}
specializationEmissionOrder :: MonomorphizationPlan -> Either [DemandId] [MonomorphizationDemand]
specializationEmissionOrder plan = do
    let table = demandTable plan
        roots = map monomorphizationDemandId (monomorphizationDemands plan)
    (_, reverseOrder) <- visitMany table Set.empty [] [] roots
    pure [demand | demandId <- reverse reverseOrder, Just demand <- [Map.lookup demandId table]]

visitMany ::
    Map DemandId MonomorphizationDemand ->
    Set DemandId ->
    [DemandId] ->
    [DemandId] ->
    [DemandId] ->
    Either [DemandId] (Set DemandId, [DemandId])
visitMany _ complete _ output [] = Right (complete, output)
visitMany table complete active output (demandId : remaining) = do
    (updatedComplete, updatedOutput) <- visitOne table complete active output demandId
    visitMany table updatedComplete active updatedOutput remaining

visitOne ::
    Map DemandId MonomorphizationDemand ->
    Set DemandId ->
    [DemandId] ->
    [DemandId] ->
    DemandId ->
    Either [DemandId] (Set DemandId, [DemandId])
visitOne table complete active output demandId
    | demandId `Set.member` complete = Right (complete, output)
    | demandId `elem` active = Left (cycleFrom demandId active)
    | otherwise = case Map.lookup demandId table of
        Nothing -> Right (complete, output)
        Just demand -> do
            let nextActive = demandId : active
                dependencies = sort (monomorphizationDependencies demand)
            (childrenComplete, childrenOutput) <- visitMany table complete nextActive output dependencies
            pure (Set.insert demandId childrenComplete, demandId : childrenOutput)

-- | Render one demand with its identity, depth, origins, and outgoing edges.
renderDemandTrace :: MonomorphizationPlan -> DemandId -> String
renderDemandTrace plan demandId = case Map.lookup demandId (demandTable plan) of
    Nothing -> "unknown demand " ++ renderDemandId demandId
    Just demand ->
        renderDemandId demandId
            ++ " "
            ++ specializationIdentity (monomorphizationSpecialization demand)
            ++ " depth="
            ++ show (monomorphizationDepth demand)
            ++ " origins=["
            ++ intercalate ", " (map renderOrigin (monomorphizationOrigins demand))
            ++ "] dependencies=["
            ++ intercalate ", " (map renderDemandId (monomorphizationDependencies demand))
            ++ "]"
    where
        renderOrigin origin = case origin of
            RootDemand location -> renderDemandLocation location
            DependencyDemand parent argumentIndex ->
                renderDemandId parent ++ ".argument[" ++ show argumentIndex ++ "]"

demandTable :: MonomorphizationPlan -> Map DemandId MonomorphizationDemand
demandTable =
    Map.fromList
        . map (\demand -> (monomorphizationDemandId demand, demand))
        . monomorphizationDemands

renderDemandId :: DemandId -> String
renderDemandId (DemandId value) = "demand#" ++ show value

cycleFrom :: DemandId -> [DemandId] -> [DemandId]
cycleFrom repeated active = repeated : reverse (takeThrough repeated active)
    where
        takeThrough _ [] = []
        takeThrough target (value : remaining)
            | value == target = [value]
            | otherwise = value : takeThrough target remaining

grouped :: (Ord value) => [value] -> [[value]]
grouped values = foldr collect [] (sort values)
    where
        collect value [] = [[value]]
        collect value groups@(first@(firstValue : _) : remaining)
            | value == firstValue = (value : first) : remaining
            | otherwise = [value] : groups
        collect value ([] : remaining) = [value] : remaining