% some latex stuff for typesetting literate code... % command to wrap around code which should not show \newcommand{\texignore}[1]{} % uniform size for all code flavours \newcommand{\codesize}{\scriptsize} % code highlighting commands (in text and in own block) \newcommand{\cd}[1]{{{\codesize \tt{#1}}}} % code environments, completely verbatim (fancyvrb): \renewcommand{\FancyVerbFormatLine}[1]{~~~#1} \DefineVerbatimEnvironment{code}{Verbatim}{fontsize=\codesize, frame=lines, label=\textit{compiled code}} \DefineVerbatimEnvironment{icode}{Verbatim}{fontsize=\codesize, frame=lines, label=\textit{ignored code}} \DefineVerbatimEnvironment{xcode}{Verbatim}{fontsize=\codesize, frame=lines} \texignore{% i.e., do not print this... \begin{code} {-# OPTIONS_GHC -cpp -XOverlappingInstances #-} {-# LANGUAGE TypeSynonymInstances #-} {-# LANGUAGE IncoherentInstances #-} module Math.ComputerAlgebra.Cash.HS_SCSCP where \end{code} } \author{Jost Berthold, Hans-Wolfgang Loidl, Chris Brown University of St.Andrews (\{hwloidl,jb, chrisb\}@cs.st-andrews.ac.uk)} \hwlcomment{This documentation is incomplete and needs update before the release!!} This document was first created to describe first (top-down) implementation ideas for SCSCP in Haskell (client only). Meanwhile, changes have been made throughout, and the module now provides types and conversions from SCSCP-XML to Haskell and vice-versa. We continue to collect the basic ideas and notes here. SCSCP\cite{SCSCP-1.2} is a protocol for interaction between computer-algebra systems, following the OpenMath standard and developed in the SCIEnce EU project. \marginpar{\jbcomment{updated to SCSCP-1.3}} Basis of our design is the version 1.2 of the protocol specification. The content dictionaries (CD) for SCSCP are scscp1 and scscp2. % and % they slightly differ from the SCSCP-1.2 description. When different, % we follow the scscp CD definition of keywords, and the % SCSCP-1.2 spec. for message formats. For any terminology, pls. refer to the aforementioned specification and to the content dictionaries. \section*{Implementing SCSCP in Haskell} \subsection*{Features summary} The goal of the implementation is to send computation requests from a Haskell program to a CA system, and to receive the answers for further processing. Furthermore, the module provides features for starting and stopping its respective communication partners, namely SCSCP servers linked to CA systems. Implementing such an SCSCP client lays the ground for a more long-term goal: implementing a coordination server to use several CA systems in parallel. Such a server should be able to answer client requests, and otherwise coordinate computations which we provide as skeletons (and define an according \cd{symgrid-par} content dictionary). \subsection*{Limitations} \begin{itemize} \item We want to implement an interface for the SCSCP usage via sockets, thus not using a web service. \item Most likely, not all possible functionality of a client will be implemented. For the implemented features, see the subsequent description. \item Where possible, potentially large computation data should not be accessed inside the calling Haskell program. Haskell serves as a coordination layer. Thus, SCSCP messages will only be decomposed to a degree which allows to coordinate the respective computation, and included computation data otherwise passed on as opaque objects. \item we do not implement the binary OpenMath format in this version, but restrict ourselves to the XML variant. \end{itemize} \newpage This is where the code starts. Some imports (technical...): \begin{code} import Text.XML.HaXml.Types -- and for parsing in OMObj: import Text.XML.HaXml.Parse import Text.XML.HaXml.Verbatim import Data.Char import Data.Maybe import Data.List -- import the standard types for CA. -- currently these are manually defined, should we move -- towards the Numeric Prelude? import Math.ComputerAlgebra.Cash.SGPTypes import Debug.Trace import Control.Monad -- for maybe monad import Control.DeepSeq (NFData(..)) import Control.Parallel.Strategies hiding (NFData(..)) -- for NFData OMObj #ifdef __PARALLEL_HASKELL__ import Eden(Trans) #endif \end{code} \subsection*{SCSCP data objects} First of all, SCSCP data (in OpenMath XML) needs to be sent and received, thus stored inside Haskell, and sometimes also accessed from Haskell code. While we want to avoid it, still we need to extract basic types such as \cd{Bool}, \cd{Int}, \cd{Integer}, and \cd{String}. We store OpenMath objects as their enclosing tag, including potential attributes, and the contents as verbatim. In this manner, we can compose the XML from it easily, even without deeper XML knowledge. For simple types (in type class OMData), we generate the OpenMath representation ourselves, and can read it out just as easily. \begin{code} -- data objects, opaque if at all possible type Tag = String data OMObj = OM Tag -- as name suggests [Attribute] -- attribute list String -- embedded XML deriving (Eq,Show) instance NFData OMObj where rnf (OM tag attrs content) = rnf (tag,attrs,content) -- instance NFData Attribute... but Attribute is an alias -- These guys need to be defined for it. instance NFData AttValue where rnf (AttValue vals) = rnf vals instance NFData Reference where rnf (RefEntity ref) = rnf ref rnf (RefChar n) = rnf n #ifdef __PARALLEL_HASKELL__ instance Trans OMObj #endif -- to parse in an OMObj, we use the HaXml parser -- failure is passed to the caller by the contents parseOM :: String -> OMObj parseOM input = case xmlParse' "parseOM-internal" input of Left errmsg -> let err = error ("cannot parse this OMObj: " ++ take 100 input ++ "...:\n" ++ errmsg) in OM err err err Right (Document _ _ (Elem tag atts cs) _) -> OM tag atts (concatMap verbatim cs) -- without parsing, directly XML elements (HaXml type) to OMObj elemToOM :: Element -> OMObj elemToOM (Elem tag atts cs) = OM tag atts (concatMap verbatim cs) contentToOM :: Content -> OMObj contentToOM (CElem (Elem tag atts cs)) = OM tag atts (concatMap verbatim cs) contentToOM (CString _ t) = toOM t -- encode it as a string contentToOM (CMisc x) = error "contentToOM: unexpected content" -- converting back OMObj as XML. We do it directly, don't know what -- HaXML would format for us writeOMObj :: OMObj -> String writeOMObj (OM tag attList []) | tag == "OMSTR" = "" -- workaround needed in GAP SCSCP | otherwise = '<':tag ++ ' ':writeAttributes attList ++ "/>" writeOMObj (OM tag attList nonemptyContent) = '<':tag ++ ' ':writeAttributes attList ++ ' ':'>':nonemptyContent ++ '<':'/':tag ++ ">" -- ... but we can convert it back to a HaXml type as well, -- ignoring that the content might contain tags: objToHaXml :: OMObj -> Content objToHaXml (OM tag attList content) = CElem (Elem tag attList [CString True content]) writeAttributes :: [Attribute] -> String writeAttributes = unwords . map showAttribute showAttribute :: Attribute -> String showAttribute (name, val) = name ++ "=" ++ show val -- Attribute is an alias, thus not instance of Show... -- These guys need to be defined for it. instance Show AttValue where show (AttValue vals) = "\"" ++ concatValStrings vals ++ "\"" concatValStrings :: [Either String Reference] -> String concatValStrings [] = "" concatValStrings ((Left s):rest) = s ++ concatValStrings rest concatValStrings ((Right ref):rest) = show ref ++ concatValStrings rest instance Show Reference where show (RefEntity ref) = '&':ref ++ ";" -- show (RefChar n)) = "\"&#" ++ show n ++ ";\"" show (RefChar n) = "&#x" ++ hex n ++ ";" -- hex :: Int -> String hex n | n < 0 = hex (n `mod` 256) -- HACK | n < 16 = c:"" | otherwise = hex rest ++ digits!!last : "" where rest = n `div` 16 last = n `mod` 16 c = digits!!n digits= "0123456789ABCDEF" \end{code} We want to pass around OM objects inside Haskell, but not inspect them (see above). Certain simple Haskell types need to be convertible to OMObj (which we do straight-forward by hand,), we define a type class and instances for the conversion here. \begin{code} -- A type class for Haskell data types which can be embedded in an -- SCSCP message. class OMData a where toOM :: a -> OMObj fromOM :: OMObj -> a instance OMData OMObj where toOM = id fromOM = id -- instance OMData Bool where toOM b = OM "OMS" atts "" where atts :: [Attribute] atts = [("cd" ,AttValue [Left "logic1"]), ("name",AttValue [Left val] ) ] val = if b then "true" else "false" fromOM (OM "OMS" attribs "" ) | (cd `elem` attribs) && isJust attribute = decode val where attribute = lookup "name" attribs AttValue ((Left val):_) = fromJust attribute cd = ("cd",(AttValue [Left "logic1"])) decode "true" = True decode "false" = False fromOM other = error ("cannot read Boolean from " ++ show other) instance OMData Int where toOM i = OM "OMI" [] (show i) fromOM (OM "OMI" [] num) = read num fromOM other = error ("cannot read Int from " ++ show other) instance OMData Integer where toOM i = OM "OMI" [] (show i) fromOM (OM "OMI" [] num) = read num fromOM other = error ("cannot read Integer from " ++ show other) instance OMData String where toOM s = OM "OMSTR" [] (encodeXML s) fromOM (OM "OMSTR" [] txt) = txt fromOM other = error ("cannot treat " ++ show other ++ " as String") {-instance OMData MatrixRow where toOM (MatrixRow ariths) = res where res = OM "OMA" [] (concat (map writeOMObj ([OM "OMS" [("cd", AttValue [Left "linalg2"]), ("name", AttValue [Left "matrixrow"])] "", toOM ariths]))) fromOM (OM "OMA" [] listXML) = case parsed of Left msg -> passError msg Right (Document _ _ (Elem oma [] ((CElem lSym):elems)) _) -> scrut lSym elems where -- use xmlParse' to deconstruct scrut lSym elems | isMatrixRowSym lSym = (MatrixRow (mkF elems)) | otherwise = passError "Not a MatrixRow element" parsed = xmlParse' "list-parse" ("" ++ trim listXML ++ "") -- String trimming (remove white). definition below. mkF e = (map (fromOM . parseOM) . filter (not . all isSpace) . -- rule out \n map verbatim) e passError msg = error ("cannot parse as a matrixRow:" ++ take 100 listXML ++ "..: " ++ msg) fromOM other = error ("cannot read matrixRow from " ++ show other) -} -- added by Chris Brown 9 Sept 2010 -- convert to numeric prelude? instance OMData Arith where toOM (Polynomial s) = toOM s toOM (Matrix rows) = OM "OMA" [] (concat (matrixOMS : map (writeOMObj . toOM) rows)) toOM (List xs) = OM "OMA" [] (concat (listOMS : map (writeOMObj . toOM) xs)) toOM (MatrixRow ariths) = OM "OMA" [] (concat (matrixRowOMS : map (writeOMObj . toOM) ariths)) toOM (Num x) = OM "OMI" [] (show x) toOM (Mul x y) = OM "OMA" [] (concat (map writeOMObj ([OM "OMS" [("cd", AttValue [Left "arith1"]), ("name", AttValue [Left "times"])] "", toOM x, toOM y]))) toOM (Power x y) = OM "OMA" [] (concat (map writeOMObj ([OM "OMS" [("cd", AttValue [Left "arith1"]), ("name", AttValue [Left "power"])] "", toOM x, toOM y]))) toOM (Rational ariths) = OM "OMA" [] (concat (rationalOMS : map (writeOMObj . toOM) ariths)) -- fromOM (OM "OMS" _ _) =error "herhehrerher!" fromOM (OM "OMSTR" [] txt) = Polynomial txt fromOM (OM "OMI" [] num) = Num (read num) fromOM (OM "OMA" [] listXML) = case parsed of Left msg -> passError msg Right (Document _ _ (Elem oma [] ((CElem lSym):elems)) _) -> scrut lSym elems where -- use xmlParse' to deconstruct scrut lSym elems -- | isEmptySym lSym = Empty | isArithMulSym lSym = (Mul (head (mkF [head elems])) (head (mkF [head (tail elems)]))) | isArithPowerSym lSym =(Power (head (mkF [head elems])) (head (mkF [head (tail elems)]))) | isMatrixSym lSym = (Matrix (mkF elems)) | isMatrixRowSym lSym = (MatrixRow (mkF elems)) | isRationalSym lSym = (Rational (mkF elems)) | isListSym lSym = (MatrixRow (mkF elems)) | otherwise = passError "Not an Arithmetic element" parsed = xmlParse' "list-parse" ("" ++ trim listXML ++ "") -- String trimming (remove white). definition below. mkF e = (map (fromOM . parseOM) . filter (not . all isSpace) . -- rule out \n map verbatim) e passError msg = error ("cannot parse as an arith:" ++ take 100 listXML ++ "..: " ++ msg) fromOM other = error ("cannot read Int from " ++ show other) instance OMData FiniteField where toOM (PrimEl x) = res where res = OM "OMA" [] ((concat (map writeOMObj [OM "OMS" att1 "", toOM x]))) att1 :: [Attribute] att1 = [("cd", AttValue [Left "finfield1"]), ("name", AttValue [Left "primitive_element"] )] fromOM (OM "OMA" [] listXML) = case parsed of Left msg -> passError msg Right (Document _ _ (Elem oma [] ((CElem lSym):elems)) _) -> if isFiniteSym lSym then (PrimEl (head (mkFinite elems))) else passError "Not a finite field" where -- use xmlParse' to deconstruct parsed = xmlParse' "list-parse" ("" ++ trim listXML ++ "") -- String trimming (remove white). definition below. mkFinite = map (fromOM . parseOM) . filter (not . all isSpace) . -- rule out \n map verbatim passError msg = error ("cannot parse as a list:" ++ take 100 listXML ++ "..: " ++ msg) encodeXML :: String -> String encodeXML "" = "" encodeXML ('&':s) = "&" ++ encodeXML s encodeXML ('<':s) = "<" ++ encodeXML s encodeXML ('>':s) = ">" ++ encodeXML s encodeXML (c:s) | isPrint c = c:encodeXML s | otherwise = "&#" ++ show (ord c) ++ ';':encodeXML s -- todo: a list instance would perhaps be very handy. -- Problems: -- 1. we need to decompose/chop the string at its element delimiters -- 2. (more severe) we could get back a weird list which leads to a -- type error (list with holes, different types). -- 3. we want a separate instance for String. So we add the flag -- -XOverlappingInstances -fallow-incoherent-instances -- added Chris Brown 11 August 2010 -- added provision to allows marshalling of matrices from GAP -- a matrix is a [[a]] component instance OMData a => OMData [a] where toOM xs = res where res = OM "OMA" [] (concat (listOMS: map (writeOMObj . toOM) xs)) -- no, we don't! -- fromOM _ = error "list conversion from OM not supported" -- or should we? fromOM (OM "OMA" [] listXML) = case parsed of Left msg -> passError msg Right (Document _ _ (Elem oma [] ((CElem lSym):elems)) _) -> if isMatrixSym lSym then mkList elems else if isListSym lSym then mkList elems else passError "not a list1" Right other -> passError "not a list2" where -- use xmlParse' to deconstruct parsed = xmlParse' "list-parse" ("" ++ trim listXML ++ "") -- String trimming (remove white). definition below. mkList = map (fromOM . parseOM) . filter (not . all isSpace) . -- rule out \n map verbatim passError msg = error ("cannot parse as a list:" ++ take 100 listXML ++ "..: " ++ msg) fromOM other = error ("list-parse: " ++ take 100 (show other) ++ "...\n is not a list.") emptySymbol = [("cd", (AttValue [Left "set1"])), ("name", (AttValue [Left "emptyset"]))] listSymbol = [("cd",(AttValue [Left "list1"])), ("name",(AttValue [Left "list"]))] setSymbol = [("cd", (AttValue [Left "set1"])), ("name", (AttValue [Left "set"]))] finiteSymbol = [("cd", (AttValue [Left "finfield1"])), ("name", (AttValue [Left "primitive_element"]))] matrixSymbol = [("cd", (AttValue [Left "linalg2"])), ("name", (AttValue [Left "matrix"]))] matrixRowSymbol = [("cd", (AttValue [Left "linalg2"])), ("name", (AttValue [Left "matrixrow"]))] arithPowerSym = [("cd", (AttValue [Left "arith1"])), ("name", (AttValue [Left "power"]))] mulPowerSym = [("cd", (AttValue [Left "arith1"])), ("name", (AttValue [Left "times"]))] rationalSym = [("cd", (AttValue [Left "nums1"])), ("name", (AttValue [Left "rational"]))] listOMS = verbatim (Elem "OMS" listSymbol []) matrixOMS = verbatim (Elem "OMS" matrixSymbol []) matrixRowOMS = verbatim (Elem "OMS" matrixRowSymbol []) rationalOMS = verbatim (Elem "OMS" rationalSym []) isListSym (Elem "OMS" s []) | s == listSymbol || s == matrixRowSymbol || s == setSymbol = True | otherwise = error ("isListSym:" ++ (show s)) isListSym other = False isEmptySym (Elem "OMS" x []) | x == emptySymbol = error "here!" isEmptySym _ = False isArithPowerSym (Elem "OMS" s []) = s == arithPowerSym isArithPowerSym _ = False isRationalSym (Elem "OMS" s []) = s == rationalSym isRationalSym _ = False isArithMulSym (Elem "OMS" s []) = s == mulPowerSym isArithMulSym _ = False isMatrixSym (Elem "OMS" s []) = s == matrixSymbol isMatrixSym other = False isMatrixRowSym (Elem "OMS" s []) = s == matrixRowSymbol isMatrixRowSym other = False isFiniteSym (Elem "OMS" s []) = s == finiteSymbol isFiniteSym other = False \end{code} \subsection*{SCSCP message data type} The following messages are described by the SCSCP spec., and mapped to a Haskell type (containing useful other types for encoding). \begin{code} -- aliases type CallID = String type CATime = Int -- ?too coarse? type CAMem = Int -- type for CA references (stored inside the server) newtype CARef = CARef String -- to be embedded in an OMR object deriving (Eq,Read,Show) instance NFData CARef where rnf (CARef s) = rnf s -- no instance Trans, should stay local! instance OMData CARef where toOM (CARef s) = OM "OMR" [("xref",AttValue [Left s])] "" fromOM (OM "OMR" attribs "" ) = CARef xref where attribute = lookup "xref" attribs AttValue ((Left xref):_) = fromJust attribute fromOM other = error ("cannot treat " ++ show other ++ " as OM-ref.") -- type for commands to execute. Either a standard operation (defined -- in scscp2 CD), or a server-provided operation encoded as -- (cd-name,op-name) type CAName = Either (String,String) CAStandardName data SCSCPMsg = -- to CA System PCall { pcCallID :: CallID , pcName :: CAName , pcData :: [OMObj] , pcOpts :: ProcOptions } | PInterrupt -- empty content (!! can cause race condition !!) -- from CA System -- procedure complete: called Proc.Result here | PResult { prResult :: OMObj , prCallID :: CallID , prTime :: Maybe CATime , prMem :: Maybe CAMem } | PTerminated { ptCallID :: CallID , ptReturned :: CAError , ptTime :: Maybe CATime , ptMem :: Maybe CAMem } deriving (Show) -- we will often need to extract the callID immediately: callID :: SCSCPMsg -> CallID callID (PCall cid _ _ _) = cid callID (PResult _ cid _ _) = cid callID (PTerminated cid _ _ _) = cid -- encoded procedure options for PCall data ProcOptions = PCOpts { pcResult :: Maybe PResultOption , pcMaxTime :: Maybe CATime , pcMinMem :: Maybe CAMem , pcMaxMem :: Maybe CAMem , pcDebug :: Maybe Int } deriving (Read,Show) -- scscp1 defines the following names: option_debuglevel, -- option_max_memory, option_min_memory, option_runtime nMaxTime= "option_runtime" nMaxMem = "option_max_memory" nMinMem = "option_min_memory" nDebug = "option_debuglevel" data PResultOption = Result | ResultRef | NoResult deriving (Read,Show) -- defined following scscp1: option_return_cookie, -- option_return_nothing, option_return_object, decodeResultOption :: String -> PResultOption decodeResultOption "option_return_nothing" = NoResult decodeResultOption "option_return_cookie" = ResultRef decodeResultOption "option_return_object" = Result -- decodeResultOption other = Result -- defaulting decodeResultOption other = error ("not a result option:" ++ other) encodeResultOption :: PResultOption -> String -- XML encodeResultOption NoResult = "option_return_nothing" encodeResultOption ResultRef = "option_return_cookie" encodeResultOption Result = "option_return_object" defaultProcOptions = PCOpts (Just Result) Nothing Nothing Nothing Nothing -- HWL: WAS: -- defaultProcOptions = PCOpts Nothing Nothing -- Nothing Nothing Nothing writeOpts :: CallID -> ProcOptions -> String writeOpts callId (PCOpts res maxtime minmem maxmem debug) = prefix ++ "\"call_id\" />" -- HWL: WAS: "call_ID" ++ encodeXML callId ++ "" ++ maybeResOpt ++ concat (zipWith maybeWriteInt [nMaxTime,nMinMem,nMaxMem,nDebug] [maxtime,minmem,maxmem,debug]) where maybeResOpt = case res of Nothing -> "" Just r -> prefix ++ show (encodeResultOption r) ++ " />" maybeWriteInt name value = case value of Nothing -> "" Just i -> prefix ++ show name ++ " />" ++ writeOMObj (toOM (i::Int)) prefix = " String -- clearly, pass on errors from CA system: errText (CAMsg msg) = msg -- I added sensible text for these, unsure. The messages should follow -- a canonical format if there is one: errText (CAInvalidRef ) -- (CARef r)) = "invalid reference " -- ++ r errText (CANoSuchProc (Left (cd,name))) = "procedure " ++ name ++ " does not exist in " ++ cd -- this would need no text, if scscp1 defined an own error type errText CAInterrupted = "Interrupted" errText CAMemExhausted = "" -- SCSCP-1.2 spec gives an example which implies: --errText CAMemExhausted = "Exceeded the permitted memory" errText CATimeExhausted = "" errText (SystemError s) = "sys error:" ++ s -- writing out add. information for PTerminated and PResult messages. -- Would be needed for a server... we add it for completeness. writeInfos :: CallID -> Maybe CATime -> Maybe CAMem -> String writeInfos id t mem = "" -- HWL: was call_ID ++ "" ++ id ++ "" ++ maybeTime ++ maybeMem where maybeTime = case t of Nothing -> "" Just t -> "" ++ writeOMObj (toOM t) maybeMem = case mem of Nothing -> "" Just m -> "" ++ writeOMObj (toOM m) \end{code} SCSCP assumes initial exchange of technical information messages (see below), after which a sequence of dialogs between the client and SCSCP server is performed, where the client sends a sequence of \cd{PCall} messages, and the server responds each of them, in their original order, with a corresponding \cd{PResult} or \cd{PTerminated} message. The specification describes \cd{CallID} as a convenience and debug feature only. Clients can also send an \cd{PInterrupt} message, containing no data. Its semantics on receiver (server) side is to stop the \emph{current} procedure immediately, further ones might be in the message queue of the server (and are not affected), there is no way of addressing one of the several \cd{PCalls} using \cd{PInterrupt}, and the server immediately reacts. \subsection{Assumed standard procedures} SCSCP assumes a set of standard procedures (e.g. to request supported operations from a server), which we encode as follows and may be used as \cd{pcName} in a \cd{PCall}. \begin{code} data CAStandardName = GetAllowedHeads -- returns "symbol_sets" | GetSignature -- HWL: could encode cd and name as args here -- returns "signature" -- (OMSymbol,minarg,maxarg,(list of) symbol_sets ) -- JB: pTerminated if not supported? Guess so. | GetTransientCD -- returns server-specific Content Dictionary -- (should prefix SCSCP_transient_) | GetServiceDescr -- returns "service_description", -- containing 3 Strings: -- CA system name, version, and descriptive text | StoreObj -- computes an object, stores it and returns CARef | RetrieveObj -- CARef -- takes ref, returns the OMObj | UnbindObj -- CARef -- deletes a CARef'ed object from the server deriving (Eq,Read,Show) -- this follows the scscp2 CD as published online encodeOp :: CAStandardName -> String encodeOp GetAllowedHeads = "get_allowed_heads" encodeOp GetSignature = "get_signature" encodeOp GetTransientCD = "get_transient_cd" encodeOp GetServiceDescr = "get_service_description" encodeOp StoreObj = "store" encodeOp RetrieveObj = "retrieve" -- CARef to be added! encodeOp UnbindObj = "unbind" -- deletes a CARef'ed, to be added! decodeOp :: String -> CAStandardName decodeOp "get_allowed_heads" = GetAllowedHeads decodeOp "get_signature" = GetSignature decodeOp "get_transient_cd" = GetTransientCD decodeOp "get_service_description" = GetServiceDescr decodeOp "store" = StoreObj decodeOp "retrieve" = RetrieveObj -- (RetrieveObj undefined) -- CARef to be added! decodeOp "unbind" = UnbindObj -- (UnbindObj undefined) -- CARef to be added! decodeOp other = error ("not a standard name: " ++ other) \end{code} These routines are called when handling the corresponding CAStandardName: \begin{code} constructServiceDescr name version descr = OM "OMA" atts (Data.List.concat (Data.List.intersperse "\n" (map writeOMObj [(toOM name), (toOM version), (toOM descr)]))) where atts :: [Attribute] atts = [("cd" ,AttValue [Left "scscp2"]), ("name",AttValue [Left "service_description"] ) ] \end{code} \textbf{TODO:} constructor functions for all CAStandardName \subsection*{The XML format specification} Part 4 of the spec. contains an informal specification of the messages, which we reproduce here for documentation. \begin{itemize} \item Procedure Call (\cd{PCall}) \begin{xcode} call_identifier runtime_limit_in_milliseconds minimal_memory_required_in_bytes memory_limit_in_bytes debuglevel_value \end{xcode} \jbcomment{So why are these call attributes not XML attributes? Must be because OpenMath had to be left unmodified. IMHO, an extension by a new tag would have been the way to go... including all PC attributes as real XML attributes, and containing the argument list only} \item Interrupt Signal (\cd{PInterrupt}): no content at all. \item Procedure Completed (\cd{PResult}) \begin{xcode} call_identifier runtime_in_milliseconds used_memory_in_bytes \end{xcode} And for referenced data (stored in CA), the following (called a cookie): \begin{xcode} call_identifier \end{xcode} \item Procedure Terminated (\cd{PTerminated}) \begin{xcode} call_identifier runtime_in_milliseconds used_memory_in_bytes Error_message \end{xcode} \end{itemize} \subsection*{Message exchange between client and server} This is mostly done using XML processing instructions. The initialisation is a sequence of messages as follows, where things like attribute order and format is strictly fixed. Examples: \begin{xcode} Server -> Client: Client -> Server: Server -> Client: \end{xcode} \begin{xcode} Server -> Client: Client -> Server: Server -> Client: OR JUST: \end{xcode} The actual data messages are enclosed in processing instruction blocks: \begin{xcode} ... message (OpenMath object), formats see above... \end{xcode} The exception is the interrupt signal, which is just SIGUSR2 to the server. Messages can be canceled using \verb!! to close the block. The server should not process the message. Sessions are terminated using \verb!!, optionally giving a reason as above. \begin{code} -- PIs as a data type: data SCSCP_PI = Init { piInitName :: String , piInitV :: String , piInitID :: String , piInitSCSCPs :: [String]} | Version String | Quit (Maybe String) | Start | End | Cancel | Other String -- catch-all... (for kant extensions) deriving (Eq,Show) piPrefix = " String writePI Start = piPrefix ++ " start ?>" writePI End = piPrefix ++ " end ?>" writePI Cancel = piPrefix ++ " cancel ?>" writePI (Quit Nothing) = piPrefix ++ " quit ?>" writePI (Quit (Just txt)) = piPrefix ++ " quit reason=" ++ show txt ++ " ?>" writePI (Version ver) = piPrefix ++ " version=" ++ show ver ++ " ?>" writePI (Init n v iD vs) = piPrefix ++ " service_name=" ++ show n ++ " service_version=" ++ show v ++ " service_id=" ++ show iD ++ " scscp_versions=" ++ show (unwords vs) ++ " ?>" writePI (Other msg) = piPrefix ++ ' ':msg ++ " ?>" parsePI :: String -> SCSCP_PI parsePI ('<':'?':'s':'c':'s':'c':'p':' ':more) = parse' (init ( init (dropWhile isSpace more))) parsePI other = error ("cannot parse this as a PI: " ++ other) {- -- libkant 4.0 uses these as well: -} parse' ('s':'t':'a':'r':'t':_) = Start parse' ('c':'a':'n':'c':'e':'l':_) = Cancel parse' ('e':'n':'d':_) = End parse' ('q':'u':'i':'t':rest) | all isSpace rest = Quit Nothing | otherwise = Quit text where text = case dropWhile isSpace rest of ('r':'e':'a':'s':'o':'n':'=':'\"':t) -> Just (takeWhile (not . isQ) t) other -> Nothing -- invalid PI, cannot parse. parse' ('v':'e':'r':'s':'i':'o':'n':'=':'\"':ver) = Version (takeWhile (not . isQ) ver) parse' ('s':'e':'r':'v':'i':'c':'e':'_':'n':'a':'m':'e':'=':'\"':rest) -- init message, we assume the attribute order given in the spec. = let (name,r2) = splitAndTrimWhen isQ rest -- drop "service_version=\"" r3 = tail' (dropWhile (not . isQ) r2) (v,r4) = splitAndTrimWhen isQ r3 -- drop "service_id=\"" r5 = tail' (dropWhile (not . isQ) r4) (iD,r6) = splitAndTrimWhen isQ r5 -- drop "scscp_versions=\"" r7 = tail' (dropWhile (not . isQ) r6) vList = words (takeWhile (not . isQ) r7) -- yes, I know this is the state monad. in Init name v iD vList -- parse' ('e':'r':'r':'o':'r':' ':'t':'e':'x':'t':'=':'\"':rest) -- = Other "error text =\"" ++ rest parse' other = Other other -- return anything as-is otherwise \end{code} \section*{Some string search and trim operations (helpers)} \begin{code} -- cut a string at the first char. for which a predicate holds, -- dropping the last character and trimming the rest of whitespaces. splitAndTrimWhen :: (Char -> Bool) -> String -> (String,String) splitAndTrimWhen pred "" = ("","") splitAndTrimWhen pred (c:cs) | pred c = ("",dropWhile isSpace cs) | otherwise = let (a,b) = splitAndTrimWhen pred cs in (c:a,b) tail' :: [a] -> [a] -- never fails tail' [] = [] tail' xs = tail xs isQ :: Char -> Bool isQ '\"' = True isQ '\'' = True isQ _ = False -- boyer-moore implementation. might be useful later. boyerMoore :: String -> String -> Maybe Int -- position of first match boyerMoore pattern searched = bm searched 0 where bm str i | null rest = if match == pattern then Just i else Nothing | and matching = Just i -- found it | otherwise = bm skipped (i+skip firstBad) where (match,rest) = splitAt l str matching = zipWith (==) pattern match firstBad = fst (last notMatched) notMatched = filter (not . snd) (zip match matching) skipped = drop (skip firstBad) str -- simplified, we only use the "bad-pattern" rule skip c = length (takeWhile (/= c) rPat) rPat = reverse pattern l = length pattern trim :: String -> String trim = dropWhile isSpace \end{code} \newpage \section*{Module overview of the Haskell SCSCP client} \begin{center} \includegraphics[width=0.9\textwidth]{modules} \end{center} The following modules are combined to the SCSCP API: \begin{itemize} \item[\cd{SCSCP\_DTD}] conversion from XML to an intermediate data type.\\ % This file is generated from \cd{SCSCP.dtd} via DTDtoHaskell (HaXml), and then retouched to avoid decomposing objects deeper than necessary. \item[\cd{Hs2SCSCP}] Conversion from Haskell structures to SCSCP XML and vice-versa. Uses the \cd{SCSCP\_DTD} as intermediate data. \item[\cd{HS\_SCSCP}] This file. Provides all SCSCP-relevant data types, no functionality. \item[\cd{SCSCP\_API}] server access functions and start/stop actions, functions for SCSCP evaluation. \end{itemize} \section*{System architecture intended for SymGrid-Par} \hwlcomment{A more detailed discussion of the SymGrid-Par design is given in my overview talk at the SCIEnce workshop in October.} \begin{center} \includegraphics[width=0.7\textwidth]{architecture1} \bigskip \includegraphics[width=0.7\textwidth]{architecture2} \end{center} \subsection*{Coordination Server} \hwlcomment{Documentation to be filled in} % The coordination server uses techniques which we have tested in % the SCSCP client, and completely rely on the \cd{Hs2SCSCP.hs} for % SCSCP. % ----------------------------------------------------------------------------- % TODO: check whether any of this is still relevant % \newpage % {\huge Junkyard: outdated material } % {\bf Notes on SCSCP specification and examples } % \begin{itemize} % \item error message data unclear: % The description of error messages (procedure terminated) % differs from the given examples, in that the examples % do not give runtime and memory. % The text says that these will be included in an % error message. % \item error messages under-specified in scscp1 CD and description: % The textual description of error messages categorises a few % error types, which is not reflected by the scscp1 CD and neither % the examples given later. % The following is missing from scscp1: % ``terminated by interrupt'', ``invalid cookie'', % ``ran out of resource(time)''. % The exact meaning of ``procedure not supported'' and % of ``cannot compute OM Object'' remains unclear. % \item invalid XML in example B.3: % Example B.3 shows an error message from the CA system % (actually, GAP) which contains literally % \verb!()!. As one can expect, this % causes an XML parse error (parsing tags named ``function'' % and ``argument'' instead of plain text). % The CA system should either wrap any unchecked text in % the OMSTR into \verb!<[[CDATA the-unchecked-text ]]>! or (better) % encode $<$ and $>$ as necessary for XML. % The website describing the scscp1 CD does the latter, % correctly. % Just a flaw in the example, or a problem of the gap server? % \item inconsistency between scscp1 CD and Spec. 1.2 examples: % The example for proc.terminated with a CA system error given in the % SCSCP-1.2.pdf uses \verb!name="error_CAS"!, whereas the scscp1 % CD specifies a \verb!error_system_specific!. % \item (Question?) isn't the call\_id mandatory, and thereby % OMATTR/OMATP a required content for any SCSCP message? % SCSCP-1.2.pdf, p.4 bottom, reads as if it was mandatory. % Examples for \verb!error_*! in scscp1 CD do not give any OMATP, % nor do they show the expected ``procedure\_terminated'' frame % inside which one will expect the error types to appear. % \end{itemize}