packages feed

essence-of-live-coding-0.2.6: src/LiveCoding/GHCi.hs

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{- | Support functions to call common live coding functionalities like launching and reloading
from a @ghci@ or @cabal repl@ session.

You typically don't need to import this module in your code,
but you should load it in your interactive session,
ideally by copying the file `essence-of-live-coding/.ghci` to your project,
adjusting it to your needs and launching @cabal repl@.
-}
module LiveCoding.GHCi where

-- base
import Control.Concurrent
import Control.Exception (SomeException, try, Exception (toException, displayException))
import Control.Monad (void, (>=>), join)
import Data.Data
import Data.Function ((&))

-- transformers
import Control.Monad.Trans.State.Strict

-- foreign-store
import Foreign.Store

-- essence-of-live-coding
import LiveCoding.LiveProgram
import LiveCoding.RuntimeIO.Launch

proxyFromLiveProgram :: LiveProgram m -> Proxy m
proxyFromLiveProgram _ = Proxy

-- | An exception type marking the absence of a foreign store of the correct type.
data NoStore = NoStore
  deriving Show

instance Exception NoStore

-- * Retrieving launched programs from the foreign store

-- | Try to retrieve a 'LiveProgram' of a given type from the 'Store',
--   handling all 'IO' exceptions.
--   Returns 'Right Nothing' if the store didn't exist.
possiblyLaunchedProgram
  :: Launchable m
  => Proxy m
  -> IO (Either SomeException (LaunchedProgram m))
possiblyLaunchedProgram _ = do
  storeMaybe <- lookupStore 0
  fmap join $ try $ traverse readStore $ maybe (Left $ toException NoStore) Right storeMaybe

-- | Try to load a 'LiveProgram' of a given type from the 'Store'.
--   If the store doesn't contain a program, it is (re)started.
sync :: Launchable m => LiveProgram m -> IO ()
sync program = do
  launchedProgramPossibly <- possiblyLaunchedProgram $ proxyFromLiveProgram program
  case launchedProgramPossibly of
    -- Looking up the store failed in some way, restart
    Left (e :: SomeException) -> do
      putStrLn $ displayException e
      launchAndSave program

    -- A program is running, update it
    Right launchedProgram -> do
      putStrLn "update"
      update launchedProgram program

-- | Launch a 'LiveProgram' and save it in the 'Store'.
launchAndSave :: Launchable m => LiveProgram m -> IO ()
launchAndSave = launch >=> save

-- | Save a 'LiveProgram' to the store.
save :: Launchable m => LaunchedProgram m -> IO ()
save = writeStore $ Store 0

-- | Try to retrieve a 'LaunchedProgram' from the 'Store',
--   and if successful, stop it.
stopStored
  :: Launchable m
  => Proxy m
  -> IO ()
stopStored proxy = do
  launchedProgramPossibly <- possiblyLaunchedProgram proxy
  either (putStrLn . displayException) stop launchedProgramPossibly

-- * GHCi commands

-- ** Debugging
-- TODO Could also parametrise this and all other commands by the 'liveProgram'

-- | Initialise a launched program in the store,
--   but don't start it.
liveinit _ = return $ unlines
  [ "programVar <- newMVar liveProgram"
  , "threadId <- myThreadId"
  , "save LaunchedProgram { .. }"
  ]

-- | Run one program step, assuming you have a launched program in a variable @launchedProgram@.
livestep _ = return "stepLaunchedProgram launchedProgram"

-- ** Running

-- | Launch or restart a program and save its reference in the store.
livelaunch _ = return "sync liveProgram"

-- | Reload the code and do hot code swap and migration.
livereload _ = return $ unlines
  [ ":reload"
  , "sync liveProgram"
  ]

-- | Stop the program.
livestop _ = return "stopStored $ proxyFromLiveProgram liveProgram"