packages feed

ghc-wasm-bindgen-0.1.0.0: src/GhcWasmBindgen/Inspect.hs

-- | Reads the export module with the GHC API and builds the
-- 'GhcWasmBindgen.Model.Api' model.
--
-- This is the only module that imports GHC; a GHC upgrade should only touch
-- this file.
module GhcWasmBindgen.Inspect
  ( InspectOptions (..)
  , Source (..)
  , inspect
  ) where

import Control.Monad (filterM, forM, forM_, unless, when)
import Control.Monad.IO.Class (liftIO)
import Data.IORef
import Data.List (isPrefixOf, nub, sortOn)
import qualified Data.Map.Strict as Map
import Data.Maybe (catMaybes, listToMaybe)
import qualified Data.Set as Set
import qualified Data.Text as T
import System.Directory (makeAbsolute)
import System.FilePath (joinPath, splitDirectories, takeDirectory)

import GHC
import GHC.Builtin.Types (boolTyCon, charTyCon, doubleTyCon, floatTyCon, intTyCon, integerTyCon, listTyCon, maybeTyCon, naturalTyCon, wordTyCon)
import GHC.Core.DataCon (dataConOrigArgTys)
import GHC.Core.InstEnv (ClsInst (..))
import GHC.Core.Multiplicity (scaledThing)
import GHC.Core.TyCon (isAlgTyCon, isBoxedTupleTyCon, tyConName)
import GHC.Core.Type (expandTypeSynonyms, mkTyConTy, splitFunTy_maybe, splitFunTys, splitTyConApp_maybe)
import GHC.Data.FastString (unpackFS)
import GHC.Data.StringBuffer (hGetStringBuffer)
import GHC.Driver.Config.Parser (initParserOpts)
import GHC.Parser.Header (getImports)
import GHC.Paths (libdir)
import GHC.Tc.Utils.TcType (tcSplitSigmaTy)
import GHC.Types.FieldLabel (flLabel)
import GHC.Types.Name (getOccString, isSymOcc, nameModule_maybe, nameOccName)
import GHC.Unit.Types (unitString)
import GHC.Utils.Outputable (showPprUnsafe)
import Language.Haskell.Syntax.Basic (field_label)

import GhcWasmBindgen.Model

-- | What to inspect, and where its code comes from.
data InspectOptions = InspectOptions
  { inspFile :: FilePath
    -- ^ The API module's source; its module name is read from the header.
  , inspSource :: Source
  }

-- | Where the module's code and its dependencies come from.
data Source
  = PackageEnv FilePath
    -- ^ Read the module, already compiled, from the packages of this GHC
    -- environment file (written by cabal).
  | SourceFiles [FilePath]
    -- ^ Typecheck the module from source, with these extra import
    -- directories (its own source root is added) and the global package
    -- database.

-- | Describe what the module exports. On failure, returns every problem found
-- (not just the first).
inspect :: InspectOptions -> IO (Either [String] Api)
inspect opts = do
  file <- makeAbsolute (inspFile opts)
  runGhc (Just libdir) $ do
    dflags0 <- getSessionDynFlags
    logger <- getLogger
    -- "-" ignores ambient package environments, so the result does not
    -- depend on the machine.
    let env = case inspSource opts of
          PackageEnv e -> e
          SourceFiles _ -> "-"
    (dflags1, _, _) <- parseDynamicFlags logger dflags0 [noLoc "-package-env", noLoc env]
    modName <- liftIO (readModuleName dflags1 file)
    dirs <- case inspSource opts of
      PackageEnv _ -> pure []
      SourceFiles extra -> liftIO ((:) (sourceRoot file modName) <$> mapM makeAbsolute extra)
    _ <- setSessionDynFlags dflags1 {ghcLink = NoLink, backend = noBackend, importPaths = dirs ++ importPaths dflags1}
    ok <- case inspSource opts of
      PackageEnv _ -> pure True
      SourceFiles _ -> do
        target <- guessTarget file Nothing Nothing
        setTargets [target]
        succeeded <$> load LoadAllTargets
    if not ok
      then pure (Left ["GHC could not typecheck " ++ inspFile opts ++ " (errors above)"])
      else do
        setContext [IIDecl (simpleImportDecl (mkModuleName modName))]
        apiMod <- lookupModule (mkModuleName modName) Nothing
        describe modName apiMod

-- | Module name from the file header, without typechecking anything.
readModuleName :: DynFlags -> FilePath -> IO String
readModuleName dflags file = do
  buf <- hGetStringBuffer file
  res <- getImports (initParserOpts dflags) False buf file file
  case res of
    Right (_, _, _, L _ m) -> pure (moduleNameString m)
    Left _ -> fail ("could not parse the module header of " ++ file)

-- | @/x/src/Foo/Api.hs@ with module @Foo.Api@ → @/x/src@.
sourceRoot :: FilePath -> String -> FilePath
sourceRoot file modName
  | modDirs == take n comps = joinPath rootDirs
  | otherwise = takeDirectory file
  where
    comps = splitOn modName
    dirs = splitDirectories (takeDirectory file)
    n = length comps - 1
    (rootDirs, modDirs) = splitAt (length dirs - n) dirs
    splitOn s = case break (== '.') s of
      (a, []) -> [a]
      (a, _ : rest) -> a : splitOn rest

------------------------------------------------------------------------------

describe :: String -> Module -> Ghc (Either [String] Api)
describe modName apiMod = do
  Just info <- getModuleInfo apiMod
  let exports = modInfoExports info
      exported = Set.fromList exports
  things <- catMaybes <$> mapM lookupName exports

  errs <- liftIO (newIORef [])
  let report e = modifyIORef' errs (e :)

  -- Functions.
  fns <- liftIO $ fmap catMaybes $ forM things $ \case
    AnId i | not (isRecordSelector i) -> do
      let name = getOccString i
      if isSymOcc (nameOccName (getName i))
        then Nothing <$ report ("(" ++ name ++ "): operators cannot be exported; export a named alias instead")
        else case runConv (fnFromType (idType i)) of
          Left e -> Nothing <$ report (name ++ " :: " ++ showPprUnsafe (idType i) ++ "\n    " ++ e)
          Right ((args, res), tcs) -> pure (Just (Fn (T.pack name) args res, tcs))
    _ -> pure Nothing

  -- Data types: every exported one plus everything reachable from them and
  -- from the function signatures.
  let exportedTcs = [tc | ATyCon tc <- things, isAlgTyCon tc, not (isClassTyCon tc)]
  adts <- liftIO (closure report exported (exportedTcs ++ concatMap snd fns))

  -- Instances the generated code would otherwise duplicate.
  hasGeneric <- Set.fromList . map (tyConName . fst) <$> filterM (hasInstance genericClass . fst) adts
  hasJson <- filterM (\(tc, _) -> (||) <$> hasInstance toJsonClass tc <*> hasInstance fromJsonClass tc) adts
  forM_ hasJson $ \(tc, _) ->
    liftIO (report (getOccString tc ++ ": already has aeson instances; ghc-wasm-bindgen derives its own with fixed options, remove them from the core"))

  -- Mangled JS names must stay distinct.
  let jsNames = [(jsIdent (fnName f), fnName f) | (f, _) <- fns]
  forM_ (Map.toList (Map.fromListWith (++) [(j, [h]) | (j, h) <- jsNames])) $ \(j, hs) ->
    when (length hs > 1) $
      liftIO (report ("functions " ++ show hs ++ " would all be exported to JS as " ++ show j))

  problems <- liftIO (readIORef errs)
  pure $
    if null problems
      then Right Api
        { apiModule = T.pack modName
        , apiTypes = sortOn adtName [adt {adtGeneric = tyConName tc `Set.notMember` hasGeneric} | (tc, adt) <- adts]
        , apiFns = sortOn fnName (map fst fns)
        }
      else Left (reverse problems)

-- | A class, as (name, modules that may define it).
type ClassRef = (String, [String])

genericClass, toJsonClass, fromJsonClass :: ClassRef
genericClass = ("Generic", ["GHC.Generics", "GHC.Internal.Generics"])
toJsonClass = ("ToJSON", ["Data.Aeson.Types.ToJSON"])
fromJsonClass = ("FromJSON", ["Data.Aeson.Types.FromJSON"])

-- | Whether a visible instance of the class exists for the type.
hasInstance :: ClassRef -> TyCon -> Ghc Bool
hasInstance (cls, mods) tc = any matches <$> getInstancesForType (mkTyConTy tc)
  where
    matches inst =
      getOccString (is_cls_nm inst) == cls
        && maybe False ((`elem` mods) . moduleNameString . moduleName) (nameModule_maybe (is_cls_nm inst))

-- | Converts every reachable type constructor, reporting unsupported ones.
-- Keeps first-seen order (exported types first).
closure :: (String -> IO ()) -> Set.Set Name -> [TyCon] -> IO [(TyCon, Adt)]
closure report exported = go Set.empty []
  where
    go _ acc [] = pure (reverse acc)
    go seen acc (tc : rest)
      | tyConName tc `Set.member` seen = go seen acc rest
      | otherwise = do
          let seen' = Set.insert (tyConName tc) seen
          case runConv (adtFromTyCon tc) of
            Left e -> report (getOccString tc ++ ": " ++ e) >> go seen' acc rest
            Right (adt, more) -> do
              let missing =
                    [getOccString tc | tyConName tc `Set.notMember` exported]
                      ++ [ getOccString tc ++ "(" ++ T.unpack (conName c) ++ ")"
                         | (c, dc) <- zip (adtCons adt) (tyConDataCons tc)
                         , getName dc `Set.notMember` exported
                         ]
              unless (null missing) $
                report (getOccString tc ++ ": used by the API, so the type and all its constructors must be exported (" ++ unwords (nub missing) ++ " missing); add `" ++ getOccString tc ++ " (..)` to the export list")
              go seen' ((tc, adt) : acc) (rest ++ more)

------------------------------------------------------------------------------
-- Type conversion. Collects the data types it encounters.

newtype Conv a = Conv {runConv :: Either String (a, [TyCon])}

instance Functor Conv where
  fmap f (Conv r) = Conv (fmap (\(a, w) -> (f a, w)) r)

instance Applicative Conv where
  pure a = Conv (Right (a, []))
  Conv f <*> Conv a = Conv $ do
    (g, w1) <- f
    (x, w2) <- a
    pure (g x, w1 ++ w2)

instance Monad Conv where
  Conv a >>= k = Conv $ do
    (x, w1) <- a
    (y, w2) <- runConv (k x)
    pure (y, w1 ++ w2)

failConv :: String -> Conv a
failConv e = Conv (Left e)

tell :: TyCon -> Conv ()
tell tc = Conv (Right ((), [tc]))

fnFromType :: Type -> Conv ([Ty], Ty)
fnFromType ty = do
  let (tvs, theta, tau) = tcSplitSigmaTy ty
  unless (null tvs) $ failConv "polymorphic functions are not supported; give the export a concrete type"
  unless (null theta) $ failConv "constraints are not supported; give the export a concrete type"
  let (args, res) = splitFunTys tau
  (,) <$> mapM (toTy . scaledThing) args <*> resultTy res
  where
    resultTy r = case splitTyConApp_maybe (expandTypeSynonyms r) of
      Just (tc, _) | getOccString tc == "IO" -> failConv "IO is not supported yet; exports must be pure"
      _ -> toTy r

toTy :: Type -> Conv Ty
toTy t0 = do
  let t = expandTypeSynonyms t0
  case splitFunTy_maybe t of
    Just _ -> failConv ("function-typed values cannot cross the boundary yet: " ++ showPprUnsafe t0)
    Nothing -> case splitTyConApp_maybe t of
      Nothing -> failConv ("unsupported type " ++ showPprUnsafe t0)
      Just (tc, args) -> case args of
        []
          | tc == doubleTyCon -> pure TDouble
          | tc == floatTyCon -> pure TFloat
          | tc == intTyCon -> pure TInt
          | tc == wordTyCon -> pure TWord
          | tc == integerTyCon -> pure TInteger
          | tc == naturalTyCon -> pure TNatural
          | tc == boolTyCon -> pure TBool
          | tc == charTyCon -> pure TChar
          | Just n <- sized "Int" intModules -> pure (TIntN n)
          | Just n <- sized "Word" wordModules -> pure (TWordN n)
          | is "Text" ["Data.Text.Internal"] -> pure TText
          | is "Day" ["Data.Time.Calendar.Days"] -> pure TDay
          | is "UTCTime" ["Data.Time.Clock.Internal.UTCTime"] -> pure TUTCTime
          | is "IntSet" ["Data.IntSet.Internal"] -> pure (TSet TInt)
        [a]
          | tc == listTyCon -> case splitTyConApp_maybe a of
              Just (c, []) | c == charTyCon -> pure TString
              _ -> TList <$> toTy a
          | tc == maybeTyCon -> TMaybe <$> toTy a
          | is "Vector" ["Data.Vector"] -> TVector <$> toTy a
          | is "NonEmpty" ["GHC.Base", "GHC.Internal.Base"] -> TNonEmpty <$> toTy a
          | is "Set" ["Data.Set.Internal"] || is "HashSet" ["Data.HashSet.Internal"] -> TSet <$> toTy a
          | is "IntMap" ["Data.IntMap.Internal"] -> TIntMap <$> toTy a
        [k, v]
          | is "Either" ["Data.Either", "GHC.Internal.Data.Either"] -> TEither <$> toTy k <*> toTy v
          | is "Map" ["Data.Map.Internal"] || is "HashMap" ["Data.HashMap.Internal"] -> do
              key <- toTy k
              case key of
                TAdt _ -> pure ()
                _ | isTextKey key -> pure ()
                _ -> failConv ("unsupported map key type " ++ showPprUnsafe k ++ "; use a string, number, Bool, Char, date or one of your data types")
              TMap key <$> toTy v
        _
          | isBoxedTupleTyCon tc, length args >= 2 -> TTuple <$> mapM toTy args
          | isAlgTyCon tc, not (isClassTyCon tc), not (fromBase tc) ->
              if null args
                then TAdt (T.pack (getOccString tc)) <$ tell tc
                else failConv ("types with parameters are not supported yet: " ++ showPprUnsafe t0)
          | otherwise -> failConv ("unsupported type " ++ showPprUnsafe t0)
        where
          is occ mods =
            getOccString tc == occ
              && maybe False ((`elem` mods) . moduleNameString . moduleName) (nameModule_maybe (tyConName tc))
          -- Int8 … Int64, Word8 … Word64.
          sized prefix mods =
            listToMaybe [n | n <- [8, 16, 32, 64], is (prefix ++ show n) mods]
          intModules = ["GHC.Int", "GHC.Internal.Int"]
          wordModules = ["GHC.Word", "GHC.Internal.Word"]

-- | Types from GHC's own libraries (Char, Integer, Float, …), which are not
-- supported unless listed above.
fromBase :: TyCon -> Bool
fromBase tc = case nameModule_maybe (tyConName tc) of
  Just m -> any (\p -> u == p || (p ++ "-") `isPrefixOf` u) ["base", "ghc-internal", "ghc-prim", "ghc-bignum"]
    where
      u = unitString (moduleUnit m)
  Nothing -> False

adtFromTyCon :: TyCon -> Conv Adt
adtFromTyCon tc = do
  unless (null (tyConTyVars tc)) $ failConv "types with parameters are not supported yet"
  cons <- forM (tyConDataCons tc) $ \dc -> do
    unless (isVanillaDataCon dc) $ failConv (getOccString dc ++ ": GADTs and existential constructors are not supported")
    tys <- mapM (toTy . scaledThing) (dataConOrigArgTys dc)
    let labels = map (unpackFS . field_label . flLabel) (dataConFieldLabels dc)
    pure Con
      { conName = T.pack (getOccString dc)
      , conFields =
          if null labels
            then Positional tys
            else Record (zip (map T.pack labels) tys)
      }
  when (null cons) $ failConv "empty data types have no values to pass"
  pure Adt {adtName = T.pack (getOccString tc), adtCons = cons, adtGeneric = True}