packages feed

ychr-0.1.0.0: src/YCHR/Internal/Collect.hs

{-# LANGUAGE OverloadedStrings #-}

-- | Library import collector.
--
-- Resolves @use_module(library(name))@ imports against the standard
-- library map. The two responsibilities are now exposed as separate
-- functions so the pipeline can build per-module operator tables before
-- the full parse:
--
--   * 'resolveLibraryClosure' walks the dependency graph from a set of
--     seed library names, returns the reachable libraries in topological
--     order, and reports unknown or circularly-imported libraries.
--
--   * 'rewriteImports' converts each parsed 'Module' to a
--     'CollectedModule', collapsing 'LibraryImport' and 'ModuleImport'
--     into the single 'CollectedImport' so everything downstream sees
--     only one kind of import.
--
--   * 'addLibraryPrelude' prepends a @prelude@ import to each library
--     module (except the prelude itself).
module YCHR.Internal.Collect
  ( CollectError (..),
    resolveLibraryClosure,
    rewriteImports,
    addLibraryPrelude,
  )
where

import Data.Map.Strict (Map)
import Data.Map.Strict qualified as Map
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import YCHR.Internal.Collected (CollectedModule, collectedFromParsed)
import YCHR.Internal.Diagnostic (Diagnostic, noDiag)
import YCHR.Internal.Parsed

data CollectError
  = UnknownLibrary Text
  | CircularLibraryImport [Text]
  deriving (Show, Eq)

-- | Walk the transitive closure of library imports.
--
-- Seeds are the user-supplied library import names (typically extracted
-- from each user module's header). When @includeStdlib@ is 'True', every
-- library in the standard library map is also seeded so the full standard
-- library is available regardless of which libraries the user imports
-- explicitly.
--
-- Returns the reachable libraries in topological order (dependencies
-- first), or a list of 'CollectError' diagnostics if any seed names an
-- unknown library or a cycle is detected.
resolveLibraryClosure ::
  Bool ->
  Map Text Module ->
  [AnnP Text] ->
  Either [Diagnostic CollectError] [Module]
resolveLibraryClosure includeStdlib stdlibMap userSeeds =
  let seeds =
        (if includeStdlib then map noAnnP (Map.keys stdlibMap) else [])
          ++ userSeeds
   in case resolveAll stdlibMap Set.empty Set.empty seeds of
        (_, libs, []) -> Right libs
        (_, _, errs) -> Left errs

-- | DFS resolution of library dependencies.
--
-- Processes a worklist of library names. For each name:
--   - If already visited (fully processed), skip.
--   - If on the current path (gray), report a cycle.
--   - Look up in the stdlib map.
--   - Recursively resolve its own library imports.
--   - Append to the result list (post-order ensures dependencies come first).
--
-- The visited set is threaded through so that a library resolved by one
-- sibling is not re-resolved by the next.
resolveAll ::
  Map Text Module ->
  Set Text ->
  Set Text ->
  [AnnP Text] ->
  (Set Text, [Module], [Diagnostic CollectError])
resolveAll _ visited _ [] = (visited, [], [])
resolveAll stdlibMap visited path (ann : rest)
  | Set.member name visited = resolveAll stdlibMap visited path rest
  | Set.member name path =
      let (visited', restMods, restErrs) = resolveAll stdlibMap visited path rest
       in ( visited',
            restMods,
            noDiag
              ( AnnP
                  ( CircularLibraryImport
                      ( Set.toList path
                          ++ [name]
                      )
                  )
                  ann.sourceLoc
                  ann.parsed
              )
              : restErrs
          )
  | otherwise =
      case Map.lookup name stdlibMap of
        Nothing ->
          let (visited', restMods, restErrs) = resolveAll stdlibMap visited path rest
           in ( visited',
                restMods,
                noDiag (AnnP (UnknownLibrary name) ann.sourceLoc ann.parsed) : restErrs
              )
        Just m ->
          let deps = libraryImports m
              path' = Set.insert name path
              (visited1, depMods, depErrs) = resolveAll stdlibMap visited path' deps
              visited2 = Set.insert name visited1
              (visited3, restMods, restErrs) = resolveAll stdlibMap visited2 path rest
           in (visited3, depMods ++ [m] ++ restMods, depErrs ++ restErrs)
  where
    name = ann.node

-- | Extract library import names from a module.
libraryImports :: Module -> [AnnP Text]
libraryImports m = [AnnP n loc p | AnnP (LibraryImport n _) loc p <- m.imports]

-- | Add a prelude import to every library module that does not already
-- declare itself to be the prelude.
addLibraryPrelude :: [Module] -> [Module]
addLibraryPrelude = map go
  where
    go :: Module -> Module
    go m
      | m.name == "prelude" = m
      | otherwise = m {imports = noAnnP (LibraryImport "prelude" Nothing) : m.imports}

-- | Convert each parsed 'Module' to a 'CollectedModule', collapsing both
-- import kinds into 'CollectedImport'. After this point the
-- library-vs-module distinction no longer exists in the types. The
-- per-module conversion lives in "YCHR.Internal.Collected" ('collectedFromParsed').
rewriteImports :: [Module] -> [CollectedModule]
rewriteImports = map collectedFromParsed