commit 077459ab3136d765e096346645f4ce007714dc5f
Author: Coder Agent <coder@agents.omni>
Date: Thu Feb 19 14:58:08 2026
bild: propagate dep-commented module langdeps through hsGraph
Task-Id: t-492
diff --git a/Omni/Bild.hs b/Omni/Bild.hs
index 449dc395..3a01e81f 100755
--- a/Omni/Bild.hs
+++ b/Omni/Bild.hs
@@ -190,6 +190,7 @@ main = Cli.Plan parser test_ pure |> Cli.main
test_isGitIgnored,
test_isGitHook,
test_detectPythonImports,
+ test_detectHaskellImports,
test_buildHsModuleGraph
]
@@ -742,10 +743,8 @@ analyzeOne namespace@(Namespace parts ext) = do
Namespace.Hs ->
contentLines
|> Meta.detectAll "--"
- |> \Meta.Parsed {..} ->
- detectHaskellImports mempty contentLines +> \(autoDeps, srcs) -> do
- let manualDeps = detectDepPrefixes "--" contentLines
- let langdeps = autoDeps <> pdep <> manualDeps
+ |> \Meta.Parsed {pout, prun} ->
+ detectHaskellImports mempty contentLines +> \(langdeps, srcs) -> do
graph <- buildHsModuleGraph namespace quapath srcs
pure
<| Just
@@ -951,10 +950,13 @@ detectHaskellImports _ contentLines = do
root <- getCoderoot
let initialMods = catMaybes (Regex.match haskellImports </ (Text.unpack </ contentLines))
initialLocals <- toLocalFiles root initialMods
- let initialLocalsSet = Set.fromList initialLocals
let localMods = [m | m <- initialMods, (Namespace.fromHaskellModule m |> Namespace.toPath) `elem` initialLocals]
let initialExternals = filter (`notElem` localMods) initialMods
- (srcs, transitiveExtMods, transitiveDeps) <- bfs root initialLocalsSet Set.empty Set.empty Set.empty
+ let Meta.Parsed {pdep = parsedEntryDeps} = Meta.detectAll "--" contentLines
+ let entryCommentDeps = parsedEntryDeps <> detectDepPrefixes "--" contentLines
+ (entryLocalDeps, entryLangDeps) <- partitionHaskellDepComments root entryCommentDeps
+ let initialLocalsSet = Set.fromList initialLocals <> entryLocalDeps
+ (srcs, transitiveExtMods, transitiveDeps) <- bfs root initialLocalsSet Set.empty Set.empty entryLangDeps
let allExtMods = Set.fromList initialExternals <> transitiveExtMods
pkgSets <- Async.mapConcurrently ghcPkgFindModuleCached (Set.toList allExtMods)
let pkgs = mconcat pkgSets <> transitiveDeps
@@ -975,15 +977,44 @@ detectHaskellImports _ contentLines = do
let localsSet = Set.fromList locals
let localModsFromPaths = Set.fromList [m | m <- mods, (Namespace.fromHaskellModule m |> Namespace.toPath) `elem` locals]
let newExternals = Set.fromList mods Set.\\ localModsFromPaths
- let newLocals = localsSet Set.\\ visited
- let Meta.Parsed {pdep = fileDeps} = Meta.detectAll "--" fileLines
- bfs root (queue' <> newLocals) (Set.insert rel visited) (extMods <> newExternals) (deps <> fileDeps)
+ let Meta.Parsed {pdep = parsedFileDeps} = Meta.detectAll "--" fileLines
+ let fileCommentDeps = parsedFileDeps <> detectDepPrefixes "--" fileLines
+ (localCommentDeps, langCommentDeps) <- partitionHaskellDepComments root fileCommentDeps
+ let seen = Set.insert rel visited
+ let newLocals = (localsSet <> localCommentDeps) Set.\\ seen
+ bfs root (queue' <> newLocals) seen (extMods <> newExternals) (deps <> langCommentDeps)
toLocalFiles :: FilePath -> [String] -> IO [FilePath]
toLocalFiles root mods = do
let rels = map (Namespace.fromHaskellModule .> Namespace.toPath) mods
filterM (\rel -> Dir.doesFileExist (root </> rel)) rels
+resolveLocalHaskellDep :: FilePath -> Meta.Dep -> IO (Maybe FilePath)
+resolveLocalHaskellDep root dep = do
+ let normalized = fromMaybe dep (List.stripPrefix "./" dep)
+ let depAsPathWithExt =
+ List.isSuffixOf ".hs" normalized
+ ?: (normalized, normalized <> ".hs")
+ let candidates =
+ [ normalized,
+ depAsPathWithExt,
+ Namespace.fromHaskellModule normalized |> Namespace.toPath
+ ]
+ |> List.nub
+ filterM (\rel -> Dir.doesFileExist (root </> rel)) candidates +> \existing ->
+ pure (head existing)
+
+partitionHaskellDepComments :: FilePath -> Set Meta.Dep -> IO (Set FilePath, Set Meta.Dep)
+partitionHaskellDepComments root deps =
+ foldM
+ ( \(localDeps, langDeps) dep ->
+ resolveLocalHaskellDep root dep +> \case
+ Just rel -> pure (Set.insert rel localDeps, langDeps)
+ Nothing -> pure (localDeps, Set.insert dep langDeps)
+ )
+ (Set.empty, Set.empty)
+ (Set.toList deps)
+
detectDepPrefixes :: [Char] -> [Text] -> Set Meta.Dep
detectDepPrefixes comment contentLines =
let commentPrefix = Text.pack comment
@@ -1063,6 +1094,22 @@ test_detectPythonImports =
Set.fromList ["Omni/Log.py"] @=? srcs
]
+test_detectHaskellImports :: Test.Tree
+test_detectHaskellImports =
+ Test.unit "collects deps from dep-commented local modules" <| do
+ root <- getCoderoot
+ let testDir = "_/tmp/bild-haskell-deps-test"
+ let depRelPath = testDir </> "Dep.hs"
+ let depAbsPath = root </> depRelPath
+ Dir.createDirectoryIfMissing True (takeDirectory depAbsPath)
+ writeFile depAbsPath (Text.unlines ["module Tmp.Dep where", "-- : dep sqlite-simple"])
+
+ let entryLines = ["module Tmp.Entry where", "-- : dep " <> Text.pack depRelPath]
+ (langdeps, srcs) <- detectHaskellImports mempty entryLines
+
+ Set.member depRelPath srcs @=? True
+ Set.member "sqlite-simple" langdeps @=? True
+
test_buildHsModuleGraph :: Test.Tree
test_buildHsModuleGraph =
Test.group
@@ -1214,11 +1261,17 @@ buildHsModuleGraph namespace entryPoint deps = do
/> Text.lines
let importedMods = catMaybes (Regex.match haskellImports </ (Text.unpack </ fileLines))
localImportMods <- filterLocalImports root importedMods
+ let Meta.Parsed {pdep = parsedFileDeps} = Meta.detectAll "--" fileLines
+ let fileCommentDeps = parsedFileDeps <> detectDepPrefixes "--" fileLines
+ (localCommentDeps, _) <- partitionHaskellDepComments root fileCommentDeps
+ let localImportSet = Set.fromList (map Text.pack localImportMods)
+ let commentImportSet = Set.fromList (map pathToModuleName (Set.toList localCommentDeps))
+ let nodeImports = (localImportSet <> commentImportSet) |> Set.delete modName |> Set.toList
let hasTH = detectTH fileLines
let node =
HsModuleNode
{ nodePath = srcPath,
- nodeImports = map Text.pack localImportMods,
+ nodeImports = nodeImports,
nodeHasTH = hasTH
}
pure (Map.insert modName node acc)