packages feed

synapse-cc-0.1.0.0: src/SynapseCC/Dependency.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Dependency graph resolution for cache invalidation
module SynapseCC.Dependency
  ( -- * Dependency graph operations
    buildDependencyGraph
  , findInvalidPlugins
  , topologicalSort
  , DependencyGraph
  ) where

import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Set (Set)
import qualified Data.Set as Set
import Data.Text (Text)

-- ============================================================================
-- Types
-- ============================================================================

-- | Dependency graph: plugin name -> list of dependencies
type DependencyGraph = Map Text [Text]

-- ============================================================================
-- Graph Construction
-- ============================================================================

-- | Build dependency graph from IR plugin cache
-- Maps each plugin to its list of dependencies
buildDependencyGraph :: Map Text [Text] -> DependencyGraph
buildDependencyGraph pluginDeps = pluginDeps

-- ============================================================================
-- Invalidation Resolution
-- ============================================================================

-- | Find all plugins that need regeneration due to dependencies
-- Takes a set of initially invalid plugins and a dependency graph,
-- returns all plugins that are transitively invalid.
--
-- Example:
--   If 'arbor' is invalid and 'cone' depends on 'arbor',
--   then both 'arbor' and 'cone' are invalid.
findInvalidPlugins
  :: Set Text          -- ^ Initially invalid plugins
  -> DependencyGraph   -- ^ Dependency map (plugin -> its dependencies)
  -> Set Text          -- ^ All invalid plugins (with transitive deps)
findInvalidPlugins initialInvalid depGraph =
  let -- Find reverse dependencies: which plugins depend on each plugin
      reverseDeps = buildReverseDependencyMap depGraph

      -- Recursively find all dependents
      allInvalid = fixpoint (expandInvalid reverseDeps) initialInvalid
  in allInvalid

-- | Build reverse dependency map: for each plugin, which other plugins depend on it
buildReverseDependencyMap :: DependencyGraph -> Map Text (Set Text)
buildReverseDependencyMap depGraph =
  Map.foldrWithKey addReverseDeps Map.empty depGraph
  where
    addReverseDeps :: Text -> [Text] -> Map Text (Set Text) -> Map Text (Set Text)
    addReverseDeps plugin deps acc =
      foldr (\dep m -> Map.insertWith Set.union dep (Set.singleton plugin) m) acc deps

-- | Expand the set of invalid plugins by one step (add direct dependents)
expandInvalid :: Map Text (Set Text) -> Set Text -> Set Text
expandInvalid reverseDeps invalid =
  let -- Find all plugins that directly depend on any invalid plugin
      newInvalid = Set.foldr
        (\invalidPlugin acc ->
          case Map.lookup invalidPlugin reverseDeps of
            Nothing -> acc  -- No dependents
            Just dependents -> Set.union acc dependents
        )
        Set.empty
        invalid
  in Set.union invalid newInvalid

-- | Apply a function repeatedly until the result stops changing (fixed point)
fixpoint :: Eq a => (a -> a) -> a -> a
fixpoint f x =
  let x' = f x
  in if x' == x then x else fixpoint f x'

-- ============================================================================
-- Topological Sort
-- ============================================================================

-- | Topological sort for regeneration order
-- Returns plugins in dependency order (dependencies before dependents)
-- If there are cycles, returns an arbitrary but valid partial order
topologicalSort :: DependencyGraph -> [Text]
topologicalSort depGraph =
  let allPlugins = Set.union
        (Map.keysSet depGraph)
        (Set.unions $ map Set.fromList $ Map.elems depGraph)
      sorted = topSort depGraph allPlugins []
  in sorted

-- | Kahn's algorithm for topological sorting
topSort :: DependencyGraph -> Set Text -> [Text] -> [Text]
topSort depGraph remaining acc
  | Set.null remaining = reverse acc
  | otherwise =
      -- Find nodes with no dependencies (or all dependencies already processed)
      let nodesWithNoDeps = Set.filter (hasNoDependencies depGraph (Set.fromList acc)) remaining
      in if Set.null nodesWithNoDeps
           then
             -- Cycle detected or no more nodes without deps
             -- Just take remaining nodes in arbitrary order
             reverse acc ++ Set.toList remaining
           else
             -- Process one node with no dependencies
             let node = Set.findMin nodesWithNoDeps
                 remaining' = Set.delete node remaining
             in topSort depGraph remaining' (node : acc)

-- | Check if a plugin has no unprocessed dependencies
hasNoDependencies :: DependencyGraph -> Set Text -> Text -> Bool
hasNoDependencies depGraph processed plugin =
  case Map.lookup plugin depGraph of
    Nothing -> True  -- No dependencies
    Just deps -> all (`Set.member` processed) deps