packages feed

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

{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE RecordWildCards #-}
{-# 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 (Exception (displayException, toException), SomeException, try)
import Control.Monad (join, void, (>=>))
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"