packages feed

qtah-generator-0.1.0: src/Graphics/UI/Qtah/Generator/Module.hs

-- This file is part of Qtah.
--
-- Copyright 2015-2016 Bryan Gardiner <bog@khumba.net>
--
-- This program is free software: you can redistribute it and/or modify
-- it under the terms of the GNU Lesser General Public License as published by
-- the Free Software Foundation, either version 3 of the License, or
-- (at your option) any later version.
--
-- This program is distributed in the hope that it will be useful,
-- but WITHOUT ANY WARRANTY; without even the implied warranty of
-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
-- GNU Lesser General Public License for more details.
--
-- You should have received a copy of the GNU Lesser General Public License
-- along with this program.  If not, see <http://www.gnu.org/licenses/>.

{-# LANGUAGE CPP #-}

module Graphics.UI.Qtah.Generator.Module (
  generateModule,
  ) where

import Control.Monad (forM_, unless, when)
#if MIN_VERSION_mtl(2,2,1)
import Control.Monad.Except (throwError)
#else
import Control.Monad.Error (throwError)
#endif
import Data.List (find, intercalate, intersperse, isPrefixOf, sort)
import Data.Maybe (isJust)
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (mconcat)
#endif
import Foreign.Hoppy.Generator.Language.Cpp (execChunkWriter, sayType)
import Foreign.Hoppy.Generator.Language.Haskell (
  Generator,
  HsTypeSide (HsHsSide),
  addExport,
  addExports,
  addImports,
  cppTypeToHsTypeAndUse,
  execGenerator,
  getClassHaskellConversion,
  importHsModuleForExtName,
  indent,
  inFunction,
  ln,
  prettyPrint,
  renderPartial,
  sayLn,
  saysLn,
  toHsBitspaceClassName,
  toHsBitspaceToNumName,
  toHsBitspaceTypeName,
  toHsBitspaceValueName,
  toHsCastMethodName,
  toHsDataTypeName,
  toHsDownCastMethodName,
  toHsEnumTypeName,
  toHsFnName,
  toHsPtrClassName,
  toHsValueClassName,
  )
import Foreign.Hoppy.Generator.Spec (
  Class,
  Constness (Const, Nonconst),
  Ctor,
  Export (ExportBitspace, ExportCallback, ExportClass, ExportEnum, ExportFn, ExportVariable),
  ExtName,
  FnName (FnName),
  Function,
  Interface,
  Method,
  MethodImpl (RealMethod),
  Type (Internal_TCallback),
  bitspaceExtName,
  bitspaceValueNames,
  callbackParams,
  classCtors,
  classExtName,
  classMethods,
  ctorExtName,
  ctorParams,
  enumExtName,
  fnExtName,
  fromExtName,
  getClassyExtName,
  hsImport1,
  hsImports,
  methodExtName,
  methodImpl,
  toExtName,
  varExtName,
  varGetterExtName,
  varIsConst,
  varSetterExtName,
  )
import Foreign.Hoppy.Generator.Types (objT)
import Graphics.UI.Qtah.Generator.Flags (qrealFloat)
import Graphics.UI.Qtah.Generator.Common (fromMaybeM, writeFileIfDifferent)
import Graphics.UI.Qtah.Generator.Types (
  QtExport (QtExport, QtExportEvent, QtExportFnRenamed, QtExportSignal, QtExportSpecials),
  QtModule,
  Signal,
  moduleNameAppend,
  qtModulePath,
  qtModuleQtExports,
  signalClass,
  signalCName,
  signalHaskellName,
  signalListenerClass,
  )
import Graphics.UI.Qtah.Generator.Interface.Imports
import Language.Haskell.Syntax (
  HsName (HsIdent),
  HsQName (UnQual),
  HsQualType (HsQualType),
  HsType (HsTyApp, HsTyCon, HsTyFun, HsTyVar),
  )
import System.Directory (createDirectoryIfMissing)
import System.Exit (exitFailure)
import System.FilePath ((</>), (<.>), pathSeparator, takeDirectory)

generateModule :: Interface -> FilePath -> String -> QtModule -> IO ()
generateModule iface srcDir baseModuleName qtModule = do
  let fullModuleName = moduleNameAppend baseModuleName $ intercalate "." $ qtModulePath qtModule
      qtExports = qtModuleQtExports qtModule

  let generation =
        fmap (("{-# LANGUAGE NoMonomorphismRestriction #-}\n\n" ++) . renderPartial) $
        execGenerator iface fullModuleName $ do
          -- As in generated Hoppy bindings, avoid non-qualified Prelude uses in
          -- generated code here.
          addImports $ hsImports "Prelude" []

          -- Generate bindings for all of the exports.
          mapM_ (sayQtExport $ qtModulePath qtModule) qtExports

  case generation of
    Left errorMsg -> do
      putStrLn $ "Error generating Qt modules: " ++ errorMsg
      exitFailure
    Right source -> do
      let path =
            srcDir </>
            map (\c -> if c == '.' then pathSeparator else c) fullModuleName <.>
            "hs"
      createDirectoryIfMissing True $ takeDirectory path
      writeFileIfDifferent path source

getFnReexportName :: Function -> String
getFnReexportName = getFnImportName

classUpCastReexportName :: String
classUpCastReexportName = "cast"

classUpCastConstReexportName :: String
classUpCastConstReexportName = "castConst"

classDownCastReexportName :: String
classDownCastReexportName = "downCast"

classDownCastConstReexportName :: String
classDownCastConstReexportName = "downCastConst"

classEncodeReexportName :: String
classEncodeReexportName = "encode"

classDecodeReexportName :: String
classDecodeReexportName = "decode"

getCtorReexportName :: Class -> Ctor -> String
getCtorReexportName cls = toBindingNameWithoutClassPrefix cls . ctorExtName

getMethodReexportName :: Class -> Method -> String
getMethodReexportName cls = toBindingNameWithoutClassPrefix cls . methodExtName

-- | Qtah uses @ClassName_innerName@ 'ExtName's for things in classes.  This
-- function strips the @ClassName_@ prefix off of an 'ExtName', if present, and
-- converts it to a function name.
toBindingNameWithoutClassPrefix :: Class -> ExtName -> String
toBindingNameWithoutClassPrefix cls name =
  toHsFnName $ toExtName $
  dropPrefix (fromExtName (classExtName cls) ++ "_") $
  fromExtName name

-- | @dropPrefix prefix str@ strips @prefix@ from @str@ if @str@ starts with
-- @prefix@, and returns @str@ unmodified otherwise.
dropPrefix :: String -> String -> String
dropPrefix prefix str =
  if prefix `isPrefixOf` str
  then drop (length prefix) str
  else str

getFnImportName :: Function -> String
getFnImportName = toHsFnName . fnExtName

sayClassEncodingFnReexports :: Class -> Generator ()
sayClassEncodingFnReexports cls = inFunction "sayClassEncodingFnReexports" $
  when (classIsConvertible cls) $ do
    -- Generated encode and decode functions require some things from Hoppy
    -- support and the Prelude.
    addImports $ mconcat [importForPrelude, importForRuntime]

    hsHsType <- cppTypeToHsTypeAndUse HsHsSide (objT cls)
    let constPtrClassName = toHsPtrClassName Const cls
        dataTypeName = toHsDataTypeName Nonconst cls
        ptrHsType = HsTyCon $ UnQual $ HsIdent dataTypeName
        thisTyVar = HsTyVar $ HsIdent "this"
        encodeFnType = HsTyFun hsHsType $ HsTyApp (HsTyCon $ UnQual $ HsIdent "QtahP.IO") ptrHsType
        decodeFnType = HsQualType [(UnQual $ HsIdent constPtrClassName, [thisTyVar])] $
                       HsTyFun thisTyVar $
                       HsTyApp (HsTyCon $ UnQual $ HsIdent "QtahP.IO") hsHsType
    ln
    saysLn [classEncodeReexportName, " :: ", prettyPrint encodeFnType]
    saysLn [classEncodeReexportName, " = QtahFHR.encodeAs (QtahP.undefined :: ", dataTypeName, ")"]
    ln
    saysLn [classDecodeReexportName, " :: ", prettyPrint decodeFnType]
    saysLn [classDecodeReexportName, " = QtahFHR.decode QtahP.. ", toHsCastMethodName Const cls]

sayQtExport :: [String] -> QtExport -> Generator ()
sayQtExport path qtExport = case qtExport of
  QtExport (ExportVariable v) -> do
    importHsModuleForExtName $ varExtName v
    addExport $ toHsFnName $ varGetterExtName v
    unless (varIsConst v) $ addExport $ toHsFnName $ varSetterExtName v

  QtExport (ExportEnum e) -> do
    importHsModuleForExtName $ enumExtName e
    let spec = toHsEnumTypeName e ++ " (..)"
    addExport spec

  QtExport (ExportBitspace b) -> do
    importHsModuleForExtName $ bitspaceExtName b
    addExport $ toHsBitspaceTypeName b
    addExport $ toHsBitspaceToNumName b
    addExport $ toHsBitspaceClassName b ++ " (..)"
    forM_ (bitspaceValueNames b) $ \(_, valueName) ->
      addExport $ toHsBitspaceValueName b valueName

  QtExport (ExportFn fn) -> do
    importHsModuleForExtName $ fnExtName fn
    addExport $ getFnReexportName fn

  QtExportFnRenamed fn rename -> do
    importHsModuleForExtName $ fnExtName fn
    addExport rename
    sayBind rename $ getFnImportName fn

  QtExport (ExportCallback _) -> return ()

  QtExport (ExportClass cls) -> sayExportClass cls

  QtExportSignal sig -> sayExportSignal sig

  QtExportEvent cls -> do
    sayExportClass cls

    let typeName = toHsDataTypeName Nonconst cls
    addImports $ mconcat [hsImport1 "Prelude" "($)",
                          importForEvent]
    ln
    saysLn ["instance QtahEvent.Event ", typeName, " where"]
    indent $ do
      sayLn "onEvent receiver' handler' = QtahEvent.onAnyEvent receiver' $ \\_ qevent' ->"
      indent $
        if path == ["Core", "QEvent"]
        then sayLn "handler' qevent'"
        else do
          addImports $ mconcat [hsImport1 "Prelude" "(==)",
                                importForPrelude,
                                importForRuntime]
          saysLn ["let event' = ", classDownCastReexportName, " qevent'"]
          sayLn "in if event' == QtahFHR.nullptr then QtahP.return QtahP.False else handler' event'"

  QtExportSpecials -> do
    -- Generate a type synonym for qreal.
    addImports importForPrelude
    addExport "QReal"
    ln
    saysLn ["type QReal = ", if qrealFloat then "QtahP.Float" else "QtahP.Double"]

sayExportClass :: Class -> Generator ()
sayExportClass cls = do
  importHsModuleForExtName $ classExtName cls
  addExports $
    (toHsValueClassName cls ++ " (..)") :
    (toHsPtrClassName Const cls ++ " (..)") :
    (toHsPtrClassName Nonconst cls ++ " (..)") :
    toHsDataTypeName Const cls :
    toHsDataTypeName Nonconst cls :
    classUpCastConstReexportName :
    classUpCastReexportName :
    classDownCastConstReexportName :
    classDownCastReexportName :
    concat [ if classIsConvertible cls
             then [classEncodeReexportName, classDecodeReexportName]
             else []
           , sort $ map (getCtorReexportName cls) $ classCtors cls
           , sort $ map (getMethodReexportName cls) $ classMethods cls
           ]

  ln
  sayBind classUpCastConstReexportName $ toHsCastMethodName Const cls
  sayBind classUpCastReexportName $ toHsCastMethodName Nonconst cls
  sayBind classDownCastConstReexportName $ toHsDownCastMethodName Const cls
  sayBind classDownCastReexportName $ toHsDownCastMethodName Nonconst cls
  sayClassEncodingFnReexports cls
  forM_ (classCtors cls) $ \ctor ->
    sayBind (getCtorReexportName cls ctor) $ toHsFnName $ getClassyExtName cls ctor
  forM_ (classMethods cls) $ \method ->
    sayBind (getMethodReexportName cls method) $ toHsFnName $ getClassyExtName cls method

-- | Generates and exports a @Signal@ definition.  We create the signal from
-- scratch in this module, rather than reexporting it from somewhere else.
sayExportSignal :: Signal -> Generator ()
sayExportSignal signal = inFunction "sayExportSignal" $ do
  addImports importForSignal

  let name = signalCName signal
      cls = signalClass signal
      ptrClassName = toHsPtrClassName Nonconst cls
      varName = toSignalBindingName signal
  addExport varName

  let listenerClass = signalListenerClass signal
  importHsModuleForExtName $ classExtName listenerClass
  -- Find the listener constructor that only takes a callback.
  listenerCtor <-
    fromMaybeM (throwError $ concat
                ["Couldn't find an appropriate ",
                show (fromExtName $ classExtName listenerClass),
                " constructor for signal ", show name]) $
    flip find (classCtors listenerClass) $ \ctor -> case ctorParams ctor of
      [Internal_TCallback {}] -> True
      _ -> False
  let [callbackType@(Internal_TCallback callback)] = ctorParams listenerCtor
      paramTypes = callbackParams callback

  -- Also find the 'connectListener' method.
  listenerConnectMethod <-
    fromMaybeM (throwError $ concat
                ["Couldn't find the connectListener method in ",
                 show listenerClass, " for signal ", show name]) $
    find ((RealMethod (FnName "connectListener") ==) . methodImpl) $ classMethods listenerClass

  callbackHsType <- cppTypeToHsTypeAndUse HsHsSide callbackType

  let varType = HsQualType [(UnQual $ HsIdent ptrClassName, [HsTyVar $ HsIdent "object"])] $
                HsTyApp (HsTyApp (HsTyCon $ UnQual $ HsIdent "QtahSignal.Signal") $
                         HsTyVar $ HsIdent "object")
                callbackHsType
      internalName = concat
                     [ fromExtName $ classExtName cls
                     , "::"
                     , name
                     , " ("
                     , fromExtName $ classExtName listenerClass
                     , ")"
                     ]
  ln
  saysLn [varName, " :: ", prettyPrint varType]
  saysLn [varName, " = QtahSignal.Signal"]
  indent $ do
    sayLn "{ QtahSignal.internalConnectSignal = \\object' fn' -> do"
    indent $ do
      saysLn ["listener' <- ", toHsFnName $ getClassyExtName listenerClass listenerCtor, " fn'"]
      saysLn [toHsFnName $ getClassyExtName listenerClass listenerConnectMethod,
              " listener' object' ", show (toSignalConnectName signal paramTypes)]
    saysLn [", QtahSignal.internalName = ", show internalName]
    sayLn "}"

sayBind :: String -> String -> Generator ()
sayBind name value = saysLn [name, " = ", value]

toSignalBindingName :: Signal -> String
toSignalBindingName = (++ "Signal") . signalHaskellName

toSignalConnectName :: Signal -> [Type] -> String
toSignalConnectName signal paramTypes =
  concat $
  "2" :  -- This is a magic code added by the SIGNAL() macro.
  signalCName signal :
  "(" :
  intersperse "," (map (execChunkWriter . sayType Nothing) paramTypes) ++
  [")"]

-- | Returns true iff a given class is convertible to/from a Haskell
-- type with @encode@ and @decode@.
classIsConvertible :: Class -> Bool
classIsConvertible = isJust . getClassHaskellConversion