packages feed

clash-lib-1.4.0: src/Clash/Edalize/Edam.hs

{-|
Copyright       : (C) 2020, QBayLogic
License         : BSD2 (see the file LICENSE)
Maintainer      : QBayLogic B.V. <devops@qbaylogic.com>

Data types and rendering for Edalize Metadata files (EDAM).
-}

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}

module Clash.Edalize.Edam
  ( Edam(..)
  , EdamFile(..)
  , EdamFileType(..)
  , EdamTools(..)
  , GhdlOptions(..)
  , IcarusOptions(..)
  , ModelsimOptions(..)
  , QuartusOptions(..)
  , VivadoOptions(..)

  , pprEdam
  ) where

import Data.Default
import Data.Maybe
import Data.Text (Text)
import Data.Text.Prettyprint.Doc

-- | EDAM data structure to be given to an Edalize backend. This contains all
-- information needed to generate a project scaffolding. Note that hooks and
-- VPI modules are currently not specified by clash.
--
data Edam = Edam
  { edamProjectName :: Text
  , edamTopEntity   :: Text
  , edamFiles       :: [EdamFile]
  , edamToolOptions :: EdamTools
  }

pprEdam :: Edam -> Doc ann
pprEdam (Edam n te fs ts) = vsep
  [ pyPre
  , hsep ["edam", equals, manifest]
  , pyPost
  ]
 where
  manifest = pyRecord
    [ pyField "name" $ squotes (pretty n)
    , pyField "toplevel" $ squotes (pretty te)
    , pyField "files" $ pyList (fmap pprFile fs)
    , pyField "tool_options" $ pprEdamTools ts
    ]

-- | Information about each file in the project. This does not include
-- is_include_file or include_path, as these are not currently used by Clash.
--
data EdamFile = EdamFile
  { efName        :: FilePath
  , efType        :: EdamFileType
  , efLogicalName :: Text
  }

pprFile :: EdamFile -> Doc ann
pprFile (EdamFile n ty ln) =
  pyRecord
    [ pyField "name" $ joinPath n
    , pyField "file_type" $ squotes (pprFileType ty)
    , pyField "logical_name" $ squotes (pretty ln)
    ]

-- | A subset of the file types recognized by Edalize. The supported formats
-- are largely from IP-XACT 2014 (IEEE 1685-2014), although Edalize extends
-- this with other types, e.g. QSYS.
--
-- Only file types which are generated by Clash are listed.
--
data EdamFileType
  = Unknown
    -- ^ Unknown file type.
  | VhdlSource
    -- ^ VHDL source.
  | VerilogSource
    -- ^ Verilog source.
  | SystemVerilogSource
    -- ^ SystemVerilog source.
  | TclSource
    -- ^ Tool Command Language source.
  | QSYS
    -- ^ QSys system source.
  | SDC
    -- ^ Synopsys Design Constraints source.
  deriving (Eq, Show)

pprFileType :: EdamFileType -> Doc ann
pprFileType = \case
  Unknown -> "unknown"
  VhdlSource -> "vhdlSource"
  VerilogSource -> "verilogSource"
  SystemVerilogSource -> "systemVerilogSource"
  TclSource -> "tclSource"
  QSYS -> "QSYS"
  SDC -> "SDC"

-- | Tool-specific configuration used by Edalize.
-- Currently only tools which are supported by Clash are provided.
--
data EdamTools = EdamTools
  { etGhdl      :: Maybe GhdlOptions
  , etIcarus    :: Maybe IcarusOptions
  , etModelsim  :: Maybe ModelsimOptions
  , etQuartus   :: Maybe QuartusOptions
  , etVivado    :: Maybe VivadoOptions
  }

instance Default EdamTools where
  def = EdamTools def def def def def

pprEdamTools :: EdamTools -> Doc ann
pprEdamTools tools =
  pyRecord
    [ pyField "ghdl" $ pprGhdlOptions ghdl
    , pyField "icarus" $ pprIcarusOptions icarus
    , pyField "modelsim" $ pprModelsimOptions modelsim
    , pyDocField
        "quartus"
        "TODO Specify options if using Quartus"
        (pprQuartusOptions quartus)

    , pyDocField
        "vivado"
        "TODO Specify options if using Vivado"
        (pprVivadoOptions vivado)
    ]
 where
  ghdl     = fromMaybe def (etGhdl tools)
  icarus   = fromMaybe def (etIcarus tools)
  modelsim = fromMaybe def (etModelsim tools)
  quartus  = fromMaybe def (etQuartus tools)
  vivado   = fromMaybe def (etVivado tools)

data GhdlOptions = GhdlOptions
  { ghdlAnalyseOpts :: [Text]
  , ghdlRunOpts     :: [Text]
  }

instance Default GhdlOptions where
  def = GhdlOptions [] []

pprGhdlOptions :: GhdlOptions -> Doc ann
pprGhdlOptions (GhdlOptions aOpts rOpts) =
  pyRecord
    [ pyDocField
        "analyze_options"
        "Command line arguments for analysis"
        (flagList (fmap pretty aOpts))

    , pyDocField
        "run_options"
        "Command line arguments for simulation"
        (flagList (fmap pretty rOpts))
    ]

data IcarusOptions = IcarusOptions
  { icarusOpts      :: [Text]
  , icarusTimeScale :: Text
  }

instance Default IcarusOptions where
  def = IcarusOptions [] "100fs/100fs"

pprIcarusOptions :: IcarusOptions -> Doc ann
pprIcarusOptions (IcarusOptions opts ts) =
  pyRecord
    [ pyDocField
        "iverilog_options"
        "Command line options for iverilog"
        (flagList (fmap pretty opts))

    , pyDocField
        "timescale"
        "Default timescale for simulation"
        (squotes (pretty ts))
    ]

data ModelsimOptions = ModelsimOptions
  { msVlogOpts :: [Text]
  , msVsimOpts :: [Text]
  }

instance Default ModelsimOptions where
  def = ModelsimOptions [] []

pprModelsimOptions :: ModelsimOptions -> Doc ann
pprModelsimOptions (ModelsimOptions vlog vsim) =
  pyRecord
    [ pyDocField
        "vlog_options"
        "Command line arguments for vlog"
        (flagList (fmap pretty vlog))

    , pyDocField
        "vsim_options"
        "Command line arguments for vsim"
        (flagList (fmap pretty vsim))
    ]

data QuartusOptions = QuartusOptions
  { quartusBoardDevIndex :: Int
  , quartusFamily        :: Text
  , quartusDevice        :: Text
  , quartusOpts          :: [Text]
  , quartusDseOpts       :: [Text]
  }

instance Default QuartusOptions where
  def = QuartusOptions 1 "" "" [] []

pprQuartusOptions :: QuartusOptions -> Doc ann
pprQuartusOptions (QuartusOptions bdi fam dev opts dse) =
  pyRecord
    [ pyDocField
        "board_device_index"
        "Specify the FPGA's device number in the JTAG chain"
        (squotes (pretty bdi))

    , pyDocField
        "family"
        "FPGA family, e.g. Cyclone IV E"
        (squotes (pretty fam))

    , pyDocField
        "device"
        "Device identifier, e.g. EP4CE55F23C8"
        (squotes (pretty dev))

    , pyDocField
        "quartus_options"
        "Command line arguments for Quartus"
        (flagList (fmap pretty opts))

    , pyDocField
        "dse_options"
        "Command line arguments for Design Space Explorer"
        (flagList (fmap pretty dse))
    ]

data VivadoOptions = VivadoOptions
  { vivadoPart :: Text
  }

instance Default VivadoOptions where
  def = VivadoOptions ""

pprVivadoOptions :: VivadoOptions -> Doc ann
pprVivadoOptions (VivadoOptions part) =
  pyRecord
    [ pyDocField
        "part"
        "Specify target part by ID, e.g. xc7z020-1clg400c"
        (squotes (pretty part))
    ]

-- Helpers; don't export

pyPre :: Doc ann
pyPre = vsep
  [ "import os"
  , ""
  , "edam_root = os.path.dirname(os.path.realpath(__file__))"
  , "work_root = 'build'"
  , ""
  , "# TODO Specify the EDA tool to use"
  , "tool = ''"
  , ""
  ]

pyPost :: Doc ann
pyPost = vsep
  [ ""
  , "if __name__ == '__main__':"
  , indent 4 $ vsep
      [ "from edalize import *"
      , ""
      , "tool = get_edatool(tool)(edam=edam, work_root=work_root)"
      , "os.makedirs(work_root)"
      , "tool.configure()"
      , "tool.build()"
      ]
  ]

pyList :: [Doc ann] -> Doc ann
pyList xs = vsep [lbracket, indent 4 (commaList xs), rbracket]

pyRecord :: [Doc ann] -> Doc ann
pyRecord xs = vsep [lbrace, indent 4 (commaList xs), rbrace]

pyComment :: Text -> Doc ann
pyComment x = hsep ["#", pretty x]

pyField :: Text -> Doc ann -> Doc ann
pyField n x = hcat [squotes (pretty n), colon, space, x]

pyDocField :: Text -> Text -> Doc ann -> Doc ann
pyDocField n d x = vsep [pyComment d, pyField n x]

joinPath :: FilePath -> Doc ann
joinPath x = hcat
  [ "os.path.join"
  , parens (hsep $ punctuate comma ["edam_root", squotes (pretty x)])
  ]

flagList :: [Doc ann] -> Doc ann
flagList = squotes . hsep

commaList :: [Doc ann] -> Doc ann
commaList = vsep . punctuate comma