swearjure-1.0.0: src/Swearjure/Eval.hs
{-# OPTIONS_GHC -Wall -Werror #-}
module Swearjure.Eval where
import Control.Applicative ((<$>), (<*>))
import Control.Monad.Except
import Control.Monad.Reader
import Control.Monad.State
import Data.Function (on)
import Data.Generics.Fixplate (Mu(..))
import Data.List (nub, sortBy)
import qualified Data.Map as M
import Data.Maybe (fromMaybe, listToMaybe, isJust, isNothing)
import qualified Data.Set as S
import qualified Data.Traversable as T
import Prelude hiding (lookup, seq, concat)
import Swearjure.AST
import Swearjure.Errors
import Swearjure.Primitives
initEnv :: Env
initEnv = Toplevel $ M.fromList $ map
(\(fname, f) -> (fname, ("clojure.core",
(False, Fix $ EFn $ PrimFn
(Prim ("core", fname) f)))))
[ ("+", plus)
, ("/", divFn)
, ("*", mul)
, ("-", minus)
, ("list", return . Fix . EList)
, ("vector", return . Fix . EVec)
, ("apply", apply)
, ("seq", liftM (Fix . EList) . seq)
, ("concat", liftM (Fix . EList) . concat)
, ("deref", deref)
, ("hash-map", hashMap)
, ("hash-set", hashSet)
, ("<", lt)
, (">", gt)
, ("<=", lte)
, (">=", gte)
, ("=", eq)
, ("==", numEq)
] ++ map
(\(fname, f) -> (fname, ("clojure.core",
(True, Fix $ EFn $ PrimFn
(Prim ("core", fname) f)))))
-- TODO: Implement these as non-prim macros in Swearjure. Somehow.
[ ("->>", threadLast)
, ("->", threadSnd)
]
++ map
(\(fname, f) -> (fname, ("swearjure.core",
(False, Fix $ EFn $ PrimFn
(Prim ("swearjure.core", fname) f)))))
[ ("<<'", readChar)
, (">>'", prChars)
, (">>", prn)
]
apply :: [Val] -> EvalState Val
apply [] = throwError $ ArityException 0 "core/apply"
apply [_] = throwError $ ArityException 1 "core/apply"
apply (f : xs) = do fn <- ifn f
lastOnes <- seq [last xs]
let spliced = init xs ++ lastOnes
runFn fn spliced
runFn :: Fn -> [Val]-> EvalState Val
runFn f@Fn {fnEnv = env, fnRecName = fname
, fnFns = options} args
= do (paramNames, restName, exprs) <- findOption (length args) options
mapping <- prepMapping paramNames restName args $ recBinding fname
thunk <- extendEnv env mapping (evalAll exprs)
thunk
where findOption n [] = throwError $ ArityException n (prStr $ Fix $ EFn f)
findOption n (opt : rst)
| argcPred opt n = return opt
| otherwise = findOption n rst
argcPred (params, Just _, _) = (length params <=)
argcPred (params, Nothing, _) = (length params ==)
recBinding (Just x) = [(x, Fix $ EFn f)]
recBinding Nothing = []
prepMapping (p : ps) r (x : xs) acc = prepMapping ps r xs $ (p, x) : acc
prepMapping [] (Just r) xs acc = return $ (r, Fix $ EList xs) : acc
prepMapping [] Nothing [] acc = return acc
prepMapping _ _ _ _ = throwError $ IllegalState $ "Well, we still have"
++ " some arguments left, but no values to assign"
-- Returns a thunk, in order to support tail recursion
evalAll [x@(Fix (EList []))] = return $ return x
evalAll [Fix (EList xs)]
| head xs == _quote = return $ return $ fromMaybe _nil (listToMaybe $ tail xs)
| head xs == _var = return $ var (tail xs)
| head xs == _nil = throwError $ IllegalArgument "Can't call nil"
-- need to trick the thunkification here: We must run this in the
-- parent environment, so let's just grab it and rewrap the result.
| head xs == _fnStar = do env' <- ask
return $ local (const env')
(makeLambda $ tail xs)
| otherwise = do evf : evxs <- mapM eval xs
return $ apply [evf, Fix (EList evxs)]
evalAll [x] = return $ return x
evalAll (x : xs) = eval x >> evalAll xs
evalAll [] = return $ return _nil
runFn (PrimFn (Prim _ prim)) args = prim args
macroexpand :: Val -> EvalState Val
macroexpand lst@(Fix (EList (x@(Fix (ESym ns s)) : xs)))
| x == _quote = return lst
| x == _fnStar = return lst
| x == _var = return lst
| otherwise = do maybeMacro <- lookupMacro ns s
case maybeMacro of
Just macro ->
do expandOnce <- apply [macro, Fix (EList xs)]
macroexpand expandOnce
Nothing -> Fix <$> T.mapM macroexpand (unFix lst)
macroexpand (Fix x) = Fix <$> T.mapM macroexpand x
eval :: Val -> EvalState Val
eval = macroexpand >=> go . unFix
where go (ESym ns s) = lookup ns s
go x@(EList []) = return $ Fix x
go (EList xs)
| head xs == _quote = return $ fromMaybe _nil (listToMaybe $ tail xs)
| head xs == _var = var (tail xs)
| head xs == _nil = throwError $ IllegalArgument "Can't call nil"
| head xs == _fnStar = makeLambda $ tail xs
| otherwise = do f : xs' <- mapM eval xs
apply [f, Fix (EList xs')]
go v@(EVec _) = Fix <$> T.mapM eval v
go (ESet xs) = do evals <- mapM eval xs
checkDupe evals
return $ Fix $ ESet evals
go (EHM pairs) = do evals <- mapMtuple eval pairs
checkDupe $ map fst evals
return $ Fix $ EHM evals
go x = return $ Fix x
mapMtuple f = mapM (\(x,y) -> (,) <$> f x <*> f y)
-- from GHC.Exts
-- | The 'sortWith' function sorts a list of elements using the
-- user supplied function to project something out of each element
sortWith :: Ord b => (a -> b) -> [a] -> [a]
sortWith f = sortBy (compare `on` f)
makeLambda :: [Val] -> EvalState Val
makeLambda xs = do env <- ask
num <- get
modify (+2)
(recName, rst) <- findName $ map unFix xs
fns <- sortWith (\(x, y, _) -> length x + countMaybe y)
<$> mkUnsureFn rst
validateArity $ map (\(x, y, _) -> (length x, y)) fns
-- TODO: Walk the values and verify that symbols and vars can
-- be resolved.
return $ Fix $ EFn
Fn { fnEnv = env
, fnNs = "user"
, fnName = mungedName recName num
, fnRecName = recName
, fnFns = fns
}
where mungedName (Just fname) num = "eval" ++ show num ++ "$" ++ fname ++ "__"
++ show (num + 1)
mungedName Nothing num = mungedName (Just "fn") num
findName (ESym _ s : rst)
| S.member s specials = throwError $ IllegalArgument
$ "Can't use " ++ s ++ " as function name"
| otherwise = return (Just s, rst)
findName r = return (Nothing, r)
mkUnsureFn :: [SwjValF Val] -> EvalState [([String], Maybe String, [Val])]
mkUnsureFn [] = return []
mkUnsureFn lst@(EList _ : _) = mkFns lst
mkUnsureFn lst@(EVec _ : _) = do fn <- mkFn lst
return [fn]
mkUnsureFn (x : _) = throwError $ CastException (typeName' x) "List/Vector"
mkFn (EVec args' : exprs)
= do (args, restArg) <- validateArgs $ map unFix args'
return (args, restArg, map Fix exprs)
mkFn (x : _) = throwError $ CastException (typeName' x) "Vector"
mkFn [] = throwError $ IllegalArgument
"(This gives a NullPointerException in Clojure)"
mkFns = mapM (mkFn <=< unList)
unList (EList ys) = return $ map unFix ys
unList x = throwError $ CastException (typeName' x) "List"
validateArgs [] = return ([], Nothing)
validateArgs (s@(ESym (Just _) _) : _)
= throwError $ IllegalArgument $
"Can't use qualified name as parameter: " ++ prStr (Fix s)
validateArgs [ESym Nothing "&", s@(ESym (Just _) _)]
= throwError $ IllegalArgument $
"Can't use qualified name as parameter: " ++ prStr (Fix s)
validateArgs [ESym Nothing "&", ESym Nothing s]
| s /= "&" = return ([], Just s)
| otherwise = throwError $ IllegalArgument "Invalid parameter list"
validateArgs (ESym Nothing s : args')
| s /= "&" = do (args, restArg) <- validateArgs args'
return (s : args, restArg)
| otherwise = throwError $ IllegalArgument "Invalid parameter list"
validateArgs (x : _) = throwError $ IllegalArgument
$ "Unsupported binding key: " ++ prStr (Fix x)
-- assumes these are sorted by size
validateArity ys
= do when (1 < variadicCount ys) $
throwError $ IllegalArgument
"Can't have more than 1 variadic overload"
when (ys /= nub ys) $ throwError $ IllegalArgument
"Can't have 2 overloads with same arity"
when (variadicCount ys == 1 &&
isNothing (snd $ last ys)) $
throwError $ IllegalArgument $ "Can't have fixed arity "
++ "function with more params than variadic function"
variadicCount ys = length (filter isJust (map snd ys))
countMaybe Nothing = 0
countMaybe (Just _) = 1
ifn :: Val -> EvalState Fn
ifn = go . unFix
where go :: SwjValF Val -> EvalState Fn
go s@(ESym _ _) = lookupThing s
go kw@(EKw _ _) = lookupThing kw
go (EFn f) = return f
go v@(EVec _) = lookup1 v
go s@(ESet _) = lookup1 s
go hm@(EHM _) = lookup12 hm
-- TODO: When dereffed, this prints badly.
go (EVar f True) = unnamedPrim -- this always throws arity exceptions
(\x -> throwError $ ArityException (length x)
(prStr $ Fix $ EFn f)) -- TODO: Not printed correctly.
go (EVar fn False) = return fn
go x = throwError $ CastException (typeName' x) "IFn"
lookupThing s = unnamedPrim
(\xs -> let (f, r) = splitAt 1 xs in
getFn $ f ++ [Fix s] ++ r)
lookup12 hm = unnamedPrim $ getFn . (Fix hm :)
lookup1 x = unnamedPrim $ get1Fn . (Fix x :)
unnamedPrim = return . PrimFn . Prim ("", "")
-- I'd usually put this in prims, but deref needs to convert val through ifn
deref :: [Val] -> EvalState Val
deref [f@(Fix (EVar _ _))] = Fix . EFn <$> (ifn f)
deref [x] = throwError $ CastException (typeName x) "java.util.concurrent.Future"
deref x = throwError $ ArityException (length x) "core/deref"
checkDupe :: [Val] -> EvalState ()
checkDupe = go S.empty
where go _ [] = return ()
go s (x : r)
| S.member x s = throwError $ IllegalState
$ "Duplicate entry: " ++ prStr x
| otherwise = go (S.insert x s) r
var :: [Val] -> EvalState Val
var (Fix (ESym ns v) : _)
= do macroLookup <- lookupMacro ns v
case macroLookup of
(Just (Fix (EFn mac))) -> return $ Fix (EVar mac True)
(Just _) -> throwError $ IllegalState $ "Internal Swearjure error --" ++
" macro lookup returned non-macro function"
Nothing -> do valLookup <- lookup ns v
case valLookup of
(Fix (EFn fn)) -> return $ Fix (EVar fn False)
_ -> throwError $ IllegalState $ "Swearjure can't take var of non-fn value"
var (v : _) = throwError $ CastException (typeName v) "Symbol"
var [] = throwError $ IllegalArgument
"(This gives a NullPointerException in Clojure)"