{-| 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