haste-compiler-0.5.3: src/Haste/AST/FlowAnalysis.hs
-- | Basic data flow analysis over the Haste AST.
module Haste.AST.FlowAnalysis (
Strict (..), VarInfo (..), InfoMap, ArgMap,
mkVarInfo, nullInfo, mergeVarInfos, findVarInfos
) where
import Haste.AST.Syntax
import Control.Monad.State
import Data.List (foldl', sort, group)
import qualified Data.Set as S
import qualified Data.Map as M
data Strict = Strict | Unknown
deriving (Eq, Show)
data VarInfo = VarInfo {
-- | Is this var always strict?
varStrict :: Strict,
-- | Does this var always have the same, statically known, arity?
-- @Nothing@, if var is not a function.
varArity :: Maybe Int,
-- | What are the sources of this var? I.e. for each occurrence where the
-- var is the LHS of an assignment or substitution (including function
-- call), add the RHS to this list.
varAlias :: [Var]
} deriving (Eq, Show)
mkVarInfo :: Strict -> Maybe Int -> VarInfo
mkVarInfo s ar = VarInfo s ar []
type InfoMap = M.Map Var VarInfo
type ArgMap = M.Map Var [Var]
nullInfo :: VarInfo
nullInfo = VarInfo Unknown Nothing []
-- | Merge two var infos. The aliases of the infos are simply concatenated
-- and filtered for duplicates. For concrete information, if the two infos
-- disagree on any field, that field becomes unknown.
mergeVarInfos :: VarInfo -> VarInfo -> VarInfo
mergeVarInfos a b = VarInfo strict' arity' (varAlias a `merge` varAlias b)
where
strict'
| varStrict a == varStrict b = varStrict a
| otherwise = Unknown
arity'
| varArity a == varArity b = varArity a
| otherwise = Nothing
merge as bs = map head . group . sort $ as ++ bs
-- | Merge the infos of a var with that of all of its aliases.
resolveVarInfo :: InfoMap -> Var -> Maybe VarInfo
resolveVarInfo m var = go (S.singleton var) var
where
go seen v = do
nfo <- M.lookup v m
let xs' = filter (not . (`S.member` seen)) (varAlias nfo)
seen' = foldl' (flip S.insert) seen xs'
nfos <- mapM (go seen') xs'
case foldl' mergeVarInfos (nfo {varAlias = xs'}) nfos of
VarInfo Unknown Nothing _ -> Nothing
nfo' -> return nfo'
type EvalM = State InfoMap
-- | Figure out as much statically known information as possible about all
-- vars in a program. The returned map will be collapsed so that recursive
-- lookups are not necessary, and the 'varAlias' field of each info will
-- be empty.
--
-- TODO: track return values, tail call status
findVarInfos :: ArgMap -> Stm -> InfoMap
findVarInfos argmap = mergeAll . snd . flip runState M.empty . goS
where
mergeAll m = M.foldWithKey resolve m m
resolve v _ m =
case resolveVarInfo m v of
Just nfo -> M.insert v (nfo {varAlias = []}) m
_ -> m
goS Stop = return ()
goS Cont = return ()
goS (Forever s) = goS s
goS (Case c d as next) = do
goE c
goS d
mapM_ (\(e, s) -> goE e >> goS s) as
goS next
goS (Assign (NewVar _ v) rhs next) = handleAssign v rhs >> goS next
goS (Assign (LhsExp _ (Var v)) rhs next) = handleAssign v rhs >> goS next
goS (Assign (LhsExp _ lhs) rhs next) = goE lhs >> goE rhs >> goS next
goS (Return ex) = goE ex
goS (ThunkRet ex) = goE ex
goS (Tailcall ex) = goE ex
handleAssign v (Var v') = v `dependsOn` v'
handleAssign v (Eval (Var v')) = v `dependsOn` v'
handleAssign v (Fun args body) = do
v `hasInfo` mkVarInfo Strict (Just $ length args)
goS body
handleAssign v (Lit _) = v `hasInfo` mkVarInfo Strict Nothing
handleAssign v (JSLit _) = v `hasInfo` mkVarInfo Strict Nothing
handleAssign v rhs = v `hasInfo` nullInfo >> goE rhs
-- If a function is ever stored anywhere except as a pure alias, we must
-- immediately stop assuming things about it, since we now have no idea
-- where it may be called. If it is not stored, we know that all calls to
-- it will be direct, so it is safe to assume things about its arguments
-- based on our static analysis.
-- To be safe - but overly conservative - we set the
-- info of any arguments of each function aliased by a var that occurs
-- outside a permitted context (function call or synonymous assignment)
-- to nullInfo.
goE (Var v) = setArgsOf v nullInfo (S.singleton v)
goE (Lit _) = return ()
goE (JSLit _) = return ()
goE (Not ex) = goE ex
goE (BinOp _ a b) = goE a >> goE b
goE (Fun _ body) = goS body
goE (Call _ _ (Var f) xs) = handleCall f xs
goE (Call _ _ (Fun as b) xs) = handleArgs as xs >> goS b
goE (Call _ _ f xs) = mapM_ goE (f:xs)
goE (Index x ix) = goE x >> goE ix
goE (Arr xs) = mapM_ goE xs
goE (Member x _) = goE x
goE (Obj xs) = mapM_ goE (map snd xs)
goE (AssignEx (Var v) rhs) = handleAssign v rhs
goE (AssignEx lhs rhs) = goE lhs >> goE rhs
goE (IfEx c l r) = mapM_ goE [c, l, r]
goE (Eval ex) = goE ex
goE (Thunk _ body) = goS body
handleArgs as xs = sequence_ (zipWith handleAssign as xs)
handleCall f argexs =
case M.lookup f argmap of
Just argvars -> handleArgs argvars argexs
_ -> return ()
-- Finds the functions (if any) aliased by v and sets the varinfo of their
-- arguments to @nfo@.
setArgsOf v nfo seen = do
case M.lookup v argmap of
Just args -> mapM_ (`hasInfo` nullInfo) args
_ -> do
nfos <- get
case M.lookup v nfos of
Nothing -> return ()
Just nfo' -> do
let aliases = filter (not . (`S.member` seen)) (varAlias nfo')
seen' = foldl' (flip S.insert) seen aliases
mapM_ (\v' -> setArgsOf v' nfo seen') aliases
hasInfo :: Var -> VarInfo -> EvalM ()
hasInfo v nfo = do
nfos <- get
put $ M.alter upd v nfos
where
upd (Just nfo') = Just $ mergeVarInfos nfo nfo'
upd _ = Just nfo
dependsOn :: Var -> Var -> EvalM ()
dependsOn v v' = do
nfos <- get
put $ M.alter upd v nfos
where
upd (Just nfo) = Just $ nfo {varAlias = v' : varAlias nfo}
upd _ = Just $ nullInfo {varAlias = [v']}