EtaMOO-0.2.0.0: src/MOO/Builtins/Values.hs
{-# LANGUAGE OverloadedStrings #-}
module MOO.Builtins.Values ( builtins ) where
import Control.Applicative ((<$>), (<*>))
import Control.Monad (mplus, unless, liftM, (>=>))
import Data.ByteString (ByteString)
import Data.Char (isDigit)
import Data.Digest.Pure.MD5 (MD5Digest)
import Data.Maybe (fromJust)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Word (Word8)
import Text.Printf (printf)
import qualified Data.ByteString as BS
import qualified Data.Digest.Pure.MD5 as MD5
import qualified Data.Vector as V
import qualified Data.Text as T
import MOO.Builtins.Common
import MOO.Builtins.Crypt
import MOO.Builtins.Match
import MOO.Parser (parseNum, parseObj)
import MOO.Task
import MOO.Types
import qualified MOO.String as Str
{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}
-- | § 4.4.2 Manipulating MOO Values
builtins :: [Builtin]
builtins = [
-- § 4.4.2.1 General Operations Applicable to all Values
bf_typeof
, bf_tostr
, bf_toliteral
, bf_toint
, bf_tonum
, bf_toobj
, bf_tofloat
, bf_equal
, bf_value_bytes
, bf_value_hash
-- § 4.4.2.2 Operations on Numbers
, bf_random
, bf_min
, bf_max
, bf_abs
, bf_floatstr
, bf_sqrt
, bf_sin
, bf_cos
, bf_tan
, bf_asin
, bf_acos
, bf_atan
, bf_sinh
, bf_cosh
, bf_tanh
, bf_exp
, bf_log
, bf_log10
, bf_ceil
, bf_floor
, bf_trunc
-- § 4.4.2.3 Operations on Strings
, bf_length
, bf_strsub
, bf_index
, bf_rindex
, bf_strcmp
, bf_decode_binary
, bf_encode_binary
, bf_match
, bf_rmatch
, bf_substitute
, bf_crypt
, bf_string_hash
, bf_binary_hash
-- § 4.4.2.4 Operations on Lists
, bf_is_member
, bf_listinsert
, bf_listappend
, bf_listdelete
, bf_listset
, bf_setadd
, bf_setremove
]
-- § 4.4.2.1 General Operations Applicable to all Values
bf_typeof = Builtin "typeof" 1 (Just 1) [TAny] TInt $ \[value] ->
return $ Int $ typeCode $ typeOf value
bf_tostr = Builtin "tostr" 0 Nothing [] TStr $
return . Str . Str.fromText . T.concat . map toText
bf_toliteral = Builtin "toliteral" 1 (Just 1) [TAny] TStr $ \[value] ->
return $ Str $ Str.fromText $ toLiteral value
-- XXX toint(" - 34 ") does not parse as -34
bf_toint = Builtin "toint" 1 (Just 1) [TAny] TInt $ \[value] -> toint value
where toint value = case value of
Int _ -> return value
Flt x | x >= 0 -> if x > fromIntegral (maxBound :: IntT)
then raise E_FLOAT else return (Int $ floor x)
| otherwise -> if x < fromIntegral (minBound :: IntT)
then raise E_FLOAT else return (Int $ ceiling x)
Obj x -> return (Int $ fromIntegral x)
Str x -> maybe (return $ Int 0) toint (parseNum $ Str.toText x)
Err x -> return (Int $ fromIntegral $ fromEnum x)
Lst _ -> raise E_TYPE
bf_tonum = bf_toint { builtinName = "tonum" }
bf_toobj = Builtin "toobj" 1 (Just 1) [TAny] TObj $ \[value] -> toobj value
where toobj value = case value of
Int x -> return (Obj $ fromIntegral x)
Flt x | x >= 0 -> if x > fromIntegral (maxBound :: ObjT)
then raise E_FLOAT else return (Obj $ floor x)
| otherwise -> if x < fromIntegral (minBound :: ObjT)
then raise E_FLOAT else return (Obj $ ceiling x)
Obj _ -> return value
Str x -> maybe (return $ Obj 0) toobj $
parseNum (Str.toText x) `mplus` parseObj (Str.toText x)
Err x -> return (Obj $ fromIntegral $ fromEnum x)
Lst _ -> raise E_TYPE
bf_tofloat = Builtin "tofloat" 1 (Just 1)
[TAny] TFlt $ \[value] -> tofloat value
where tofloat value = case value of
Int x -> return (Flt $ fromIntegral x)
Flt _ -> return value
Obj x -> return (Flt $ fromIntegral x)
Str x -> maybe (return $ Flt 0) tofloat (parseNum $ Str.toText x)
Err x -> return (Flt $ fromIntegral $ fromEnum x)
Lst _ -> raise E_TYPE
bf_equal = Builtin "equal" 2 (Just 2) [TAny, TAny] TInt $ \[value1, value2] ->
return $ truthValue (value1 `equal` value2)
bf_value_bytes = Builtin "value_bytes" 1 (Just 1) [TAny] TInt $ \[value] ->
return $ Int $ fromIntegral $ storageBytes value
bf_value_hash = Builtin "value_hash" 1 (Just 1) [TAny] TStr $
builtinFunction bf_toliteral >=>
builtinFunction bf_string_hash . return
-- § 4.4.2.2 Operations on Numbers
bf_random = Builtin "random" 0 (Just 1) [TInt] TInt $ \optional ->
let [Int mod] = defaults optional [Int maxBound]
in if mod < 1 then raise E_INVARG
else Int `liftM` random (1, mod)
bf_min = Builtin "min" 1 Nothing [TNum] TNum $ \args -> case args of
Int x:xs -> minMaxInt min x xs
Flt x:xs -> minMaxFlt min x xs
bf_max = Builtin "max" 1 Nothing [TNum] TNum $ \args -> case args of
Int x:xs -> minMaxInt max x xs
Flt x:xs -> minMaxFlt max x xs
minMaxInt :: (IntT -> IntT -> IntT) -> IntT -> [Value] -> MOO Value
minMaxInt f = go
where go x (Int y:rs) = go (f x y) rs
go x [] = return $ Int x
go _ _ = raise E_TYPE
minMaxFlt :: (FltT -> FltT -> FltT) -> FltT -> [Value] -> MOO Value
minMaxFlt f = go
where go x (Flt y:rs) = go (f x y) rs
go x [] = return $ Flt x
go _ _ = raise E_TYPE
bf_abs = Builtin "abs" 1 (Just 1) [TNum] TNum $ \[arg] -> case arg of
Int x -> return $ Int $ abs x
Flt x -> return $ Flt $ abs x
bf_floatstr = Builtin "floatstr" 2 (Just 3)
[TFlt, TInt, TAny] TStr $ \(Flt x : Int precision : optional) ->
let [scientific] = booleanDefaults optional [False]
prec = min precision 19
format = printf "%%.%d%c" prec $ if scientific then 'e' else 'f'
in if precision < 0 then raise E_INVARG
else return $ Str $ Str.fromString $ printf format x
floatBuiltin :: Id -> (FltT -> FltT) -> Builtin
floatBuiltin name f = Builtin name 1 (Just 1)
[TFlt] TFlt $ \[Flt x] -> checkFloat (f x)
bf_sqrt = floatBuiltin "sqrt" sqrt
bf_sin = floatBuiltin "sin" sin
bf_cos = floatBuiltin "cos" cos
bf_tan = floatBuiltin "tan" tan
bf_asin = floatBuiltin "asin" asin
bf_acos = floatBuiltin "acos" acos
bf_atan = Builtin "atan" 1 (Just 2) [TFlt, TFlt] TFlt $ \args ->
checkFloat $ case args of
[Flt y] -> atan y
[Flt y, Flt x] -> atan2 y x
bf_sinh = floatBuiltin "sinh" sinh
bf_cosh = floatBuiltin "cosh" cosh
bf_tanh = floatBuiltin "tanh" tanh
bf_exp = floatBuiltin "exp" exp
bf_log = floatBuiltin "log" log
bf_log10 = floatBuiltin "log10" (logBase 10)
bf_ceil = floatBuiltin "ceil" $ fromInteger . ceiling
bf_floor = floatBuiltin "floor" $ fromInteger . floor
bf_trunc = floatBuiltin "trunc" $ \x ->
fromInteger $ if x < 0 then ceiling x else floor x
-- § 4.4.2.3 Operations on Strings
bf_length = Builtin "length" 1 (Just 1) [TAny] TInt $ \[arg] -> case arg of
Str string -> return $ Int $ fromIntegral $ Str.length string
Lst list -> return $ Int $ fromIntegral $ V.length list
_ -> raise E_TYPE
bf_strsub = Builtin "strsub" 3 (Just 4) [TStr, TStr, TStr, TAny]
TStr $ \(Str subject : Str what : Str with : optional) ->
let [case_matters] = booleanDefaults optional [False]
caseFold str = if case_matters then str
else Str.fromText (Str.toCaseFold str)
-- XXX this won't work for Unicode in general
subs "" = []
subs subject = case Str.breakOn (caseFold what) (caseFold subject) of
(_, "") -> [subject]
(prefix, _) -> let (s, r) = Str.splitAt (Str.length prefix) subject
in s : with : subs (Str.drop whatLen r)
whatLen = Str.length what
in if Str.null what then raise E_INVARG
else return $ Str $ Str.concat $ subs subject
indexBuiltin :: Id -> (StrT -> IntT) -> (StrT -> StrT -> IntT) -> Builtin
indexBuiltin name nullCase mainCase =
Builtin name 2 (Just 3) [TStr, TStr, TAny]
TInt $ \(Str str1 : Str str2 : optional) ->
let [case_matters] = booleanDefaults optional [False]
caseFold str = if case_matters then str
else Str.fromText (Str.toCaseFold str)
-- XXX this won't work for Unicode in general
in return $ Int $ if Str.null str2 then nullCase str1
else mainCase (caseFold str2) (caseFold str1)
bf_index = indexBuiltin "index" nullCase mainCase
where nullCase = const 1
mainCase needle haystack = case Str.breakOn needle haystack of
(_, "") -> 0
(prefix, _) -> fromIntegral $ 1 + Str.length prefix
bf_rindex = indexBuiltin "rindex" nullCase mainCase
where nullCase haystack = fromIntegral $ Str.length haystack + 1
mainCase needle haystack = case Str.breakOnEnd needle haystack of
("", _) -> 0
(prefix, _) -> fromIntegral $
1 + Str.length prefix - Str.length needle
bf_strcmp = Builtin "strcmp" 2 (Just 2)
[TStr, TStr] TInt $ \[Str str1, Str str2] ->
return $ Int $ case Str.toText str1 `compare` Str.toText str2 of
LT -> -1
EQ -> 0
GT -> 1
bf_decode_binary = Builtin "decode_binary" 1 (Just 2)
[TStr, TAny] TLst $ \(Str bin_string : optional) ->
let [fully] = booleanDefaults optional [False]
mkResult | fully = fromListBy (Int . fromIntegral)
| otherwise = fromList . groupPrinting ("" ++)
in (mkResult . BS.unpack) `liftM` binaryString bin_string
where groupPrinting :: (String -> String) -> [Word8] -> [Value]
groupPrinting g (w:ws)
| Str.validChar c = groupPrinting (g [c] ++) ws
| null group = Int (fromIntegral w) : groupPrinting g ws
| otherwise = Str (Str.fromString group) :
Int (fromIntegral w) : groupPrinting ("" ++) ws
where c = toEnum (fromIntegral w)
group = g ""
groupPrinting g []
| null group = []
| otherwise = [Str $ Str.fromString group]
where group = g ""
bf_encode_binary = Builtin "encode_binary" 0 Nothing [] TStr $
liftM (Str . Str.fromBinary) . encodeBinary
encodeBinary :: [Value] -> MOO ByteString
encodeBinary = maybe (raise E_INVARG) (return . BS.pack) . encode
where encode :: [Value] -> Maybe [Word8]
encode (Int n : rest)
| n >= 0 && n <= 255 = (fromIntegral n :) <$> encode rest
| otherwise = Nothing
encode (Str s : rest) = (++) <$> encodeStr s <*> encode rest
encode (Lst v : rest) = (++) <$> encode (V.toList v) <*> encode rest
encode (_ : _ ) = Nothing
encode [] = Just []
encodeStr :: StrT -> Maybe [Word8]
encodeStr = mapM encodeChar . Str.toString
encodeChar :: Char -> Maybe Word8
encodeChar c
| n >= 0 && n <= 255 = Just (fromIntegral n)
| otherwise = Nothing
where n = fromEnum c
matchBuiltin :: Id -> (Regexp -> Text -> MatchResult) -> Builtin
matchBuiltin name matchFunc = Builtin name 2 (Just 3) [TStr, TStr, TAny]
TLst $ \(Str subject : Str pattern : optional) ->
let [case_matters] = booleanDefaults optional [False]
in runMatch matchFunc subject pattern case_matters
bf_match = matchBuiltin "match" match
bf_rmatch = matchBuiltin "rmatch" rmatch
runMatch :: (Regexp -> Text -> MatchResult) -> StrT -> StrT -> Bool -> MOO Value
runMatch match subject pattern caseMatters =
case Str.toRegexp caseMatters pattern of
Left (err, at) -> raiseException (Err E_INVARG)
(Str.fromString $ "Invalid pattern: " ++ err)
(Int $ fromIntegral at)
Right regexp -> case match regexp (Str.toText subject) of
MatchFailed -> return emptyList
MatchAborted -> raise E_QUOTA
MatchSucceeded offsets ->
let (m : offs) = offsets
(start, end) = convert m
replacements = repls 9 offs
in return $ fromList
[Int start, Int end, fromList replacements, Str subject]
where convert :: (Int, Int) -> (IntT, IntT)
convert (s, e) = (1 + fromIntegral s, fromIntegral e)
-- convert from 0-based open interval to 1-based closed one
repls :: Int -> [(Int, Int)] -> [Value]
repls n (r:rs) = let (s,e) = convert r
in fromList [Int s, Int e] : repls (n - 1) rs
repls n []
| n > 0 = fromList [Int 0, Int (-1)] : repls (n - 1) []
| otherwise = []
bf_substitute = Builtin "substitute" 2 (Just 2)
[TStr, TLst] TStr $ \[Str template, Lst subs] ->
case V.toList subs of
[Int start', Int end', Lst replacements', Str subject'] -> do
let start = fromIntegral start'
end = fromIntegral end'
subject = Str.toString subject'
subjectLen = Str.length subject'
valid s e = (s == 0 && e == -1) ||
(s > 0 && e >= s - 1 && e <= subjectLen)
substr start end =
let len = end - start + 1
in take len $ drop (start - 1) subject
substitution (Lst sub) = case V.toList sub of
[Int start', Int end'] -> do
let start = fromIntegral start'
end = fromIntegral end'
unless (valid start end) $ raise E_INVARG
return $ substr start end
_ -> raise E_INVARG
substitution _ = raise E_INVARG
unless (valid start end && V.length replacements' == 9) $
raise E_INVARG
replacements <- (substr start end :) `liftM`
mapM substitution (V.toList replacements')
let walk ('%':c:cs)
| isDigit c = let i = fromEnum c - fromEnum '0'
in (replacements !! i ++) `liftM` walk cs
| c == '%' = ("%" ++) `liftM` walk cs
| otherwise = raise E_INVARG
walk (c:cs) = ([c] ++) `liftM` walk cs
walk [] = return []
(Str . Str.fromString) `liftM` walk (Str.toString template)
_ -> raise E_INVARG
bf_crypt = Builtin "crypt" 1 (Just 2)
[TStr, TStr] TStr $ \(Str text : optional) ->
let (saltArg : _) = maybeDefaults optional
go salt = case crypt (Str.toString text) (Str.toString salt) of
Just encrypted -> return $ Str $ Str.fromString encrypted
Nothing -> raise E_QUOTA
in if maybe True invalidSalt saltArg
then generateSalt >>= go
else go $ fromStr $ fromJust saltArg
where invalidSalt (Str salt) = salt `Str.compareLength` 2 == LT
generateSalt = do
c1 <- randSaltChar
c2 <- randSaltChar
return $ Str.fromString [c1, c2]
randSaltChar = (saltStuff !!) `liftM` random (0, length saltStuff - 1)
saltStuff = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "./"
hash :: ByteString -> Value
hash bs = Str $ Str.fromString $ show md5hash
where md5hash = MD5.hash' bs :: MD5Digest
bf_string_hash = Builtin "string_hash" 1 (Just 1)
[TStr] TStr $ \[Str text] ->
return $ hash $ encodeUtf8 (Str.toText text)
bf_binary_hash = Builtin "binary_hash" 1 (Just 1)
[TStr] TStr $ \[Str bin_string] ->
hash `liftM` binaryString bin_string
-- § 4.4.2.4 Operations on Lists
-- bf_length already defined above
bf_is_member = Builtin "is_member" 2 (Just 2)
[TAny, TLst] TInt $ \[value, Lst list] ->
return $ Int $ maybe 0 (fromIntegral . succ) $
V.findIndex (`equal` value) list
bf_listinsert = Builtin "listinsert" 2 (Just 3)
[TLst, TAny, TInt] TLst $ \(Lst list : value : optional) ->
let [Int index] = defaults optional [Int 1]
in return $ Lst $ listInsert list (fromIntegral index - 1) value
bf_listappend = Builtin "listappend" 2 (Just 3)
[TLst, TAny, TInt] TLst $ \(Lst list : value : optional) ->
let [Int index] = defaults optional [Int $ fromIntegral $ V.length list]
in return $ Lst $ listInsert list (fromIntegral index) value
bf_listdelete = Builtin "listdelete" 2 (Just 2)
[TLst, TInt] TLst $ \[Lst list, Int index] ->
let index' = fromIntegral index
in if index' < 1 || index' > V.length list then raise E_RANGE
else return $ Lst $ listDelete list (index' - 1)
bf_listset = Builtin "listset" 3 (Just 3)
[TLst, TAny, TInt] TLst $ \[Lst list, value, Int index] ->
let index' = fromIntegral index
in if index' < 1 || index' > V.length list then raise E_RANGE
else return $ Lst $ listSet list (index' - 1) value
bf_setadd = Builtin "setadd" 2 (Just 2)
[TLst, TAny] TLst $ \[Lst list, value] ->
return $ Lst $ if value `V.elem` list then list else V.snoc list value
bf_setremove = Builtin "setremove" 2 (Just 2)
[TLst, TAny] TLst $ \[Lst list, value] ->
return $ Lst $ case V.elemIndex value list of
Nothing -> list
Just index -> listDelete list (fromIntegral index)