packages feed

hs-bindgen-1.0.0.0: app/HsBindgen/App/Output.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE RecordWildCards #-}

-- | Output mode and category selection options for @hs-bindgen@
module HsBindgen.App.Output (
    -- * Output mode
    OutputMode(..)
  , parseOutputMode
    -- * Single-file category selection
  , SingleFileCategory(..)
  , parseSingleFileCategories
    -- * Output options
  , OutputOptions(..)
  , parseOutputOptions
    -- * Builder
  , buildCategoryChoice
  ) where

import Data.Default (Default (..))
import Data.List.NonEmpty (NonEmpty (..))
import Data.Text (Text)
import GHC.Generics (Generic)
import Options.Applicative
import Options.Applicative.NonEmpty (some1)

import HsBindgen.Backend.Category (ByCategory (..), Choice (..),
                                   RenameTerm (..))
import HsBindgen.Backend.Level (Level (..))

{-------------------------------------------------------------------------------
  Output mode
-------------------------------------------------------------------------------}

-- | Output mode (single-file vs multi-module)
data OutputMode =
    FilePerModule
      -- ^ One file per category (default)
  | SingleFile (NonEmpty SingleFileCategory)
      -- ^ Single file combining categories
  deriving (Show, Eq)

-- | Parse output mode
parseOutputMode :: OutputMode -> Parser OutputMode
parseOutputMode defMode =
  asum [
    (flag' FilePerModule $ mconcat [
        long "file-per-module"
      , help "Generate one file per binding category (default)"
      ])
  , (flag' SingleFile $ mconcat [
        long "single-file"
      , help (  "Generate a single module file. If more than one category selected "
             ++ "they will be combined. If more than one of the same category is "
             ++ "selected only the last suffix is used."
             )
      ]) <*> parseSingleFileCategories
  , pure defMode
  ]

{-------------------------------------------------------------------------------
  Single-file category selection
-------------------------------------------------------------------------------}

-- | Which category to include in single-file mode (required when mode=SingleFile)
data SingleFileCategory =
    SingleFileSafe Text
      -- ^ Safe only, with optional suffix (empty = none)
  | SingleFileUnsafe Text
      -- ^ Unsafe only, with optional suffix
  | SingleFilePointer Text
      -- ^ Pointer only, with optional suffix
  deriving (Show, Eq)

-- | Parse single-file category selections (one or more required)
parseSingleFileCategories :: Parser (NonEmpty SingleFileCategory)
parseSingleFileCategories = some1 parseSingleFileCategory
  where
    parseSingleFileCategory :: Parser SingleFileCategory
    parseSingleFileCategory =
          (SingleFileSafe <$> strOption (mconcat [
                long "safe"
              , metavar "SUFFIX"
              , help "Include safe bindings (empty suffix for no suffix)"
              ]))
      <|> (SingleFileUnsafe <$> strOption (mconcat [
                long "unsafe"
              , metavar "SUFFIX"
              , help "Include unsafe bindings (empty suffix for no suffix)"
              ]))
      <|> (SingleFilePointer <$> strOption (mconcat [
                long "pointer"
              , metavar "SUFFIX"
              , help "Include pointer bindings (empty suffix for no suffix)"
              ]))

{-------------------------------------------------------------------------------
  Output options
-------------------------------------------------------------------------------}

-- | Output options
newtype OutputOptions = OutputOptions { mode :: OutputMode }
  deriving (Show, Eq, Generic)

-- | Parse output options
parseOutputOptions :: OutputMode -> Parser OutputOptions
parseOutputOptions defMode = OutputOptions <$> parseOutputMode defMode

{-------------------------------------------------------------------------------
  Builder
-------------------------------------------------------------------------------}

-- | Build 'ByCategory' 'Choice' from output options
--
-- When multiple categories are selected, each category gets the specified suffix
-- to avoid duplicate symbols. When only one category is selected with an empty
-- suffix, no renaming occurs.
--
buildCategoryChoice :: OutputOptions -> ByCategory Choice
buildCategoryChoice opts = case opts.mode of
  FilePerModule       -> useAllCategories
  SingleFile cats     -> buildSingleFileChoice cats
  where
    -- Multi-module: all categories, no renaming
    useAllCategories :: ByCategory Choice
    useAllCategories = ByCategory {
          cType   = IncludeTypeCategory
        , cSafe   = IncludeTermCategory def
        , cUnsafe = IncludeTermCategory def
        , cFunPtr = IncludeTermCategory def
        , cGlobal = IncludeTermCategory def
        }

    -- Single-file: build based on category selections
    buildSingleFileChoice :: NonEmpty SingleFileCategory -> ByCategory Choice
    buildSingleFileChoice cats = ByCategory {
          cType   = IncludeTypeCategory
        , cSafe   = foldMap safeChoice cats
        , cUnsafe = foldMap unsafeChoice cats
        , cFunPtr = foldMap pointerChoice cats
        , cGlobal = IncludeTermCategory def
        }

    safeChoice :: SingleFileCategory -> Choice LvlTerm
    safeChoice = \case
      SingleFileSafe s    -> IncludeTermCategory (rename s)
      _                   -> ExcludeCategory

    unsafeChoice :: SingleFileCategory -> Choice LvlTerm
    unsafeChoice = \case
      SingleFileUnsafe s  -> IncludeTermCategory (rename s)
      _                   -> ExcludeCategory

    pointerChoice :: SingleFileCategory -> Choice LvlTerm
    pointerChoice = \case
      SingleFilePointer s -> IncludeTermCategory (rename s)
      _                   -> ExcludeCategory

    rename :: Text -> RenameTerm
    rename "" = def  -- Empty suffix = no renaming
    rename s  = RenameTerm (<> s)