EtaMOO-0.1.0.0: src/MOO/Builtins/Values.hs
{-# LANGUAGE OverloadedStrings, ForeignFunctionInterface #-}
module MOO.Builtins.Values ( builtins ) where
import Control.Monad (mplus, unless, liftM)
import Control.Exception (bracket)
import Control.Concurrent.MVar (MVar, newMVar, takeMVar, putMVar)
import System.IO.Unsafe (unsafePerformIO)
import Text.Printf (printf)
import Foreign.C (CString, withCString, peekCString)
import Data.Maybe (fromJust)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.ByteString (ByteString)
import Data.Char (intToDigit, isDigit)
import qualified Data.Text as T
import qualified Data.Vector as V
import qualified Data.Vector.Mutable as VM
import Data.Digest.Pure.MD5 (MD5Digest)
import qualified Data.Digest.Pure.MD5 as MD5
import MOO.Types
import MOO.Task
import MOO.Parser (parseNum, parseObj)
import MOO.Builtins.Common
import MOO.Builtins.Match
{-# ANN module ("HLint: ignore Use camelCase" :: String) #-}
-- | § 4.4.2 Manipulating MOO Values
builtins :: [BuiltinSpec]
builtins = [
("typeof" , (bf_typeof , Info 1 (Just 1) [TAny] TInt))
, ("tostr" , (bf_tostr , Info 0 Nothing [] TStr))
, ("toliteral" , (bf_toliteral , Info 1 (Just 1) [TAny] TStr))
, ("toint" , (bf_toint , Info 1 (Just 1) [TAny] TInt))
, ("tonum" , fromJust $ lookup "toint" builtins)
, ("toobj" , (bf_toobj , Info 1 (Just 1) [TAny] TObj))
, ("tofloat" , (bf_tofloat , Info 1 (Just 1) [TAny] TFlt))
, ("equal" , (bf_equal , Info 2 (Just 2) [TAny, TAny] TInt))
, ("value_bytes" , (bf_value_bytes , Info 1 (Just 1) [TAny] TInt))
, ("value_hash" , (bf_value_hash , Info 1 (Just 1) [TAny] TStr))
, ("random" , (bf_random , Info 0 (Just 1) [TInt] TInt))
, ("min" , (bf_min , Info 1 Nothing [TNum] TNum))
, ("max" , (bf_max , Info 1 Nothing [TNum] TNum))
, ("abs" , (bf_abs , Info 1 (Just 1) [TNum] TNum))
, ("floatstr" , (bf_floatstr , Info 2 (Just 3) [TFlt, TInt,
TAny] TStr))
, ("sqrt" , (bf_sqrt , Info 1 (Just 1) [TFlt] TFlt))
, ("sin" , (bf_sin , Info 1 (Just 1) [TFlt] TFlt))
, ("cos" , (bf_cos , Info 1 (Just 1) [TFlt] TFlt))
, ("tan" , (bf_tan , Info 1 (Just 1) [TFlt] TFlt))
, ("asin" , (bf_asin , Info 1 (Just 1) [TFlt] TFlt))
, ("acos" , (bf_acos , Info 1 (Just 1) [TFlt] TFlt))
, ("atan" , (bf_atan , Info 1 (Just 2) [TFlt, TFlt] TFlt))
, ("sinh" , (bf_sinh , Info 1 (Just 1) [TFlt] TFlt))
, ("cosh" , (bf_cosh , Info 1 (Just 1) [TFlt] TFlt))
, ("tanh" , (bf_tanh , Info 1 (Just 1) [TFlt] TFlt))
, ("exp" , (bf_exp , Info 1 (Just 1) [TFlt] TFlt))
, ("log" , (bf_log , Info 1 (Just 1) [TFlt] TFlt))
, ("log10" , (bf_log10 , Info 1 (Just 1) [TFlt] TFlt))
, ("ceil" , (bf_ceil , Info 1 (Just 1) [TFlt] TFlt))
, ("floor" , (bf_floor , Info 1 (Just 1) [TFlt] TFlt))
, ("trunc" , (bf_trunc , Info 1 (Just 1) [TFlt] TFlt))
, ("length" , (bf_length , Info 1 (Just 1) [TAny] TInt))
, ("strsub" , (bf_strsub , Info 3 (Just 4) [TStr, TStr,
TStr, TAny] TStr))
, ("index" , (bf_index , Info 2 (Just 3) [TStr, TStr,
TAny] TInt))
, ("rindex" , (bf_rindex , Info 2 (Just 3) [TStr, TStr,
TAny] TInt))
, ("strcmp" , (bf_strcmp , Info 2 (Just 2) [TStr, TStr] TInt))
, ("decode_binary" , (bf_decode_binary , Info 1 (Just 2) [TStr, TAny] TLst))
, ("encode_binary" , (bf_encode_binary , Info 0 Nothing [] TStr))
, ("match" , (bf_match , Info 2 (Just 3) [TStr, TStr,
TAny] TLst))
, ("rmatch" , (bf_rmatch , Info 2 (Just 3) [TStr, TStr,
TAny] TLst))
, ("substitute" , (bf_substitute , Info 2 (Just 2) [TStr, TLst] TStr))
, ("crypt" , (bf_crypt , Info 1 (Just 2) [TStr, TStr] TStr))
, ("string_hash" , (bf_string_hash , Info 1 (Just 1) [TStr] TStr))
, ("binary_hash" , (bf_binary_hash , Info 1 (Just 1) [TStr] TStr))
, ("is_member" , (bf_is_member , Info 2 (Just 2) [TAny, TLst] TInt))
, ("listinsert" , (bf_listinsert , Info 2 (Just 3) [TLst, TAny,
TInt] TLst))
, ("listappend" , (bf_listappend , Info 2 (Just 3) [TLst, TAny,
TInt] TLst))
, ("listdelete" , (bf_listdelete , Info 2 (Just 2) [TLst, TInt] TLst))
, ("listset" , (bf_listset , Info 3 (Just 3) [TLst, TAny,
TInt] TLst))
, ("setadd" , (bf_setadd , Info 2 (Just 2) [TLst, TAny] TLst))
, ("setremove" , (bf_setremove , Info 2 (Just 2) [TLst, TAny] TLst))
]
-- § 4.4.2.1 General Operations Applicable to all Values
bf_typeof [value] = return $ Int $ typeCode $ typeOf value
bf_tostr values = return $ Str $ T.concat $ map toText values
bf_toliteral [value] = return $ Str $ toLiteral value
-- XXX toint(" - 34 ") does not parse as -34
bf_toint [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 x)
Err x -> return (Int $ fromIntegral $ fromEnum x)
Lst _ -> raise E_TYPE
bf_toobj [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 x `mplus` parseObj x
Err x -> return (Obj $ fromIntegral $ fromEnum x)
Lst _ -> raise E_TYPE
bf_tofloat [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 x)
Err x -> return (Flt $ fromIntegral $ fromEnum x)
Lst _ -> raise E_TYPE
bf_equal [value1, value2] = return $ truthValue (value1 `equal` value2)
bf_value_bytes [value] = return $ Int $ fromIntegral $ storageBytes value
bf_value_hash [value] = do
literal <- bf_toliteral [value]
bf_string_hash [literal]
-- § 4.4.2.2 Operations on Numbers
bf_random optional
| mod <= 0 = raise E_INVARG
| otherwise = Int `liftM` random (1, mod)
where [Int mod] = defaults optional [Int maxBound]
bf_min (Int x:xs) = minMaxInt min x xs
bf_min (Flt x:xs) = minMaxFlt min x xs
bf_max (Int x:xs) = minMaxInt max x xs
bf_max (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 [Int x] = return $ Int $ abs x
bf_abs [Flt x] = return $ Flt $ abs x
bf_floatstr (Flt x : Int precision : optional)
| precision < 0 = raise E_INVARG
| otherwise = return $ Str $ T.pack $ printf format x
where [scientific] = booleanDefaults optional [False]
prec = min precision 19
format = printf "%%.%d%c" prec $ if scientific then 'e' else 'f'
bf_sqrt [Flt x] = checkFloat $ sqrt x
bf_sin [Flt x] = checkFloat $ sin x
bf_cos [Flt x] = checkFloat $ cos x
bf_tan [Flt x] = checkFloat $ tan x
bf_asin [Flt x] = checkFloat $ asin x
bf_acos [Flt x] = checkFloat $ acos x
bf_atan [Flt y] = checkFloat $ atan y
bf_atan [Flt y,
Flt x] = checkFloat $ atan2 y x
bf_sinh [Flt x] = checkFloat $ sinh x
bf_cosh [Flt x] = checkFloat $ cosh x
bf_tanh [Flt x] = checkFloat $ tanh x
bf_exp [Flt x] = checkFloat $ exp x
bf_log [Flt x] = checkFloat $ log x
bf_log10 [Flt x] = checkFloat $ logBase 10 x
bf_ceil [Flt x] = checkFloat $ fromIntegral (ceiling x :: Integer)
bf_floor [Flt x] = checkFloat $ fromIntegral (floor x :: Integer)
bf_trunc [Flt x]
| x < 0 = checkFloat $ fromIntegral (ceiling x :: Integer)
| otherwise = checkFloat $ fromIntegral (floor x :: Integer)
-- § 4.4.2.3 Operations on Strings
bf_length [Str string] = return $ Int $ fromIntegral $ T.length string
bf_length [Lst list] = return $ Int $ fromIntegral $ V.length list
bf_length _ = raise E_TYPE
bf_strsub (Str subject : Str what : Str with : optional)
| T.null what = raise E_INVARG
| otherwise = return $ Str $ T.concat $ subs subject
where [case_matters] = booleanDefaults optional [False]
caseFold str = if case_matters then str else T.toCaseFold str
-- this won't work for Unicode in general
subs "" = []
subs subject = case T.breakOn what' (caseFold subject) of
(_, "") -> [subject]
(prefix, _) -> let (s, r) = T.splitAt (T.length prefix) subject
in s : with : subs (T.drop whatLen r)
what' = caseFold what
whatLen = T.length what
bf_index (Str str1 : Str str2 : optional)
| T.null str2 = return (Int 1)
| otherwise =
return $ Int $ case T.breakOn (caseFold str2) (caseFold str1) of
(_, "") -> 0
(prefix, _) -> fromIntegral $ 1 + T.length prefix
where [case_matters] = booleanDefaults optional [False]
caseFold str = if case_matters then str else T.toCaseFold str
-- this won't work for Unicode in general
bf_rindex (Str str1 : Str str2 : optional)
| T.null str2 = return (Int $ fromIntegral $ T.length str1 + 1)
| otherwise =
return $ Int $ case T.breakOnEnd needle haystack of
("", _) -> 0
(prefix, _) -> fromIntegral $ 1 + T.length prefix - T.length needle
where [case_matters] = booleanDefaults optional [False]
needle = caseFold str2
haystack = caseFold str1
caseFold str = if case_matters then str else T.toCaseFold str
-- this won't work for Unicode in general
bf_strcmp [Str str1, Str str2] =
return $ Int $ case compare str1 str2 of
LT -> -1
EQ -> 0
GT -> 1
bf_decode_binary (Str bin_string : optional) =
maybe (raise E_INVARG) (return . mkResult) $ text2binary bin_string
where [fully] = booleanDefaults optional [False]
mkResult | fully = fromListBy (Int . fromIntegral)
| otherwise = fromList . groupPrinting ("" ++)
groupPrinting g (w:ws)
| validStrChar c = groupPrinting (g [c] ++) ws
| null group = Int (fromIntegral w) : groupPrinting g ws
| otherwise = Str (T.pack group) : Int (fromIntegral w) :
groupPrinting ("" ++) ws
where c = toEnum (fromIntegral w)
group = g ""
groupPrinting g []
| null group = []
| otherwise = [Str $ T.pack group]
where group = g ""
bf_encode_binary = liftM (Str . T.pack) . encodeBinary
encodeBinary :: [Value] -> MOO String
encodeBinary (Int n : args)
| n >= 0 && n <= 255 = prepend `liftM` encodeBinary args
| otherwise = raise E_INVARG
where c = toEnum n'
n' = fromIntegral n
prepend | validStrChar c &&
c /= '\t' = (c :)
| otherwise = \r -> '~' : hex (n' `div` 16)
: hex (n' `mod` 16) : r
hex = intToDigit -- N.B. not uppercase
encodeBinary (Str str : args) = (encodeStr (T.unpack str) ++) `liftM`
encodeBinary args
where encodeStr ('~' :cs) = "~7e" ++ encodeStr cs
encodeStr ('\t':cs) = "~09" ++ encodeStr cs
encodeStr (c :cs) = c : encodeStr cs
encodeStr "" = ""
encodeBinary (Lst list : args) = do
listEncoding <- encodeBinary (V.toList list)
(listEncoding ++) `liftM` encodeBinary args
encodeBinary (_:_) = raise E_INVARG
encodeBinary [] = return ""
bf_match (Str subject : Str pattern : optional) =
runMatch match subject pattern case_matters
where [case_matters] = booleanDefaults optional [False]
bf_rmatch (Str subject : Str pattern : optional) =
runMatch rmatch subject pattern case_matters
where [case_matters] = booleanDefaults optional [False]
runMatch :: (Regexp -> Text -> IO MatchResult) ->
StrT -> StrT -> Bool -> MOO Value
runMatch match subject pattern case_matters = do
let compiled = unsafePerformIO $ newRegexp pattern case_matters
case compiled of
Left (err, at) -> raiseException $ Exception (Err E_INVARG)
(T.pack $ "Invalid pattern: " ++ err)
(Int $ fromIntegral at)
Right regexp -> do
let result = unsafePerformIO $ match regexp subject
case result of
MatchFailed -> return (Lst V.empty)
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 from 0-based open interval to 1-based closed one
convert (s,e) = (1 + fromIntegral s, fromIntegral e)
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 [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 = T.unpack subject'
subjectLen = T.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 . T.pack) `liftM` walk (T.unpack template)
_ -> raise E_INVARG
foreign import ccall unsafe "static unistd.h crypt"
c_crypt :: CString -> CString -> IO CString
{-# ANN crypt ("HLint: ignore Use >=>" :: String) #-}
crypt :: String -> String -> String
crypt key salt =
unsafePerformIO $ bracket (takeMVar cryptLock) (putMVar cryptLock) $ \_ ->
withCString key $ \c_key ->
withCString salt $ \c_salt ->
c_crypt c_key c_salt >>= peekCString
cryptLock :: MVar ()
cryptLock = unsafePerformIO $ newMVar ()
{-# NOINLINE cryptLock #-}
bf_crypt (Str text : optional)
| maybe True invalidSalt saltArg = generateSalt >>= go
| otherwise = go $ fromStr $ fromJust saltArg
where (saltArg : _) = maybeDefaults optional
invalidSalt (Str salt) = salt `T.compareLength` 2 == LT
generateSalt = do
c1 <- randSaltChar
c2 <- randSaltChar
return $ T.pack [c1, c2]
randSaltChar = (saltStuff !!) `liftM` random (0, length saltStuff - 1)
saltStuff = ['a'..'z'] ++ ['A'..'Z'] ++ ['0'..'9'] ++ "./"
go salt = return $ Str $ T.pack $ crypt (T.unpack text) (T.unpack salt)
hash :: ByteString -> Value
hash bs = Str $ T.pack $ show md5hash
where md5hash = MD5.hash' bs :: MD5Digest
bf_string_hash [Str text] = return $ hash $ encodeUtf8 text
bf_binary_hash [Str bin_string] = hash `liftM` binaryString bin_string
-- § 4.4.2.4 Operations on Lists
-- bf_length already defined above
bf_is_member [value, Lst list] =
return $ Int $ maybe 0 (fromIntegral . succ) $
V.findIndex (`equal` value) list
listInsert :: LstT -> Int -> Value -> LstT
listInsert list index value
| index <= 0 = V.cons value list
| index > listLen = V.snoc list value
| otherwise = V.create $ do
list' <- V.thaw list >>= flip VM.grow 1
let moveLen = listLen - index
s = VM.slice index moveLen list'
t = VM.slice (index + 1) moveLen list'
VM.move t s
VM.write list' index value
return list'
where listLen = V.length list
listDelete :: LstT -> Int -> LstT
listDelete list index
| index == 0 = V.create $ VM.tail `liftM` V.thaw list
| index == listLen - 1 = V.create $ VM.init `liftM` V.thaw list
| otherwise = V.create $ do
list' <- V.thaw list
let moveLen = listLen - index - 1
s = VM.slice index moveLen list'
t = VM.slice (index + 1) moveLen list'
VM.move s t
return $ VM.init list'
where listLen = V.length list
bf_listinsert (Lst list : value : optional) =
return $ Lst $ listInsert list (fromIntegral index - 1) value
where [Int index] = defaults optional [Int 1]
bf_listappend (Lst list : value : optional) =
return $ Lst $ listInsert list (fromIntegral index) value
where [Int index] = defaults optional [Int $ fromIntegral $ V.length list]
bf_listdelete [Lst list, Int index]
| index' < 1 || index' > V.length list = raise E_RANGE
| otherwise = return $ Lst $ listDelete list (index' - 1)
where index' = fromIntegral index
bf_listset [Lst list, value, Int index]
| index' < 1 || index' > V.length list = raise E_RANGE
| otherwise = return $ Lst $ listSet list index' value
where index' = fromIntegral index
bf_setadd [Lst list, value] =
return $ Lst $ if value `V.elem` list then list else V.snoc list value
bf_setremove [Lst list, value] =
return $ Lst $ case V.elemIndex value list of
Nothing -> list
Just index -> listDelete list (fromIntegral index)