hydra-0.15.0: src/exec/diag-module/Main.hs
-- Diagnostic: bisect the incremental-inference bug down to individual
-- definitions.
--
-- Modes:
-- stack exec diag-module -- <ns1,ns2,...>
-- Use full modules as the dirty set; load the rest from dist/json.
--
-- stack exec diag-module -- --subset <ns> <defName1,defName2,...>
-- Build a synthetic module containing only the named definitions
-- of <ns>, use that as the single dirty module, load the rest of
-- the universe (including the full version of <ns>) from dist/json.
-- This lets us narrow the failure to a specific definition.
--
-- stack exec diag-module -- --list-defs <ns>
-- Print the definition names of the module, one per line.
module Main where
import Hydra.Kernel
import Hydra.Generation (inferModulesGivenIO)
import qualified Hydra.Codegen as CodeGeneration
import qualified Hydra.Sources.All as All
import qualified Hydra.Sources.Ext as Ext
import qualified Hydra.Show.Errors as ShowError
import qualified Hydra.Json.Model as JsonModel
import Hydra.Dsl.Bootstrap (bootstrapGraph)
import qualified Hydra.PackageRouting as PackageRouting
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BS
import qualified Data.Aeson.KeyMap as AKM
import qualified Data.Aeson.Key as AK
import qualified Data.Vector as V
import qualified Data.Text as T
import qualified Data.Map as M
import qualified Data.List as L
import qualified Control.Exception as E
import qualified Control.Monad as CM
import qualified System.Environment as Env
import qualified System.FilePath as FP
import qualified System.Directory as SD
import System.IO (hSetBuffering, stdout, BufferMode(..))
parseJsonFile :: FilePath -> IO (Either String JsonModel.Value)
parseJsonFile path = do
bytes <- BS.readFile path
case A.eitherDecode bytes of
Left err -> return (Left err)
Right aesonVal -> return (Right (aesonToJsonModel aesonVal))
aesonToJsonModel :: A.Value -> JsonModel.Value
aesonToJsonModel v = case v of
A.Null -> JsonModel.ValueNull
A.Bool b -> JsonModel.ValueBoolean b
A.Number n -> JsonModel.ValueNumber n
A.String t -> JsonModel.ValueString (T.unpack t)
A.Array xs -> JsonModel.ValueArray (fmap aesonToJsonModel (V.toList xs))
A.Object o -> JsonModel.ValueObject (M.fromList [(T.unpack (AK.toText k), aesonToJsonModel x) | (k, x) <- AKM.toList o])
loadModuleFromJson :: FilePath -> [Module] -> Namespace -> IO Module
loadModuleFromJson distJsonRoot universe ns = do
let pkg = PackageRouting.namespaceToPackage ns
pkgDir = distJsonRoot FP.</> pkg FP.</> "src" FP.</> "main" FP.</> "json"
filePath = pkgDir FP.</> CodeGeneration.namespaceToPath ns ++ ".json"
parseResult <- parseJsonFile filePath
case parseResult of
Left err -> fail $ "JSON parse error for " ++ unNamespace ns ++ " at " ++ filePath ++ ": " ++ err
Right jsonVal -> case CodeGeneration.decodeModuleFromJson bootstrapGraph universe jsonVal of
Left err -> fail $ "Module decode error for " ++ unNamespace ns ++ ": " ++ ShowError.error err
Right m -> return m
main :: IO ()
main = do
hSetBuffering stdout NoBuffering
args <- Env.getArgs
case args of
["--list-defs", ns] -> listDefs ns
["--subset", ns, defSpec] -> runSubset ns defSpec
[spec] -> runModules spec
_ -> fail "Usage: diag-module [--list-defs <ns>] [--subset <ns> <def1,def2,...>] [<ns1,ns2,...>]"
listDefs :: String -> IO ()
listDefs nsStr = do
let ns = Namespace nsStr
universe = All.mainModules ++ Ext.hydraExtModules
case L.find (\m -> moduleNamespace m == ns) universe of
Nothing -> fail $ "No such module: " ++ nsStr
Just m -> do
let defs = moduleDefinitions m
putStrLn $ "Definitions in " ++ nsStr ++ " (" ++ show (length defs) ++ " total):"
mapM_ putStrLn (definitionNames defs)
definitionNames :: [Definition] -> [String]
definitionNames ds = fmap definitionName ds
where
definitionName d = case d of
DefinitionTerm td -> unName (termDefinitionName td)
DefinitionType td -> unName (typeDefinitionName td)
runModules :: String -> IO ()
runModules spec = do
let universe = All.mainModules ++ Ext.hydraExtModules
putStrLn $ "Universe: " ++ show (length universe) ++ " modules"
let allKernelNss = [moduleNamespace m | m <- universe,
let ns = unNamespace (moduleNamespace m),
not ("hydra.test." `L.isPrefixOf` ns),
not ("hydra.ext." `L.isPrefixOf` ns),
not ("hydra.wasm." `L.isPrefixOf` ns),
not ("hydra.coq." `L.isPrefixOf` ns),
not ("hydra.javascript." `L.isPrefixOf` ns)]
half = length allKernelNss `div` 2
dirtyNss = case spec of
"ALL" -> allKernelNss
"HALF1" -> take half allKernelNss
"HALF2" -> drop half allKernelNss
_ -> fmap Namespace (splitCommas spec)
putStrLn $ "Dirty set: " ++ show (length dirtyNss) ++ " modules"
CM.when (length dirtyNss <= 20) $
mapM_ (\ns -> putStrLn $ " " ++ unNamespace ns) dirtyNss
let dirtySet = M.fromList [(ns, ()) | ns <- dirtyNss]
dirtyMods = [m | m <- universe, M.member (moduleNamespace m) dirtySet]
cleanMods = [m | m <- universe, not (M.member (moduleNamespace m) dirtySet)]
runWithCleanAndDirty cleanMods dirtyMods
-- Subset mode: build a dirty module at namespace "hydra.bisect" that
-- contains CLONES of the named definitions from <ns>. The full <ns> stays
-- in the clean universe, so cross-definition references in the clones
-- resolve against it (i.e. clone of X calling Y sees clean X.Y). The
-- cloned definition names are `hydra.bisect.<lastComponent>`, so there's
-- no name collision with the clean universe.
--
-- This isolates whether a specific definition's term body / type
-- annotation triggers the unification bug, without perturbing the rest
-- of the universe.
runSubset :: String -> String -> IO ()
runSubset nsStr defSpec = do
let ns = Namespace nsStr
universe = All.mainModules ++ Ext.hydraExtModules
wanted = splitCommas defSpec
bisectNs = Namespace "hydra.bisect"
case L.find (\m -> moduleNamespace m == ns) universe of
Nothing -> fail $ "No such module: " ++ nsStr
Just fullMod -> do
let allDefs = moduleDefinitions fullMod
lastCompOf s = L.reverse (L.takeWhile (/= '.') (L.reverse s))
defMatches n d = case d of
DefinitionTerm td -> lastCompOf (unName (termDefinitionName td)) == n
DefinitionType td -> lastCompOf (unName (typeDefinitionName td)) == n
picked = [d | n <- wanted, d <- allDefs, defMatches n d]
let defName d = case d of
DefinitionTerm td -> unName (termDefinitionName td)
DefinitionType td -> unName (typeDefinitionName td)
CM.when (length picked /= length wanted) $ do
putStrLn "Warning: not all requested definitions were found."
let found = fmap defName picked
missing = filter (\n -> not (elem n found)) wanted
mapM_ (\m -> putStrLn $ " missing: " ++ m) missing
let cloneDef d = case d of
DefinitionTerm td ->
DefinitionTerm td { termDefinitionName = rename (termDefinitionName td) }
DefinitionType td ->
DefinitionType td { typeDefinitionName = rename (typeDefinitionName td) }
rename (Name full) =
let simple = L.reverse (L.takeWhile (/= '.') (L.reverse full))
in Name ("hydra.bisect." ++ simple)
cloned = fmap cloneDef picked
let syntheticDirty = Module {
moduleNamespace = bisectNs,
moduleDefinitions = cloned,
moduleTermDependencies = [ns],
moduleTypeDependencies = [ns],
moduleDescription = Just "Bisection dummy module" }
putStrLn $ "Universe: " ++ show (length universe) ++ " kept intact; dirty is new hydra.bisect with " ++ show (length picked) ++ " cloned defs:"
mapM_ (\d -> putStrLn $ " " ++ defName d ++ " → " ++ cloneName d) picked
runWithCleanAndDirty universe [syntheticDirty]
where
cloneName d = case d of
DefinitionTerm td -> "hydra.bisect." ++ lastComp (unName (termDefinitionName td))
DefinitionType td -> "hydra.bisect." ++ lastComp (unName (typeDefinitionName td))
lastComp s = L.reverse (L.takeWhile (/= '.') (L.reverse s))
runWithCleanAndDirty :: [Module] -> [Module] -> IO ()
runWithCleanAndDirty cleanMods dirtyMods = do
putStrLn $ "Loading " ++ show (length cleanMods) ++ " clean modules from dist/json..."
distJsonRoot <- SD.makeAbsolute "../../dist/json"
cleanLoaded <- mapM (loadModuleFromJson distJsonRoot (cleanMods ++ dirtyMods) . moduleNamespace) cleanMods
putStrLn "Running inferModulesGiven..."
result <- E.try (inferModulesGivenIO (cleanLoaded ++ dirtyMods) dirtyMods) :: IO (Either E.SomeException [Module])
case result of
Left e -> do
putStrLn "FAILED"
putStrLn $ show e
Right _ -> putStrLn "OK"
splitCommas :: String -> [String]
splitCommas [] = []
splitCommas s = let (h, t) = break (== ',') s
in h : case t of
[] -> []
_ -> splitCommas (drop 1 t)