packages feed

essence-of-live-coding-0.2.5: 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)
import Control.Monad (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

-- * 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 (Maybe (LaunchedProgram m)))
possiblyLaunchedProgram _ = do
  storeMaybe <- lookupStore 0
  try $ traverse readStore 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) -> putStrLn "exc" >> launchAndSave program
    -- The store was empty, restart
    Right Nothing -> putStrLn "empty" >> launchAndSave program
    -- A program is running, update it
    Right (Just launchedProgram) -> 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 = void $ (fmap $ fmap $ fmap stop) $ possiblyLaunchedProgram proxy

-- * 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"