angle-0.8.1.0: src/Angle/Exec/Builtins.hs
{-|
Module : Angle.Exec.Builtins
Description : Builtin language functions and variables.
Copyright : (c) Ben Moon, 2015
License : GPL-3
Maintainer : guiltydolphin@gmail.com
Stability : experimental
Portability : POSIX
This module defines and exports functions and variables that can be used in Angle.
Minutae:
A function beginning with @builtin@ (e.g., @builtinPrint@) defines a builtin function accessible through the language by the section following @builtin@ (although the casing may differ). Thus @builtinPrint@ defines the builtin @print@ function.
The builtin @someFunction@ function, the @someFunction@ builtin, all refer to @someFunction@ that is callable from the language.
Typically, builtin functions can act upon either a set amount
of arguments or a list of arguments, wherein the behaviour
is similar to the standard call, but produces a list of
results rather than a singleton.
For example, the @isNull@ builtin produces a single boolean
value when passed one argument, but produces a list of boolean
values when passed multiple arguments - as is common with
predicate functions in Angle.
-}
module Angle.Exec.Builtins
( isBuiltin
, builtinPrint
, builtinStr
, builtinIndex
, builtinLength
, builtinAsType
, builtinGetArgs
, builtinInput
, builtinIsNull
, builtinOpen
, builtinRead
, builtinRound
, builtinWrite
, builtinClose
, builtinShell
, argsToString
-- ** Assignment handling
, handleBuiltinAssignFun
, handleBuiltinAssignLit
-- ** Default environments
, startEnv
, initialEnvNotMain
, initialEnvMain
) where
import Control.Monad
import Control.Monad.State
import System.Environment
import System.IO ( hFlush
, hGetLine
, hGetContents
, hGetChar
, hPutStr
, hClose
, stdout
, stdin
, stderr
, openFile
, IOMode(..))
import System.IO.Error ( tryIOError
, isAlreadyExistsError
, isDoesNotExistError
, isAlreadyInUseError
, isFullError
, isEOFError
, isIllegalOperation
, isPermissionError)
import System.Process (readProcess)
import Angle.Exec.Error
import Angle.Exec.Types
import Angle.Types.Lang
import Angle.Types.Scope
emptyArgs :: ArgSig
emptyArgs = ArgSig { stdArgs=[], catchAllArg=Nothing }
builtinCallSig :: LangIdent -> Lambda
builtinCallSig name =
Lambda
{ lambdaArgs = emptyArgs
{ catchAllArg = Just (CatchArg (LangIdent "x") Nothing) }
, lambdaBody = body
, lambdaScope = Nothing
}
where body = SingleStmt
( StmtExpr
( ExprFunCall name False
[ ExprParamExpand
( LangIdent "x" )])) startRef
builtinVar :: LangIdent -> (LangIdent, VarVal Lambda)
builtinVar name = (name, VarVal
{ varDef = Just $ builtinCallSig name
, varBuiltin = True })
builtinsVars :: BindEnv LangIdent Lambda
builtinsVars = bindEnvFromList $
map (builtinVar . LangIdent) builtins
builtinValue :: LangIdent -> LangLit -> (LangIdent, VarVal LangLit)
builtinValue name val = (name, VarVal
{ varDef = Just val
, varBuiltin = True })
builtinsValues :: BindEnv LangIdent LangLit
builtinsValues = bindEnvFromList $
map (\(x,y) -> builtinValue (LangIdent x) y) builtinValues
-- | Starting environment with builtin functions defined.
startEnv :: Env
startEnv = basicEnv { currentScope = startScope }
startScope :: Scope
startScope = emptyScope
{ lambdaBindings = builtinsVars
, valueBindings = builtinsValues
}
-- | Starting environment for programs not running as main.
initialEnvNotMain :: Env
initialEnvNotMain = startEnv { runAsMain = False }
-- | Starting environment for programs running as main.
initialEnvMain :: Env
initialEnvMain = startEnv { runAsMain = True }
-- | Builtin file handles
builtinHandles :: [(String, LangLit)]
builtinHandles = [ ("stdin", LitHandle stdin)
, ("stdout", LitHandle stdout)
, ("stderr", LitHandle stderr)
]
builtinVariables :: [(String, LangLit)]
builtinVariables = [ ("as_constr", LitBool False)
, ("main", LitBool False)
]
-- | True if the identifier represents a builtin function.
isBuiltin :: LangIdent -> Bool
isBuiltin = (`elem`builtins) . getIdent
-- | List of the builtin functions.
builtins :: [String]
builtins = [ "print", "str"
, "index", "length"
, "input", "eval"
, "isNull"
, "asType", "getArgs"
, "read", "write", "open", "close"
, "include"
, "local", "nonlocal", "global"
, "shell"
, "round"]
-- | List of builtin variables and their values.
builtinValues :: [(String, LangLit)]
builtinValues = builtinHandles ++ builtinVariables
-- | Builtin @print@ function.
--
-- @print(..x)@: converts each element of @..x@ to a string
-- before printing the concatenated result followed by a newline to STDOUT.
builtinPrint :: [LangLit] -> ExecIO LangLit
builtinPrint xs = withIOError (putStrLn res) >> returnVal (LitStr res)
where res = argsToString xs
-- | Builtin @input@ function.
--
-- @input(..x)@: does the same as @builtinPrint@, but does
-- not automatically append a newline and returns the result
-- from STDIN.
builtinInput :: [LangLit] -> ExecIO LangLit
builtinInput xs = liftM LitStr (withIOError $ putStr res >> hFlush stdout >> getLine) >>= returnVal
where res = argsToString xs
-- | Convert arguments to a string.
argsToString :: [LangLit] -> String
argsToString = concatMap (\x -> case x of
(LitStr s) -> s
_ -> showSyn x)
-- | Builtin @asType@ function.
--
-- @asType(typ, x)@ deduces the type of @typ@ and attempts to
-- cast @x@ to that type.
--
-- @asType(typ, ..x)@ produces a list of casted values.
builtinAsType :: [LangLit] -> ExecIO LangLit
builtinAsType [x] = return x
builtinAsType [x,y] = asType x y
builtinAsType (x:xs) = liftM LitList $ mapM (asType x) xs
builtinAsType _ = throwExecError $ callBuiltinErr "asType: invalid call"
asType :: LangLit -> LangLit -> ExecIO LangLit
asType x y | typeOf x == typeOf y = return y
asType (LitStr _) (LitList xs)
| all isLitChar xs = return . LitStr $ map (\(LitChar x) -> x) xs
where isLitChar (LitChar _) = True
isLitChar _ = False
asType (LitStr _) x = return . toLitStr $ x
asType (LitFloat _) (LitInt x) = return . LitFloat $ fromIntegral x
asType (LitFloat _) (LitStr y) = fromStr y LitFloat
asType (LitInt _) (LitStr y) = fromStr y LitInt
asType (LitList _) (LitStr xs) = return . LitList $ map LitChar xs
asType (LitBool _) (LitStr y) =
case y of
"true" -> return $ LitBool True
"false" -> return $ LitBool False
_ -> return LitNull
asType (LitList _) x@(LitRange _ (Just _) _) = iterToLit x
asType (LitList _) (LitRange _ Nothing _) = throwExecError infiniteRangeErr
asType x y = throwExecError $ typeCastErr (typeOf y) (typeOf x)
fromStr :: (Read a) => String -> (a -> LangLit) -> ExecIO LangLit
fromStr s f = case reads s of
[(r,"")] -> return $ f r
_ -> throwExecError . readErr $ s
-- | Builtin @length@ function.
--
-- @length(x:\@list)@ returns the number of elements in the list.
--
-- @length(x:\@range)@ returns the number of elements that would be
-- produced by the range if the range is finite, otherwise the
-- keyword @:infinite@.
builtinLength :: [LangLit] -> ExecIO LangLit
builtinLength [LitList xs] = return . LitInt $ length xs
builtinLength [x@(LitRange{})] | isInfiniteRange x = return $ LitKeyword $ LangIdent "infinite"
builtinLength [LitRange x (Just y) Nothing] = return . LitInt $ (fromEnumL y + 1) - fromEnumL x
builtinLength [LitRange x (Just y) (Just z)] = return .
LitInt $ ceiling ((fromIntegral div1 / fromIntegral div2) :: Double)
where
div1 = (fromEnumL y + 1) - fromEnumL x
div2 = fromEnumL z - fromEnumL x
builtinLength _ = throwExecError $ callBuiltinErr "length: invalid call"
-- | Builtin @isNull@ function.
--
-- @isNull(x)@ returns @true@ if the passed value is the null literal.
--
-- @isNull(..x)@ returns a list of the above.
builtinIsNull :: [LangLit] -> ExecIO LangLit
builtinIsNull [x] = return . LitBool $ isNull x
builtinIsNull xs = return . LitList $ map (LitBool . isNull) xs
-- | Builtin @str@ function.
--
-- @str(x)@ is the same as @asType("", x)@
builtinStr :: [LangLit] -> ExecIO LangLit
builtinStr [] = return $ LitStr ""
builtinStr xs | length xs > 1 = throwExecError $ wrongNumberOfArgumentsErr 1 (length xs)
| otherwise = return $ toLitStr (head xs)
-- | Builtin @index@ function.
--
-- @index(x:\@int, xs:\@list)@: retrieve element at index @x@ from @xs@
--
-- @index(x:\@int, y:\@int, xs:\@list)@: return a list of elements that lie between index @x@ and index @y@ of @xs@.
--
-- Negative indices are treated as working backwards from the
-- end of the list. With @-1@ being the last element.
builtinIndex :: [LangLit] -> ExecIO LangLit
builtinIndex [LitInt x,LitList xs]
| x >= length xs = throwExecError $ indexOutOfBoundsErr x
| x < 0 = return $ xs !! (length xs + x)
| otherwise = return $ xs !! x
builtinIndex [x, LitStr s] = builtinIndex [x,LitList $ map LitChar s] >>= joinList
where
joinList l = builtinAsType [LitStr "", l]
builtinIndex [LitInt x,LitInt y,LitList xs]
| x > length xs || y > length xs
= throwExecError $ indexOutOfBoundsErr x
| x < 0 = builtinIndex
[LitInt (length xs + x), LitInt y, LitList xs]
| y < 0 = builtinIndex
[LitInt x, LitInt (length xs + y + 1), LitList xs]
| otherwise
= return . LitList $ splice x y xs
builtinIndex [x,y,LitStr s] = builtinIndex [x, y, LitList $ map LitChar s] >>= joinList
where
joinList l = builtinAsType [LitStr "", l]
builtinIndex _ = throwExecError $ callBuiltinErr "index: invalid call signature"
splice :: Int -> Int -> [a] -> [a]
splice x y = take (y-x) . drop x
toLitStr :: LangLit -> LangLit
toLitStr x@(LitStr _) = x
toLitStr (LitChar x) = LitStr [x]
toLitStr LitNull = LitStr ""
toLitStr x = LitStr $ showSyn x
-- | Builtin @getArgs@ function.
--
-- @getArgs()@ returns the arguments passed to the program.
builtinGetArgs :: [LangLit] -> ExecIO LangLit
builtinGetArgs _ = liftM (LitList . map LitStr) $ withIOError getArgs
-- | Builtin @open@ function.
--
-- @open(file, mode)@ returns a file handle for @file@ in @mode@.
--
-- Modes are as follows:
--
-- [<] read only mode
--
-- [>] write mode (clobbers)
--
-- [>>] append mode
--
-- [<>] read-write mode
builtinOpen :: [LangLit] -> ExecIO LangLit
builtinOpen [LitStr fn, LitStr "<"] = liftM LitHandle $ withIOError $ openFile fn ReadMode
builtinOpen [LitStr fn, LitStr ">"] = liftM LitHandle $ withIOError $ openFile fn WriteMode
builtinOpen [LitStr fn, LitStr ">>"] = liftM LitHandle $ withIOError $ openFile fn AppendMode
builtinOpen [LitStr fn, LitStr "<>"] = liftM LitHandle $ withIOError $ openFile fn ReadWriteMode
builtinOpen _ = throwExecError $ callBuiltinErr "open: invalid call signature"
-- | Builtin @read@ function.
--
-- @read(handle)@ returns the unread part of the characters managed by @handle@.
--
-- @read(handle, integer)@ reads @integer@ lines from @handle@ and returns them as a new-line separated string.
--
-- @read(:char, handle)@ reads a single character from @handle@ and returns it as a character.
--
-- @read(:char, handle, integer)@ reads @integer@ characters from @handle@ and returns them in a string.
builtinRead :: [LangLit] -> ExecIO LangLit
builtinRead [LitHandle h] = liftM LitStr $ withIOError $ hGetContents h
builtinRead [LitHandle h, LitInt n] = liftM LitStr (withIOError $ liftM unlines $ replicateM n $ hGetLine h) >>= returnVal
builtinRead [LitKeyword (LangIdent "char"), LitHandle h] = liftM LitChar (withIOError $ hGetChar h) >>= returnVal
builtinRead [LitKeyword (LangIdent "char"), LitHandle h, LitInt n] = liftM LitStr (withIOError $ replicateM n $ hGetChar h) >>= returnVal
builtinRead (s@(LitStr _):xs) = builtinOpen [s, LitStr "<"] >>= (builtinRead . (:xs))
builtinRead _ = throwExecError $ callBuiltinErr "read: invalid call signature"
-- | Builtin @write@ function.
--
-- @write(handle, string)@ writes @string@ to @handle@.
builtinWrite :: [LangLit] -> ExecIO LangLit
builtinWrite [LitHandle h, l@(LitStr s)] = withIOError (hPutStr h s) >> return l;
builtinWrite [h@(LitStr _), m@(LitStr _), l@(LitStr _)] = builtinOpen [h, m] >>= (builtinWrite . (:[l]))
builtinWrite _ = throwExecError $ callBuiltinErr "write: invalid call signature"
-- | Builtin @close@ function.
--
-- @close(handle)@ closes @handle@ for reading and writing.
builtinClose :: [LangLit] -> ExecIO LangLit
builtinClose xs = mapM_ bClose xs >> return LitNull
where
bClose (LitHandle x) = withIOError $ hClose x
bClose x = throwExecError $ typeUnexpectedErr (typeOf x) LTHandle
-- | Builtin @shell@ function.
--
-- @shell(executable, arguments, stdin)@ runs the shell command
-- @executable@ with @args@ and @stdin@ and returns the result in a
-- string.
--
-- @shell(executable) = shell(executable, [], "")@
--
-- @shell(executable, arguments) = shell(executable, arguments, "")@
--
-- @shell(executable, stdin) = shell(executable, [], stdin)@
builtinShell :: [LangLit] -> ExecIO LangLit
builtinShell [p@(LitStr _)] = builtinShell [p, LitList [], LitStr ""]
builtinShell [p@(LitStr _), l@(LitList _)] = builtinShell [p, l, LitStr ""]
builtinShell [p@(LitStr _), sIn@(LitStr _)] = builtinShell [p, LitList [], sIn]
builtinShell [LitStr p, LitList args, LitStr sIn] = liftM LitStr $ withIOError $ readProcess p xs sIn
where xs = map ((\(LitStr x) -> x) . toLitStr) args
builtinShell _ = throwExecError $ callBuiltinErr "shell: invalid call signature"
-- | Builtin @round@ function.
--
-- @round(num)@ returns the closest integer to @num@ (prefers higher
-- numbers).
builtinRound :: [LangLit] -> ExecIO LangLit
builtinRound [x@(LitInt _)] = return x
builtinRound [LitFloat x] = return $ LitInt $ round x
builtinRound xs = liftM LitList $ mapM (builtinRound . (:[])) xs
builtinRound [x] = throwExecError $ typeNotValidErr x
-- | Handler for assignments to builtin variables as literals.
handleBuiltinAssignLit :: LangIdent -> LangLit -> ExecIO a
handleBuiltinAssignLit n@(LangIdent "main") _ = throwExecError $ assignToBuiltinErr n (Just "assigned by execution program")
handleBuiltinAssignLit n _ = throwExecError $ assignToBuiltinErr n Nothing
-- | Handler for assignments to builtin variables as functions.
handleBuiltinAssignFun :: LangIdent -> Lambda -> ExecIO a
handleBuiltinAssignFun n _ = throwExecError $ assignToBuiltinErr n Nothing
handleIOError :: IOError -> ExecIO a
handleIOError e = throwExecError $ err e
where
err | isAlreadyExistsError e = alreadyExistsErr
| isDoesNotExistError e = doesNotExistErr
| isAlreadyInUseError e = alreadyInUseErr
| isFullError e = deviceFullErr
| isEOFError e = eofErr
| isIllegalOperation e = illegalOperationErr
| isPermissionError e = permissionErr
| otherwise = error "Cannot handle user io exceptions"
-- | Executes code in IO but attempts to embed the exception into
-- Angle's error system.
withIOError :: IO a -> ExecIO a
withIOError x = do
r <- liftIO $ tryIOError x
case r of
Right res -> return res
Left e -> handleIOError e