diff --git a/ipython-kernel/examples/Calc.hs b/ipython-kernel/examples/Calc.hs new file mode 100644 index 0000000000000000000000000000000000000000..ba4cf1f8ba09c28725194849066607fc495a71bb --- /dev/null +++ b/ipython-kernel/examples/Calc.hs @@ -0,0 +1,236 @@ +{-# LANGUAGE FlexibleContexts, FlexibleInstances, OverloadedStrings, MultiWayIf #-} +module Main where + +import Control.Applicative +import Control.Arrow + +import Control.Concurrent (MVar, newMVar, takeMVar, putMVar, threadDelay) +import Control.Monad (guard) +import Control.Monad.IO.Class (MonadIO(..)) +import Control.Monad.State.Strict (StateT, get, modify, runStateT) + +import Data.Char (isDigit) +import Data.List (isPrefixOf) +import Data.Monoid ((<>)) +import qualified Data.Text as T + +import IHaskell.IPython.Kernel +import IHaskell.IPython.EasyKernel (easyKernel, KernelConfig(..)) + +import System.Environment (getArgs) + +import Text.Parsec (Parsec, ParseError, alphaNum, char, letter, oneOf, optionMaybe, runParser, (<?>)) +import qualified Text.Parsec.Token as P + +--------------------------------------------------------- +-- Hutton's Razor, plus time delays, plus a global state +--------------------------------------------------------- + +-- | This language is Hutton's Razor with two added operations that +-- are needed to demonstrate the kernel features: a global state, +-- accessed and modified using Count, and a sleep operation. +data Razor = I Integer + | Plus Razor Razor + | SleepThen Double Razor + | Count + deriving (Read, Show, Eq) + + +--------- +-- Parser +--------- + +razorDef :: Monad m => P.GenLanguageDef String a m +razorDef = P.LanguageDef + { P.commentStart = "(*" + , P.commentEnd = "*)" + , P.commentLine = "//" + , P.nestedComments = True + , P.identStart = letter <|> char '_' + , P.identLetter = alphaNum <|> char '_' + , P.opStart = oneOf "+" + , P.opLetter = oneOf "+" + , P.reservedNames = ["sleep", "then", "end", "count"] + , P.reservedOpNames = [] + , P.caseSensitive = True + } + +lexer :: Monad m => P.GenTokenParser String a m +lexer = P.makeTokenParser razorDef + +parens :: Parsec String a b -> Parsec String a b +parens = P.parens lexer + +reserved :: String -> Parsec String a () +reserved = P.reserved lexer + +integer :: Parsec String a Integer +integer = P.integer lexer + +float :: Parsec String a Double +float = P.float lexer + +operator :: Parsec String a String +operator = P.operator lexer + +keyword :: String -> Parsec String a () +keyword kwd = reserved kwd <?> "the keyword \"" ++ kwd ++ "\"" + +literal :: Parsec String a Razor +literal = I <$> integer + +sleepThen :: Parsec String a Razor +sleepThen = do keyword "sleep" + delay <- float <?> "seconds" + keyword "then" + body <- expr + keyword "end" <?> "" + return $ SleepThen delay body + +count :: Parsec String a Razor +count = keyword "count" >> return Count + +expr :: Parsec String a Razor +expr = do one <- parens expr <|> literal <|> sleepThen <|> count + rest <- optionMaybe (do op <- operator + guard (op == "+") + expr) + case rest of + Nothing -> return one + Just other -> return $ Plus one other + +parse :: String -> Either ParseError Razor +parse = runParser expr () "(input)" + + +---------------------- +-- Language operations +---------------------- + +-- | Completion +langCompletion :: T.Text -> T.Text -> Int -> Maybe ([T.Text], T.Text, T.Text) +langCompletion _code line col = + let (before, _) = T.splitAt col line + in fmap (\word -> (map T.pack . matchesFor $ T.unpack word, word, word)) + (lastMaybe (T.words before)) + where + lastMaybe :: [a] -> Maybe a + lastMaybe [] = Nothing + lastMaybe [x] = Just x + lastMaybe (_:xs) = lastMaybe xs + matchesFor :: String -> [String] + matchesFor input = filter (isPrefixOf input) available + available = ["sleep", "then", "end", "count"] ++ map show [(-1000::Int)..1000] + +-- | Documentation lookup +langInfo :: T.Text -> Maybe (T.Text, T.Text, T.Text) +langInfo obj = + if | any (T.isPrefixOf obj) ["sleep", "then", "end"] -> + Just (obj, sleepDocs, sleepType) + | T.isPrefixOf obj "count" -> + Just (obj, countDocs, countType) + | obj == "+" -> Just (obj, plusDocs, plusType) + | T.all isDigit obj -> Just (obj, intDocs obj, intType) + | [x, y] <- T.splitOn "." obj, + T.all isDigit x, + T.all isDigit y -> Just (obj, floatDocs obj, floatType) + | otherwise -> Nothing + where + sleepDocs = "sleep DURATION then VALUE end: sleep DURATION seconds, then eval VALUE" + sleepType = "sleep FLOAT then INT end" + plusDocs = "Perform addition" + plusType = "INT + INT" + intDocs i = "The integer " <> i + intType = "INT" + floatDocs f = "The floating point value " <> f + floatType = "FLOAT" + countDocs = "Increment and return the current counter" + countType = "INT" + +-- | Messages sent to the frontend during evaluation will be lists of trace elements +data IntermediateEvalRes = Got Razor Integer + | Waiting Double + deriving Show + +-- | Cons for lists of trace elements - in this case, "sleeping" +-- messages should replace old ones to create a countdown effect. +consRes :: IntermediateEvalRes -> [IntermediateEvalRes] -> [IntermediateEvalRes] +consRes r@(Waiting _) (Waiting _ : s) = r:s +consRes r s = r:s + +-- | Execute an expression. +execRazor :: MVar Integer -- ^ The global counter state + -> Razor -- ^ The term to execute + -> IO () -- ^ Callback to clear output so far + -> ([IntermediateEvalRes] -> IO ()) -- ^ Callback for intermediate results + -> StateT ([IntermediateEvalRes], T.Text) IO Integer +execRazor _ x@(I i) _ _ = + modify (second (<> (T.pack (show x)))) >> return i +execRazor val tm@(Plus x y) clear send = + do modify (second (<> (T.pack (show tm)))) + x' <- execRazor val x clear send + modify (first $ consRes (Got x x')) + sendState + y' <- execRazor val y clear send + modify (first $ consRes (Got y y')) + sendState + let res = x' + y' + modify (first $ consRes (Got tm res)) + sendState + return res + where sendState = liftIO clear >> fst <$> get >>= liftIO . send +execRazor val (SleepThen delay body) clear send + | delay <= 0.0 = execRazor val body clear send + | delay > 0.1 = do modify (first $ consRes (Waiting delay)) + sendState + liftIO $ threadDelay 100000 + execRazor val (SleepThen (delay - 0.1) body) clear send + | otherwise = do modify (first $ consRes (Waiting 0)) + sendState + liftIO $ threadDelay (floor (delay * 1000000)) + execRazor val body clear send + where sendState = liftIO clear >> fst <$> get >>= liftIO . send +execRazor val Count clear send = do + i <- liftIO $ takeMVar val + modify (first $ consRes (Got Count i)) + sendState + liftIO $ putMVar val (i+1) + return i + + where sendState = liftIO clear >> fst <$> get >>= liftIO . send + +-- | Generate a language configuration for some initial state +mkConfig :: MVar Integer -- ^ The internal state of the execution + -> KernelConfig IO [IntermediateEvalRes] (Either ParseError Integer) +mkConfig var = KernelConfig + { languageName = "Hutton's Razor + extra" + , languageVersion = [0,1,0] + , displayResult = displayRes + , displayOutput = displayOut + , completion = langCompletion + , objectInfo = langInfo + , run = parseAndRun + , debug = False + } + where + displayRes (Left err) = + [ DisplayData MimeHtml . T.pack $ "<em>" ++ show err ++ "</em>" + , DisplayData PlainText . T.pack $ show err + ] + displayRes (Right x) = + return . DisplayData MimeHtml . T.pack $ + "Answer: <strong>" ++ show x ++ "</strong>" + displayOut out = + let outLines = reverse (map (T.pack . show) out) + in return (DisplayData PlainText (T.unlines outLines)) + parseAndRun code clear send = + case parse (T.unpack code) of + Left err -> return (Left err, Err, "") + Right tm -> do + (res, (_, pager)) <- runStateT (execRazor var tm clear send) ([], "") + return (Right res, Ok, T.unpack pager) + +main :: IO () +main = do ["kernel", profileFile] <- getArgs + val <- newMVar 1 + easyKernel profileFile (mkConfig val) diff --git a/ipython-kernel/ipython-kernel.cabal b/ipython-kernel/ipython-kernel.cabal index 95c1335925339d1084ce3870123fdfe207f31bb8..7ca7ccfb2b840c024aa00155341485673a865acc 100644 --- a/ipython-kernel/ipython-kernel.cabal +++ b/ipython-kernel/ipython-kernel.cabal @@ -14,6 +14,10 @@ build-type: Simple cabal-version: >=1.16 +flag examples + description: Build example programs + default: False + library exposed-modules: IHaskell.IPython.Kernel IHaskell.IPython.Types @@ -22,6 +26,7 @@ library IHaskell.IPython.Message.Writer IHaskell.IPython.Message.Parser IHaskell.IPython.Message.UUID + IHaskell.IPython.EasyKernel -- other-modules: other-extensions: OverloadedStrings hs-source-dirs: src @@ -31,7 +36,23 @@ library bytestring >=0.10, cereal >=0.3, containers >=0.5, + mtl >=2.1, text >=0.11, + transformers >=0.3, unix >=2.6, uuid >=1.3, zeromq4-haskell >=0.1 + +-- Example program +executable simple-calc-example + hs-source-dirs: examples + main-is: Calc.hs + build-depends: ipython-kernel, + base >=4.6 && < 4.8, + mtl >=2.1, + parsec >=3.1, + text >= 0.11, + transformers >=0.3 + + if !flag(examples) + buildable: False diff --git a/ipython-kernel/src/IHaskell/IPython/EasyKernel.hs b/ipython-kernel/src/IHaskell/IPython/EasyKernel.hs new file mode 100644 index 0000000000000000000000000000000000000000..5da049a2e5440dedcd39cd1aa9d1a445ae90eb8d --- /dev/null +++ b/ipython-kernel/src/IHaskell/IPython/EasyKernel.hs @@ -0,0 +1,208 @@ +{-# LANGUAGE OverloadedStrings #-} + +-- | This module provides automation for writing simple IPython +-- kernels. In particular, it provides a record type that defines +-- configurations and a function that interprets a configuration as an +-- action in some monad that can do IO. +-- +-- The configuration consists primarily of functions that implement +-- the various features of a kernel, such as running code, looking up +-- documentation, and performing completion. An example for a simple +-- language that nevertheless has side effects, global state, and +-- timing effects is included in the examples directory. +-- +-- Presently, there is no automation for creating the profile in the +-- .ipython directory. One should follow the IPython instructions for +-- this. +module IHaskell.IPython.EasyKernel (easyKernel, KernelConfig(..)) where + +import Data.Aeson (decode) + +import qualified Data.ByteString.Lazy as BL + +import Control.Concurrent (MVar, readChan, writeChan, newMVar, readMVar, modifyMVar_) +import Control.Monad.IO.Class (MonadIO(..)) +import Control.Monad (forever, when) + +import qualified Data.Map as Map +import Data.Maybe (fromMaybe) +import qualified Data.Text as T + +import IHaskell.IPython.Kernel +import IHaskell.IPython.Message.UUID as UUID + + +import System.Exit (exitSuccess) +import System.IO (openFile, IOMode(ReadMode)) + +-- | The kernel configuration specifies the behavior that is specific +-- to your language. The type parameters provide the monad in which +-- your kernel will run, the type of intermediate outputs from running +-- cells, and the type of final results of cells, respectively. +data KernelConfig m output result = KernelConfig + { languageName :: String -- ^ The name of the language + , languageVersion :: [Int] -- ^ The version of the language + , displayOutput :: output -> [DisplayData] -- ^ How to render intermediate output + , displayResult :: result -> [DisplayData] -- ^ How to render final cell results + , completion :: T.Text -> T.Text -> Int -> Maybe ([T.Text], T.Text, T.Text) + -- ^ Perform completion. The returned tuple consists of the matches, + -- the matched text, and the completion text. The arguments are the + -- code in the cell, the current line as text, and the column at + -- which the cursor is placed. + , objectInfo :: T.Text -> Maybe (T.Text, T.Text, T.Text) + -- ^ Return the information or documentation for its argument. The + -- returned tuple consists of the name, the documentation, and the + -- type, respectively. + , run :: T.Text -> IO () -> (output -> IO ()) -> m (result, ExecuteReplyStatus, String) + -- ^ Execute a cell. The arguments are the contents of the cell, an + -- IO action that will clear the current intermediate output, and an + -- IO action that will add a new item to the intermediate + -- output. The result consists of the actual result, the status to + -- be sent to IPython, and the contents of the pager. Return the + -- empty string to indicate that there is no pager output. Errors + -- should be handled by defining an appropriate error constructor in + -- your result type. + , debug :: Bool -- ^ Whether to print extra debugging information to + -- the console + } + +getProfile :: FilePath -> IO Profile +getProfile fn = do + profData <- openFile fn ReadMode >>= BL.hGetContents + case decode profData of + Just prof -> return prof + Nothing -> error "Invalid profile data" + +createReplyHeader :: MonadIO m => MessageHeader -> m MessageHeader +createReplyHeader parent = do + -- Generate a new message UUID. + newMessageId <- liftIO UUID.random + let repType = fromMaybe err (replyType $ msgType parent) + err = error $ "No reply for message " ++ show (msgType parent) + + return MessageHeader { + identifiers = identifiers parent, + parentHeader = Just parent, + metadata = Map.fromList [], + messageId = newMessageId, + sessionId = sessionId parent, + username = username parent, + msgType = repType + } + + + +-- | Execute an IPython kernel for a config. Your 'main' action should +-- call this as the last thing it does. +easyKernel :: (MonadIO m) => FilePath -> KernelConfig m output result -> m () +easyKernel profileFile config = do + prof <- liftIO $ getProfile profileFile + zmq@(Channels shellReqChan shellRepChan ctrlReqChan ctrlRepChan iopubChan) <- + liftIO $ serveProfile prof + execCount <- liftIO $ newMVar 0 + forever $ do + req <- liftIO $ readChan shellReqChan + repHeader <- createReplyHeader (header req) + when (debug config) . liftIO $ print req + reply <- replyTo config execCount zmq req repHeader + liftIO $ writeChan shellRepChan reply + + +replyTo :: MonadIO m + => KernelConfig m output result + -> MVar Integer + -> ZeroMQInterface + -> Message + -> MessageHeader + -> m Message +replyTo config _ _ KernelInfoRequest{} replyHeader = + return KernelInfoReply + { header = replyHeader + , language = languageName config + , versionList = languageVersion config + } +replyTo config _ interface ShutdownRequest{restartPending=pending} replyHeader = do + liftIO $ writeChan (shellReplyChannel interface) $ ShutdownReply replyHeader pending + liftIO exitSuccess + +replyTo config execCount interface req@ExecuteRequest { getCode = code } replyHeader = do + let send msg = writeChan (iopubChannel interface) msg + + busyHeader <- dupHeader replyHeader StatusMessage + liftIO . send $ PublishStatus busyHeader Busy + + outputHeader <- dupHeader replyHeader DisplayDataMessage + (res, replyStatus, pagerOut) <- + let clearOutput = do + clearHeader <- dupHeader replyHeader ClearOutputMessage + send $ ClearOutput clearHeader False + sendOutput x = + send $ PublishDisplayData outputHeader (languageName config) + (displayOutput config x) + in run config code clearOutput sendOutput + liftIO . send $ PublishDisplayData outputHeader (languageName config) (displayResult config res) + + + idleHeader <- dupHeader replyHeader StatusMessage + liftIO . send $ PublishStatus idleHeader Idle + + liftIO $ modifyMVar_ execCount (return . (+1)) + counter <- liftIO $ readMVar execCount + + return ExecuteReply + { header = replyHeader + , pagerOutput = pagerOut + , executionCounter = fromIntegral counter + , status = replyStatus + } + +replyTo config _ _ req@CompleteRequest{} replyHeader = do + let code = getCode req + line = getCodeLine req + col = getCursorPos req + + return $ case completion config code line col of + Nothing -> + CompleteReply + { header = replyHeader + , completionMatches = [] + , completionMatchedText = "" + , completionText = "" + , completionStatus = False + } + Just (matches, matchedText, cmplText) -> + CompleteReply + { header = replyHeader + , completionMatches = matches + , completionMatchedText = matchedText + , completionText = cmplText + , completionStatus = True + } + +replyTo config _ _ ObjectInfoRequest { objectName = obj } replyHeader = + return $ case objectInfo config obj of + Just (name, docs, ty) -> ObjectInfoReply + { header = replyHeader + , objectName = obj + , objectFound = True + , objectTypeString = ty + , objectDocString = docs + } + Nothing -> ObjectInfoReply + { header = replyHeader + , objectName = obj + , objectFound = False + , objectTypeString = "" + , objectDocString = "" + } + +replyTo _ _ _ msg _ = do + liftIO $ putStrLn "Unknown message: " + liftIO $ print msg + return msg + + +dupHeader :: MonadIO m => MessageHeader -> MessageType -> m MessageHeader +dupHeader hdr mtype = + do uuid <- liftIO UUID.random + return hdr { messageId = uuid , msgType = mtype }