packages feed

hydra-0.14.0: src/exec/verify-json-kernel/Main.hs

-- | Verification tool that decodes all kernel modules from JSON and compares with originals.
-- This is a standalone executable that does not run with regular tests (too slow).
--
-- This tests that: JSON file → parse → Term → Module equals the original Module

module Main where

import Hydra.Kernel
import Hydra.Module (_Module)
import Hydra.Sources.All (kernelModules)
import Hydra.Generation (modulesToGraph)
import Hydra.CodeGeneration (namespaceToPath)
import qualified Hydra.Json.Model as Json
import qualified Hydra.Json.Decode as JsonDecode
import qualified Hydra.Decode.Module as DecodeModule
import qualified Hydra.Decode.Core as DecodeCore
import qualified Hydra.Rewriting as Rewriting

import Control.Monad (forM, when)
import System.Exit (exitFailure, exitSuccess)
import System.Directory (doesFileExist)
import System.FilePath ((</>))
import System.IO (hFlush, stdout)
import qualified Data.Map as M
import qualified Data.ByteString.Lazy as BS
import qualified Data.Aeson as A
import qualified Data.Aeson.KeyMap as AKM
import qualified Data.Aeson.Key as AK
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Scientific as SC

flushPut :: String -> IO ()
flushPut s = putStrLn s >> hFlush stdout

-- | Convert Aeson JSON value to Hydra JSON value
aesonToHydra :: A.Value -> Json.Value
aesonToHydra v = case v of
  A.Object km -> Json.ValueObject $ M.fromList (mapPair <$> AKM.toList km)
    where
      mapPair (k, v') = (AK.toString k, aesonToHydra v')
  A.Array a -> Json.ValueArray (aesonToHydra <$> V.toList a)
  A.String t -> Json.ValueString $ T.unpack t
  A.Number s -> Json.ValueNumber $ SC.toRealFloat s
  A.Bool b -> Json.ValueBoolean b
  A.Null -> Json.ValueNull

-- | Parse JSON from file using Aeson (fast)
parseJsonFile :: FilePath -> IO (Either String Json.Value)
parseJsonFile fp = do
  content <- BS.readFile fp
  return $ aesonToHydra <$> A.eitherDecode content

-- | Build a schema map (Name -> Type) from a graph's schema types.
-- This is used by the JSON decoder to resolve type variables.
-- Uses graphSchemaTypes (which contains TypeScheme values) and extracts the underlying Type.
buildSchemaMap :: Graph -> M.Map Name Type
buildSchemaMap g = M.map extractType (graphSchemaTypes g)
  where
    extractType (TypeScheme _ t _) = stripTypeAnnotationsTop t

    -- Strip only top-level annotations
    stripTypeAnnotationsTop (TypeAnnotated (AnnotatedType t _)) = stripTypeAnnotationsTop t
    stripTypeAnnotationsTop t = t

main :: IO ()
main = do
  flushPut "=== Verify JSON Kernel ==="
  flushPut ""

  let basePath = "src/gen-main/json"

  flushPut "Building graph from kernel modules..."
  -- Build the graph from all kernel modules - this provides the type info
  let graph = modulesToGraph kernelModules kernelModules

  flushPut "Building schema map for JSON decoding..."
  let schemaMap = buildSchemaMap graph
  flushPut $ "  Schema map has " ++ show (M.size schemaMap) ++ " type definitions"

  -- Use TypeVariable to reference the Module type - the decoder will resolve it
  let modType = TypeVariable _Module
  flushPut "Using TypeVariable for Module type (decoder will resolve it)."

  flushPut "Counting modules..."
  let numModules = length kernelModules
  flushPut $ "Verifying " ++ show numModules ++ " kernel modules..."
  putStrLn ""

  -- For each module, read JSON file, decode to Term, then to Module, and compare
  results <- forM kernelModules $ \origMod -> do
    let ns = moduleNamespace origMod
        nsStr = unNamespace ns
        filePath = basePath </> namespaceToPath ns ++ ".json"

    flushPut $ "  Processing: " ++ nsStr

    exists <- doesFileExist filePath
    if not exists
      then do
        flushPut $ "  ✗ " ++ nsStr ++ " (file not found)"
        return (False, nsStr ++ ": file not found")
      else do
        -- Parse JSON using Aeson (fast!)
        parseResult <- parseJsonFile filePath
        case parseResult of
          Left err -> do
            flushPut $ "  ✗ " ++ nsStr ++ " (JSON parse error)"
            flushPut $ "    " ++ err
            return (False, nsStr ++ ": JSON parse error")
          Right jsonVal -> do
            flushPut $ "    JSON parsed successfully"

            -- Decode JSON to Term using type-directed decoder with Module type
            -- The schemaMap is used to resolve type variables
            case JsonDecode.fromJson schemaMap (Name "hydra.module.Module") modType jsonVal of
              Left err -> do
                flushPut $ "  ✗ " ++ nsStr ++ " (JSON decode error)"
                flushPut $ "    " ++ err
                return (False, nsStr ++ ": " ++ err)
              Right term -> do
                flushPut $ "    JSON decoded to Term"

                -- Decode Term to Module
                case DecodeModule.module_ graph term of
                  Left (DecodingError err) -> do
                    flushPut $ "  ✗ " ++ nsStr ++ " (module decode error)"
                    flushPut $ "    " ++ err
                    return (False, nsStr ++ ": " ++ err)
                  Right decodedMod ->
                    let compareMod = if isTypeModule origMod then decodedMod else stripTypeAnnotations decodedMod
                        compareOrig = if isTypeModule origMod then origMod else stripTypeAnnotations origMod
                    in if compareMod == compareOrig
                      then do
                        flushPut $ "  ✓ " ++ nsStr
                        return (True, nsStr)
                      else do
                        flushPut $ "  ✗ " ++ nsStr ++ " (content mismatch)"
                        let diff = findDifference compareOrig compareMod
                        flushPut $ "    " ++ diff
                        return (False, nsStr ++ ": " ++ diff)

  let failures = filter (not . fst) results
      successes = filter fst results

  putStrLn ""
  if null failures
    then do
      putStrLn "=== SUCCESS ==="
      putStrLn $ "All " ++ show (length successes) ++ " modules verified successfully!"
      exitSuccess
    else do
      putStrLn "=== FAILED ==="
      putStrLn $ show (length failures) ++ " modules failed verification:"
      mapM_ (putStrLn . ("  " ++) . snd) (take 20 failures)
      when (length failures > 20) $
        putStrLn $ "  ... and " ++ show (length failures - 20) ++ " more"
      exitFailure

-- | Check whether a module contains only native type definitions (no term definitions).
isTypeModule :: Module -> Bool
isTypeModule m = all isNativeType (moduleBindings m)

-- | Strip type annotations from a module for comparison with raw (pre-inference) modules.
-- Removes TypeLambda, TypeApplication, binding TypeSchemes, and lambda domain types from terms,
-- but preserves user annotations.
stripTypeAnnotations :: Module -> Module
stripTypeAnnotations m = m {
  moduleDefinitions = fmap stripDef (moduleDefinitions m) }
  where
    stripDef (DefinitionTerm td) = DefinitionTerm td {
      termDefinitionTerm = Rewriting.removeTypesFromTerm (termDefinitionTerm td),
      termDefinitionType = Just $ TypeScheme [] (TypeVariable $ Name "hydra.core.Unit") Nothing }
    stripDef d = d

-- | Find the first difference between two modules
findDifference :: Module -> Module -> String
findDifference orig decoded
  | moduleNamespace orig /= moduleNamespace decoded =
      "namespace differs: " ++ unNamespace (moduleNamespace orig) ++ " vs " ++ unNamespace (moduleNamespace decoded)
  | length (moduleDefinitions orig) /= length (moduleDefinitions decoded) =
      "element count differs: " ++ show (length (moduleDefinitions orig)) ++ " vs " ++ show (length (moduleDefinitions decoded))
  | moduleTermDependencies orig /= moduleTermDependencies decoded =
      "termDependencies differ"
  | moduleTypeDependencies orig /= moduleTypeDependencies decoded =
      "typeDependencies differ"
  | moduleDescription orig /= moduleDescription decoded =
      "description differs"
  | otherwise =
      "elements differ (checking first mismatch...)" ++ findElementDiff (moduleDefinitions orig) (moduleDefinitions decoded)

findElementDiff :: [Definition] -> [Definition] -> String
findElementDiff [] [] = ""
findElementDiff (o:os) (d:ds)
  | o == d = findElementDiff os ds
  | otherwise = " - definition differs at " ++ show (defName o) ++ " vs " ++ show (defName d)
  where
    defName (DefinitionTerm td) = termDefinitionName td
    defName (DefinitionType td) = typeDefinitionName td
findElementDiff _ _ = " - different element counts"