packages feed

hs-bindgen-1.0.0.0: src-internal/HsBindgen/Test/Hs.hs

module HsBindgen.Test.Hs (
    genTestsHs
  ) where

import HsBindgen.Backend.Category
import HsBindgen.Backend.Hs.AST qualified as Hs
import HsBindgen.Config
import HsBindgen.Errors

{-------------------------------------------------------------------------------
  Generation
-------------------------------------------------------------------------------}

-- | Generate Haskell test modules
genTestsHs ::
     FilePath              -- ^ Test module path
  -> FilePath              -- ^ Spec module path
  -> FilePath              -- ^ Main module path
  -> BaseModuleName        -- ^ Generated Haskell module name
  -> FilePath              -- ^ C test header file path
  -> ByCategory_ [Hs.Decl l] -- ^ Declarations
  -> IO ()
genTestsHs = throwPure_TODO 22 "generate test suite"

{-
genTestsHs
  hsTestPath
  hsSpecPath
  hsMainPath
  moduleName
  cTestHeaderPath
  lineLength
  decls = do
    writeFile hsTestPath $ renderPretty ctx HsTestModule{..}
    writeFile hsSpecPath $ renderPretty ctx (HsSpecModule hsTestModuleName)
    writeFile hsMainPath $ renderPretty ctx HsMainModule
  where
    ctx :: Context
    ctx = mkContext lineLength

    cTestHeaderFilename :: FilePath
    cTestHeaderFilename = FilePath.takeFileName cTestHeaderPath

    cFunPrefix :: CFunPrefix
    cFunPrefix = getCFunPrefix cTestHeaderFilename

    hsTestModulePragmas :: [Pragma]
    hsTestModulePragmas =
      [ "LANGUAGE CApiFFI"
      , "LANGUAGE RecordWildCards"
      , "LANGUAGE TypeApplications"
      , "OPTIONS_GHC -Wno-orphans"
      ]

    hsTestModuleName :: ModuleName
    hsTestModuleName = moduleName ++ ".Test"

    hsTestModuleImports :: [Import]
    hsTestModuleImports = List.sort
      -- source below ordered by abbreviation
      [ ("Foreign",                             "F")
      , ("Foreign.C",                           "FC")
      , ("HsBindgen.TestRuntime.GenSeq",        "GenSeq")
      , ("HsBindgen.TestRuntime.Perturb",       "Perturb")
      , ("Data.Proxy",                          "Proxy")
      , ("Test.QuickCheck",                     "QC")
      , ("HsBindgen.TestRuntime.RepZero",       "RepZero")
      , ("HsBindgen.TestRuntime.SameSemantics", "SameSemantics")
      , ("HsBindgen.TestRuntime.Storable",      "Storable")
      , ("Test.Tasty",                          "Tasty")
      , ("Test.Tasty.HUnit",                    "THU")
      , ("Test.Tasty.QuickCheck",               "TQC")
      , (moduleName,                            "X")
      ]

    hsTestModuleFfiFunctions :: [FfiFunction]
    hsTestModuleFfiFunctions =
      concatMap (getFfiFunctions cTestHeaderFilename cFunPrefix) decls

    hsTestModuleOrphanInstances :: [OrphanInstance]
    hsTestModuleOrphanInstances = concatMap getOrphanInstances decls

    hsTestModuleTypeTests :: [TypeTest]
    hsTestModuleTypeTests = concatMap getTypeTests decls

    hsTestModuleTestsFun :: TestsFun
    hsTestModuleTestsFun =
      TestsFun moduleName $ concatMap getTestsFunNames decls

getFfiFunctions :: IncludeFile -> CFunPrefix -> Hs.Decl -> [FfiFunction]
getFfiFunctions includeFile cFunPrefix = \case
    Hs.DeclDefineInstance instanceDecl -> case instanceDecl of
      Hs.InstanceStorable Hs.Struct{..} _storableInstance ->
        [ FfiSizeof   includeFile cFunPrefix structName
        , FfiAlignof  includeFile cFunPrefix structName
        , FfiGenSeqHs includeFile cFunPrefix structName
        , FfiGenSeqC  includeFile cFunPrefix structName
        , FfiPerturb  includeFile cFunPrefix structName
        ]
      _otherwise -> []
    _otherwise -> []

getOrphanInstances :: Hs.Decl -> [OrphanInstance]
getOrphanInstances = \case
    Hs.DeclDefineInstance instanceDecl -> case instanceDecl of
      Hs.InstanceStorable Hs.Struct{..} _storableInstance ->
        let fieldNames = Hs.fieldName <$> structFields
        in  [ ArbitraryInstance     structName structConstr fieldNames
            , GenSeqInstance        structName structConstr fieldNames
            , PerturbInstance       structName structConstr fieldNames
            , SameSemanticsInstance structName              fieldNames
            ]
      _otherwise -> []
    _otherwise -> []

getTypeTests :: Hs.Decl -> [TypeTest]
getTypeTests = \case
    Hs.DeclDefineInstance instanceDecl -> case instanceDecl of
      Hs.InstanceStorable Hs.Struct{..} _storableInstance ->
        [TypeTest structName]
      _otherwise -> []
    _otherwise -> []

getTestsFunNames :: Hs.Decl -> [HsName NsTypeConstr]
getTestsFunNames = \case
    Hs.DeclDefineInstance instanceDecl -> case instanceDecl of
      Hs.InstanceStorable Hs.Struct{..} _storableInstance -> [structName]
      _otherwise -> []
    _otherwise -> []

{-------------------------------------------------------------------------------
  AST
-------------------------------------------------------------------------------}

type Import      = (ModuleName, ImportAbbr)
type ImportAbbr  = String
type IncludeFile = String
type ModuleName  = String
type Pragma      = String

data HsTestModule = HsTestModule {
      hsTestModulePragmas         :: [Pragma]
    , hsTestModuleName            :: ModuleName
    , hsTestModuleImports         :: [Import]
    , hsTestModuleFfiFunctions    :: [FfiFunction]
    , hsTestModuleOrphanInstances :: [OrphanInstance]
    , hsTestModuleTypeTests       :: [TypeTest]
    , hsTestModuleTestsFun        :: TestsFun
    }

data FfiFunction =
    FfiSizeof   IncludeFile CFunPrefix (HsName NsTypeConstr)
  | FfiAlignof  IncludeFile CFunPrefix (HsName NsTypeConstr)
  | FfiGenSeqHs IncludeFile CFunPrefix (HsName NsTypeConstr)
  | FfiGenSeqC  IncludeFile CFunPrefix (HsName NsTypeConstr)
  | FfiPerturb  IncludeFile CFunPrefix (HsName NsTypeConstr)

data OrphanInstance =
    ArbitraryInstance     (HsName NsTypeConstr) (HsName NsConstr) [HsName NsVar]
  | GenSeqInstance        (HsName NsTypeConstr) (HsName NsConstr) [HsName NsVar]
  | PerturbInstance       (HsName NsTypeConstr) (HsName NsConstr) [HsName NsVar]
  | SameSemanticsInstance (HsName NsTypeConstr)                   [HsName NsVar]

newtype TypeTest = TypeTest (HsName NsTypeConstr)

data TestsFun = TestsFun ModuleName [HsName NsTypeConstr]

newtype HsSpecModule = HsSpecModule ModuleName

data HsMainModule = HsMainModule

{-------------------------------------------------------------------------------
  Pretty printing
-------------------------------------------------------------------------------}

instance Pretty HsTestModule where
  pretty HsTestModule{..} = vsep $
      vcat
        [ hsep ["{-#", string pragma, "#-}"]
        | pragma <- hsTestModulePragmas
        ]
    : hcat ["module ", string hsTestModuleName, " (tests) where"]
    : vcat
        [ hsep ["import qualified", string moduleName, "as", string importAbbr]
        | (moduleName, importAbbr) <- hsTestModuleImports
        ]
    : prettySection "FFI"
    : map pretty hsTestModuleFfiFunctions
    ++ prettySection "Instances"
    : map pretty hsTestModuleOrphanInstances
    ++ prettySection "Tests"
    : map pretty hsTestModuleTypeTests
    ++ [pretty hsTestModuleTestsFun]

instance Pretty FfiFunction where
  pretty = \case
    FfiSizeof   inc pfx name -> prettyFfiSizeof   inc pfx name
    FfiAlignof  inc pfx name -> prettyFfiAlignof  inc pfx name
    FfiGenSeqHs inc pfx name -> prettyFfiGenSeqHs inc pfx name
    FfiGenSeqC  inc pfx name -> prettyFfiGenSeqC  inc pfx name
    FfiPerturb  inc pfx name -> prettyFfiPerturb  inc pfx name

prettyFfiSizeof :: IncludeFile -> CFunPrefix -> HsName NsTypeConstr -> CtxDoc
prettyFfiSizeof inc pfx name =
    hang
      ( hcat
          [ "foreign import capi unsafe \"", string inc, " "
          , string pfx, "_sizeof_", prettyHsName name, "\""
          ]
      )
      2
      (hcat ["sizeof", prettyHsName name, " :: IO FC.CSize"])

prettyFfiAlignof :: IncludeFile -> CFunPrefix -> HsName NsTypeConstr -> CtxDoc
prettyFfiAlignof inc pfx name =
    hang
      ( hcat
          [ "foreign import capi unsafe \"", string inc, " "
          , string pfx, "_alignof_", prettyHsName name, "\""
          ]
      )
      2
      (hcat ["alignof", prettyHsName name, " :: IO FC.CSize"])

prettyFfiGenSeqHs :: IncludeFile -> CFunPrefix -> HsName NsTypeConstr -> CtxDoc
prettyFfiGenSeqHs inc pfx' name' = vsep
    [ hang
        ( hcat ["genSeqHs", name, " :: X.", name, " -> IO Bool"]
            $$ hcat ["genSeqHs", name, " x = F.alloca $ \\ptr -> do"]
        )
        4
        ( "F.poke ptr x"
            $$ hcat ["(/= 0) <$> ", pfx, "_genseqhs_", name, " ptr"]
        )
    , hang
        ( hcat
            [ "foreign import capi unsafe \"", string inc, " "
            , pfx, "_genseqhs_", name, "\""
            ]
        )
        2
        ( hcat
            [pfx, "_genseqhs_", name, " :: F.Ptr X.", name, " -> IO FC.CBool"]
        )
    ]
  where
    pfx, name :: CtxDoc
    pfx  = string pfx'
    name = prettyHsName name'

prettyFfiGenSeqC :: IncludeFile -> CFunPrefix -> HsName NsTypeConstr -> CtxDoc
prettyFfiGenSeqC inc pfx' name' = vsep
    [ hang
        ( hcat ["genSeqC", name, " :: IO X.", name]
            $$ hcat ["genSeqC", name, " x = F.alloca $ \\ptr -> do"]
        )
        4
        ( hcat [pfx, "_genseqc_", name, " ptr"]
            $$ "F.peek ptr"
        )
    , hang
        ( hcat
            [ "foreign import capi unsafe \"", string inc, " "
            , pfx, "_genseqc_", name, "\""
            ]
        )
        2
        (hcat [pfx, "_genseqc_", name, " :: F.Ptr X.", name, " -> IO ()"])
    ]
  where
    pfx, name :: CtxDoc
    pfx  = string pfx'
    name = prettyHsName name'

prettyFfiPerturb :: IncludeFile -> CFunPrefix -> HsName NsTypeConstr -> CtxDoc
prettyFfiPerturb inc pfx' name' = vsep
    [ hang
        ( hcat ["perturb", name, " :: FC.CLong -> X.", name, " -> IO X.", name]
            $$ hcat ["perturb", name, " size source ="]
        )
        4
        ( hang "F.alloca $ \\sourcePtr ->" 2
            . hang "F.alloca $ \\targetPtr -> do" 2
            $ vcat
                [ "F.poke sourcePtr source"
                , hcat [pfx, "_perturb_", name, " size sourcePtr targetPtr"]
                , "F.peek targetPtr"
                ]
        )
    , hang
        ( hcat
            [ "foreign import capi unsafe \"", string inc, " "
            , pfx, "_perturb_", name, "\""
            ]
        )
        2
        ( hcat
            [ pfx, "_perturb_", name, " :: FC.CLong -> F.Ptr X.", name
            , " -> F.Ptr X.", name, " -> IO ()"
            ]
        )
    ]
  where
    pfx, name :: CtxDoc
    pfx  = string pfx'
    name = prettyHsName name'

instance Pretty OrphanInstance where
  pretty = \case
    ArbitraryInstance typeName cnstName fieldNames ->
      prettyArbitraryInstance typeName cnstName fieldNames
    GenSeqInstance typeName cnstName fieldNames ->
      prettyGenSeqInstance typeName cnstName fieldNames
    PerturbInstance typeName cnstName fieldNames ->
      prettyPerturbInstance typeName cnstName fieldNames
    SameSemanticsInstance typeName fieldNames ->
      prettySameSemanticsInstance typeName fieldNames

prettyArbitraryInstance ::
     HsName NsTypeConstr
  -> HsName NsConstr
  -> [HsName NsVar]
  -> CtxDoc
prettyArbitraryInstance typeName' cnstName' fieldNames' =
    hangs
      (hcat ["instance QC.Arbitrary X.", typeName, " where"])
      2
      [ hang "arbitrary = do" 2 . vcat $
          [fieldName <+> "<- QC.arbitrary" | fieldName <- fieldNames]
            ++ [hcat ["pure X.", cnstName, "{..}"]]
      , hang (hcat ["shrink X.", cnstName, "{..} ="]) 2 $
          vlist '[' ']' shrinkList
      ]
  where
    typeName, cnstName :: CtxDoc
    typeName = prettyHsName typeName'
    cnstName = prettyHsName cnstName'

    fieldNames :: [CtxDoc]
    fieldNames = prettyHsName <$> fieldNames'

    shrinkList :: [CtxDoc]
    shrinkList =
      [ hsep $
            hcat ["X.", cnstName]
          : [ if n `Bits.testBit` i then fieldName else "RepZero.repZero"
            | (i, fieldName) <- zip [0..] fieldNames
            ]
      | n <- reverse @Word [0 .. 2 ^ length fieldNames - 2]
      ]

prettyGenSeqInstance ::
     HsName NsTypeConstr
  -> HsName NsConstr
  -> [HsName NsVar]
  -> CtxDoc
prettyGenSeqInstance typeName' cnstName' fieldNames' =
    hang
      (hcat ["instance GenSeq.GenSeq X.", typeName, " where"])
      2
      ( hang "genSeq n =" 2 . vcat $
             [ if i == 0
                 then hcat ["let ", fieldName, " = GenSeq.genSeq n"]
                 else nest 4 $ hcat
                        [ fieldName, " = GenSeq.genSeq (n + "
                        , showToCtxDoc @Word i, ")"
                        ]
             | (i, fieldName) <- zip [0..] fieldNames
             ]
          ++ [hcat ["in  X.", cnstName, "{..}"]]
      )
  where
    typeName, cnstName :: CtxDoc
    typeName = prettyHsName typeName'
    cnstName = prettyHsName cnstName'

    fieldNames :: [CtxDoc]
    fieldNames = prettyHsName <$> fieldNames'

prettyPerturbInstance ::
     HsName NsTypeConstr
  -> HsName NsConstr
  -> [HsName NsVar]
  -> CtxDoc
prettyPerturbInstance typeName' cnstName' fieldNames' =
    hang
      (hcat ["instance Perturb.Perturb X.", typeName, " where"])
      2
      ( hang
          (hcat ["perturb size X.", cnstName, "{..} = X.", cnstName])
          2
          ( vlist '{' '}'
              [ hcat [fieldName, " = Perturb.perturb size ", fieldName]
              | fieldName <- fieldNames
              ]
          )
      )
  where
    typeName, cnstName :: CtxDoc
    typeName = prettyHsName typeName'
    cnstName = prettyHsName cnstName'

    fieldNames :: [CtxDoc]
    fieldNames = prettyHsName <$> fieldNames'

prettySameSemanticsInstance ::
     HsName NsTypeConstr
  -> [HsName NsVar]
  -> CtxDoc
prettySameSemanticsInstance typeName' fieldNames' =
    hang
      (hcat ["instance SameSemantics.SameSemantics X.", typeName, " where"])
      2
      ( hang "sameSemantics l r = and" 2 $ vlist '[' ']'
          [ hcat ["SameSemantics.sameSemanticsOn X.", fieldName, " l r"]
          | fieldName <- fieldNames
          ]
      )
  where
    typeName :: CtxDoc
    typeName = prettyHsName typeName'

    fieldNames :: [CtxDoc]
    fieldNames = prettyHsName <$> fieldNames'

instance Pretty TypeTest where
  pretty (TypeTest name) = prettyTypeTest name

prettyTypeTest :: HsName NsTypeConstr -> CtxDoc
prettyTypeTest name' =
    hang
      ( hcat ["test", name, " :: Tasty.TestTree"]
          $$ hcat ["test", name, " = Tasty.testGroup \"", name, "\""]
      )
      2
      ( vlist '[' ']'
          [ hang "THU.testCase Storable.nameHsSizeOfXEqCSizeOfX $" 2 $
              hcat
                [ "Storable.assertHsSizeOfXEqCSizeOfX @X.", name
                , " Proxy.Proxy sizeof", name
                ]
          , hang "THU.testCase Storable.nameHsAlignOfXEqCAlignOfX $" 2 $
              hcat
                [ "Storable.assertHsAlignOfXEqCAlignOfX @X.", name
                , " Proxy.Proxy alignof", name
                ]
          , hang "THU.testProperty Storable.namePokePeekXSameSemanticsX $" 2 $
              hcat ["Storable.prop_PokePeekXSameSemanticsX @X.", name]
          , hang "THU.testCase GenSeq.nameHsGenSeq1CEq $" 2 $
              hcat ["GenSeq.assertHsGenSeq1CEq genSeqHs", name]
          , hang "THU.testCase GenSeq.nameHsGenSeq1SameSemanticsCGenSeq1 $" 2 $
              hcat ["GenSeq.assertHsGenSeq1SameSemanticsCGenSeq1 genSeqC", name]
          , hang "TQC.testProperty Perturb.nameHsPerturbNXSameSemanticsCPerturbNX $" 2 $
              hcat
                [ "Perturb.prop_HsPerturbNXSameSemanticsCPerturbNX perturb"
                , name
                ]
          ]
      )
  where
    name :: CtxDoc
    name = prettyHsName name'

instance Pretty TestsFun where
  pretty (TestsFun moduleName typeNames) = prettyTestsFun moduleName typeNames

prettyTestsFun :: ModuleName -> [HsName NsTypeConstr] -> CtxDoc
prettyTestsFun moduleName typeNames =
    hang
      ( "tests :: Tasty.TestTree"
          $$ hcat ["tests = Tasty.testGroup \"", string moduleName, "\""]
      )
      4
      ( vlist '[' ']'
          [ "test" >< prettyHsName typeName
          | typeName <- typeNames
          ]
      )

instance Pretty HsSpecModule where
  pretty (HsSpecModule moduleName) = vsep
    [ "module Spec (tests) where"
    , "import Test.Tasty (TestTree, testGroup)"
    , "import qualified" <+> string moduleName
    , vcat
        [ "tests :: TestTree"
        , "tests = testGroup \"test-hs-bindgen\""
        , nest 4 $ vlist '[' ']' [string moduleName >< ".tests"]
        ]
    ]

instance Pretty HsMainModule where
  pretty HsMainModule = vsep
    [ "module Main (main) where"
    , "import Test.Tasty (defaultMain)"
    , "import qualified Spec"
    , vcat
        [ "main :: IO ()"
        , "main = defaultMain Spec.tests"
        ]
    ]

{-------------------------------------------------------------------------------
  Auxiliary functions
-------------------------------------------------------------------------------}

prettySection :: String -> CtxDoc
prettySection label = renderedLines $ \w ->
    let rule = List.replicate (w - 1) '-'
    in  ['{' : rule, "  " ++ label, rule ++ "}"]
-}