baskell-0.1.1: src/TypeCheck.hs
{-# LANGUAGE CPP #-}
{-------------------------------------------------------------------------------
Copyright: Bernie Pope 2004
Module: TypeCheck
Description: Infer types for Baskell programs and
expressions. Type inference is based on
a simple constraint solving process.
A single pass is made over the AST to
generate a set of type constraints. The
contraints are in the form of equalities:
type1 = type2
These constraints are then passed to a solver
which simplifies them as much as possible.
If a constraint can't be solved it will appear
in the solution. For example:
Int = Bool
Thus, the type you get back
is really a set of constraints, rather than
the (more traditional) single type or type
error. This type checker never gives errors!
Primary Authors: Bernie Pope
-------------------------------------------------------------------------------}
{-
This file is part of baskell.
baskell is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
baskell is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with baskell; if not, write to the Free Software
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
-}
module TypeCheck
( typeCheckExpression
, typeCheckProgram
, renderConstraints
, Constraint
, Binding (..)
, SolverType (..)
)
where
import AST
( Ident
, Exp (..)
, Lit (..)
, Decl (..)
, Program (..)
)
import qualified Data.Map as Map
( Map
, empty
, fromList
, union
, insert
, lookup
)
import Pretty
( Pretty (..)
, parensIf
, text
, (<+>)
, render
, vcat
, Doc
, parens
, cat
, (<>)
, punctuate
, comma
, brackets
, int
, empty
, ($$)
)
import Data.List
( mapAccumL
, find
, delete
)
import Depend
( depend )
import qualified Type
( Type (..) )
import Utils
( nameSupply )
import Control.Monad
( zipWithM
, liftM
, liftM2
, unless
)
import Control.Monad.State
( runStateT
, get
, put
, StateT
, gets
, modify
, execStateT
)
import Control.Monad.Trans
( lift
, liftIO
)
import Control.Monad.Reader
( ReaderT
, local
, ask
, runReaderT
)
--------------------------------------------------------------------------------
data SolverType
= TypeOf Binding Ident -- type of an identifier
| TVar Int
| TInt
| TChar
| TBool
| TList SolverType
| TFun SolverType SolverType
| TTuple [SolverType]
deriving (Eq, Show)
-- how an identifier is bound
data Binding
= Free -- not bound at all
| LamBound -- bound in a lambda abstraction
| LetBound -- bound in a function declaration (top level)
deriving (Eq, Show)
type BinderEnv = Map.Map Ident Binding
-- an equality constraint on types
type Constraint = (SolverType, SolverType)
-- infer the type of an expression from the command line
-- print out the type. An initial set of assumptions tell the
-- types of functions in scope
typeCheckExpression :: [Constraint] -> Exp -> IO ()
typeCheckExpression assumptions exp = do
let initialCount = 0
initialType = TVar initialCount
initialConstraint = (reservedIdent, initialType)
(constraints, finalCount)
<- runTC (typeExp initialType exp) (initialCount + 1) Map.empty
let initialStore
= Store
{ store_active = initialConstraint:constraints
, store_solution = []
, store_assumptions = assumptions
, store_count = finalCount
}
store <- runSolve solve initialStore
putStrLn $ render $ prettyTypeOfExp $ store_solution store
-- pretty print the type infered for an expression on the
-- command line
prettyTypeOfExp :: [Constraint] -> Doc
prettyTypeOfExp cs
= case find typeOfReservedIdent cs of
Nothing -> empty
Just c@(_typeOf, theType)
-> prettyTypeSol (theType, delete c cs)
where
typeOfReservedIdent :: Constraint -> Bool
typeOfReservedIdent (t1@(TypeOf LetBound ident), _t2)
= t1 == reservedIdent
typeOfReservedIdent otherConstraint = False
prettyTypeSol :: (SolverType, [Constraint]) -> Doc
prettyTypeSol (t, []) = prettyType t
prettyTypeSol (t, cs@(_:_))
= text "if" $$
indent (vcat $ map prettyConstraint cs) $$
text "then" $$
indent (pretty t)
indent :: Doc -> Doc
indent doc = text " " <> doc
-- infer the types for a whole program
-- the decls must be sorted into dependency order
typeCheckProgram :: [Constraint] -> Program -> IO [Constraint]
typeCheckProgram assumptions (Program decls) = do
store <- runSolve (typeDeclss (depend decls)) initialStore
return $ store_solution store
where
initialStore = Store
{ store_active = []
, store_solution = []
, store_assumptions = assumptions
, store_count = 0
}
--------------------------------------------------------------------------------
-- infer the types of declarations in dependency order
-- type solutions of earlier declarations become
-- type assumptions of later declarations
-- thus if f depends on g, g will be typed first
-- and its type will be an assumption when f is typed
typeDeclss :: [[Decl]] -> Solve ()
typeDeclss dss = mapM_ typeDecls dss
typeDecls :: [Decl] -> Solve ()
typeDecls ds = do
count <- gets store_count
(constraints, nextCount) <- liftIO $ runTC (mapM typeDecl ds) count Map.empty
modify $ \store -> store { store_count = nextCount }
updateActive $ concat constraints
solve
solution <- gets store_solution
updateAssumptions solution
modify $ \store -> store { store_active = [] }
--------------------------------------------------------------------------------
type TcState = Int
type TC a = ReaderT BinderEnv (StateT TcState IO) a
runTC :: TC a -> TcState -> BinderEnv -> IO (a, TcState)
runTC action state env = runStateT (runReaderT action env) state
freshVar :: TC SolverType
freshVar = do
count <- lift get
lift $ put (count + 1)
return $ TVar count
extendEnv :: Ident -> Binding -> TC a -> TC a
extendEnv ident binding action =
local (Map.insert ident binding) action
lookupIdentBinding :: Ident -> TC Binding
lookupIdentBinding ident = do
env <- ask
return $ case Map.lookup ident env of
Just bind -> bind
Nothing -> Free
-- type a single declaration
typeDecl :: Decl -> TC [Constraint]
typeDecl (Sig {}) = return [] -- XXX
typeDecl (Decl ident body) = do
newVar <- freshVar
cs <- typeExp newVar body
let c1 = (TypeOf LetBound ident, newVar)
return $ c1:cs
-- type expressions
-- * arg1 maps vars to their binding style
-- * arg2 is the expected type of this expression,
-- as required by its context
-- * arg3 is the expression itself
typeExp :: SolverType -> Exp -> TC [Constraint]
typeExp t (Var ident) = do
binding <- lookupIdentBinding ident
return [(TypeOf binding ident, t)]
-- XXX can we avoid the need to introduce t2?
typeExp t (Lam ident body) = do
t1 <- freshVar
t2 <- freshVar
let c1 = (t, TFun (TypeOf LamBound ident) t1)
c2 = (t2, TypeOf LamBound ident)
csBody <- extendEnv ident LamBound $ typeExp t1 body
return $ [c1, c2] ++ csBody
typeExp t (LamStrict ident body) = do
t1 <- freshVar
t2 <- freshVar
let c1 = (t, TFun (TypeOf LamBound ident) t1)
c2 = (t2, TypeOf LamBound ident)
csBody <- extendEnv ident LamBound $ typeExp t1 body
return $ [c1, c2] ++ csBody
typeExp t exp@(App e1 e2) = do
t1 <- freshVar
csRight <- typeExp t1 e2
csLeft <- typeExp (TFun t1 t) e1
return $ csLeft ++ csRight
typeExp t (Literal lit) = typeLit t lit
-- XXX delete this?
typeExp t (Tuple exps) = do
let dimension = length exps
vars <- sequence $ replicate dimension freshVar
let c1 = (t, TTuple vars)
cssExps <- typeExpList vars exps
return $ c1 : concat cssExps
-- primitives don't give rise to constraints
-- their types are already known
typeExp t (Prim _name _impl) = return []
-- XXX delete this? Is it some kind of mapM, or mapAccum ?
-- an list expression
typeExpList :: [SolverType] -> [Exp] -> TC [[Constraint]]
typeExpList ts es = zipWithM typeExp ts es
-- literals
-- * arg1 is the expected type of this literal,
-- as required by its context
-- * arg2 is the literal itself
typeLit :: SolverType -> Lit -> TC [Constraint]
typeLit t (LitInt _i) = return [(t, TInt)]
typeLit t (LitChar _c) = return [(t, TChar)]
typeLit t (LitBool _b) = return [(t, TBool)]
typeLit t LitCons = do
t1 <- freshVar
return [(t, TFun t1 (TFun (TList t1) (TList t1)))]
typeLit t LitNil = do
t1 <- freshVar
return [(t, TList t1)]
--------------------------------------------------------------------------------
-- constraint resolution
type Solve a = StateT Store IO a
runSolve :: Solve () -> Store -> IO Store
runSolve action store = execStateT action store
-- the constraint store
data Store
= Store
{ store_active :: [Constraint] -- not yet solved
, store_solution :: [Constraint] -- solved in this pass
, store_assumptions :: [Constraint] -- prior assumptions
, store_count :: Int -- counter for generating fresh vars
}
deriving (Eq, Show)
-- keep reducing the store until there are no active constraints left
solve :: Solve ()
solve = do
#ifdef DEBUG
-- store <- get
-- liftIO $ debugPrintStore store
#endif
active <- gets store_active
unless (null active) $ do
modify $ \store -> store { store_active = tail active }
applyRule $ head active
solve
-- eliminate a given active constraint
-- there are three situations to consider, the contraint deals with:
-- 1) the type of an identifier (typeOf x = Bool)
-- 2) a type variable (tvar 12 = Char)
-- 3) an equality between two concrete types (List (tvar 24) = List Int)
applyRule :: Constraint -> Solve ()
applyRule c@(t1, t2)
= case c of
-- type of identifier constraints
(TypeOf {}, _) -> typeOfRule c
(_, TypeOf {}) -> typeOfRule (t2, t1)
-- type variable contraints
(TVar {}, _) -> substitute c
(_, TVar {}) -> substitute (t2, t1)
-- constraints on concrete types
(_, _) -> match (t1, t2)
-- resolve contraints on types of identifiers
-- the variable could be:
-- * free
-- * let bound
-- * lambda bound
typeOfRule :: Constraint -> Solve ()
typeOfRule (t1@(TypeOf binding ident), t2)
| binding == Free = freeIdent (t1, t2)
| binding == LetBound = updateSolution [(t1, t2)]
| binding == LamBound = substitute (t1, t2)
-- a free identifier could be typed in the assumptions
-- or it may be unknown. If it is in the assumptions
-- then replace all occurrences of t2 with an *instance*
-- of the type found in the assumptions.
freeIdent :: Constraint -> Solve ()
freeIdent (t1@(TypeOf _binding ident), t2) = do
store <- get
let assumptions = store_assumptions store
case lookupAssumption assumptions ident of
Just scheme -> applyScheme (scheme, t2)
Nothing -> do
let solAndActive
= store_active store ++ store_solution store
newConstraints
= [ (t2, t3) | t3 <- lookupFreeIdent solAndActive ident ]
if null newConstraints
then updateSolution [(t1, t2)]
else updateActive newConstraints
-- look for a type assumption for an identifier in
-- a set of contraints
lookupAssumption :: [Constraint] -> Ident -> Maybe SolverType
lookupAssumption [] _key = Nothing
lookupAssumption ((TypeOf _binding ident, t) : cs) key
| ident == key = Just t
| otherwise = lookupAssumption cs key
lookupAssumption (_other : cs) key
= lookupAssumption cs key
lookupFreeIdent :: [Constraint] -> Ident -> [SolverType]
lookupFreeIdent [] _key = []
lookupFreeIdent (c@(t1, t2) : cs) key
= case c of
(TypeOf _binding ident, _)
-> if ident == key then t2 : rest else rest
(_, TypeOf _binding ident)
-> if ident == key then t1 : rest else rest
(_, _) -> rest
where
rest = lookupFreeIdent cs key
-- apply a type scheme to the contraint store
applyScheme :: Constraint -> Solve ()
applyScheme (scheme, t) = do
schemeInstance <- typeInstance scheme
updateActive [(schemeInstance, t)]
-- match two concrete types. This might generate new active
-- contraints if either of the types has arguments
match :: Constraint -> Solve ()
match (t1@(TVar i), t2@(TVar j))
| i == j = return ()
| otherwise = updateActive [(t1, t2)]
match (TInt, TInt) = return ()
match (TChar, TChar) = return ()
match (TBool, TBool) = return ()
match (TList t1, TList t2) = updateActive [(t1, t2)]
match (TFun t1 t2, TFun t3 t4) = updateActive [(t1, t3), (t2, t4)]
match (t1@(TTuple ts1), t2@(TTuple ts2))
| length ts1 == length ts2 = updateActive (zip ts1 ts2)
-- type error
| otherwise = updateSolution [(t1, t2)]
match (t1@(TypeOf _ _), t2@(TypeOf _ _))
| t1 == t2 = return ()
| otherwise = updateActive [(t1, t2)]
-- type error
match (t1, t2) = updateSolution [(t1, t2)]
-- substitute a type variable or a typeOf with
-- another type in the store
substitute :: Constraint -> Solve ()
substitute (t1, t2)
| t1 == t2 = return ()
-- occurs check failure, infinite type
| occursInType t1 t2 = updateSolution [(t1, t2)]
| otherwise = do
store <- get
let newActives = map (subTypeInConstraint t1 t2) (store_active store)
newSolution = map (subTypeInConstraint t1 t2) (store_solution store)
put $ store { store_active = newActives
, store_solution = newSolution }
subTypeInConstraint :: SolverType -> SolverType -> Constraint -> Constraint
subTypeInConstraint t1 t2 (typeLeft, typeRight)
= (newLeftType, newRightType)
where
newLeftType = subTypeInType t1 t2 typeLeft
newRightType = subTypeInType t1 t2 typeRight
subTypeInType :: SolverType -> SolverType -> SolverType -> SolverType
subTypeInType old new thisType@(TVar _)
| thisType == old = new
| otherwise = thisType
subTypeInType old new (TList t)
= TList $ subTypeInType old new t
subTypeInType old new (TFun t1 t2)
= TFun newT1 newT2
where
newT1 = subTypeInType old new t1
newT2 = subTypeInType old new t2
subTypeInType old new (TTuple ts)
= TTuple newTs
where
newTs = map (subTypeInType old new) ts
subTypeInType old new thisType@(TypeOf _ _)
| thisType == old = new
| otherwise = thisType
subTypeInType old new otherType = otherType
-- make a fresh instance of an existing type.
-- instance has same shape as existing type
-- but all variables are fresh
type TyVarMap = Map.Map Int Int
type Inst a = StateT (TyVarMap, Int) IO a
lookupTyVarMap :: Int -> Inst (Maybe Int)
lookupTyVarMap i = do
map <- gets fst
return $ Map.lookup i map
freshTyVarCounter :: Inst Int
freshTyVarCounter = do
count <- gets snd
modify $ \(map, count) -> (map, count+1)
return count
extendTyVarMap :: Int -> Int -> Inst ()
extendTyVarMap x y =
modify $ \(map, count) -> (Map.insert x y map, count)
typeInstance :: SolverType -> Solve SolverType
typeInstance t = do
count <- gets store_count
(resultType, (_env, finalCount))
<- liftIO $ runStateT (mkInstance t) (Map.empty, count)
modify $ \store -> store { store_count = finalCount }
return resultType
where
mkInstance :: SolverType -> Inst SolverType
mkInstance (TVar var) = do
mbVar <- lookupTyVarMap var
case mbVar of
Nothing -> do
count <- freshTyVarCounter
extendTyVarMap var count
return $ TVar count
Just newVar -> return $ TVar newVar
mkInstance (TList t) = liftM TList $ mkInstance t
mkInstance (TFun t1 t2) = liftM2 TFun (mkInstance t1) (mkInstance t2)
mkInstance (TTuple ts) = liftM TTuple $ mapM mkInstance ts
mkInstance otherType = return otherType
-- does a type var or typeOf occur within another type?
occursInType :: SolverType -> SolverType -> Bool
occursInType search thisType@(TVar _)
= search == thisType
occursInType search (TList t)
= occursInType search t
occursInType search (TFun t1 t2)
= occursInType search t1 || occursInType search t2
occursInType search (TTuple ts)
= any (occursInType search) ts
occursInType search thisType@(TypeOf _ _)
= search == thisType
occursInType search other = False
-- update the solution constraints in the store
updateSolution :: [Constraint] -> Solve ()
updateSolution cs = do
oldSolution <- gets store_solution
modify $ \store -> store { store_solution = cs ++ oldSolution }
-- update the active constraints in the store
updateActive :: [Constraint] -> Solve ()
updateActive cs = do
oldActive <- gets store_active
modify $ \store -> store { store_active = cs ++ oldActive }
-- update the active constraints in the store
updateAssumptions :: [Constraint] -> Solve ()
updateAssumptions cs = do
oldAssumps <- gets store_assumptions
modify $ \store -> store { store_assumptions = cs ++ oldAssumps }
--------------------------------------------------------------------------------
-- pretty printing of types and constraints
debugPrintStore :: Store -> IO ()
debugPrintStore store
= do
putStrLn "---- the current store ----"
putStrLn "active constraints:"
putStrLn $ renderConstraintsUgly $ store_active store
putStrLn "solution:"
putStrLn $ renderConstraintsUgly $ store_solution store
-- putStrLn "assumptions:"
-- putStrLn $ renderConstraints $ store_assumptions store
putStr "count: "
print $ store_count store
return ()
data PrettyState
= PrettyState
{ prettyState_varMap :: Map.Map Int String
, prettyState_nameSupply :: [String]
}
initPrettyState :: PrettyState
initPrettyState
= PrettyState
{ prettyState_varMap = Map.empty
, prettyState_nameSupply = nameSupply
}
instance Pretty SolverType where
pretty = prettyType
-- pretty printing of types, type variables get nice names
prettyType :: SolverType -> Doc
prettyType = snd . prettyTypeWorker False initPrettyState
prettyTypeWorker :: Bool -> PrettyState -> SolverType -> (PrettyState, Doc)
prettyTypeWorker _bracks state (TVar i)
= case Map.lookup i varMap of
Nothing
-> (newState, text newName)
Just name -> (state, text name)
where
varMap = prettyState_varMap state
nameSupply = prettyState_nameSupply state
newName = head nameSupply
newState
= PrettyState
{ prettyState_varMap = Map.insert i newName varMap
, prettyState_nameSupply = tail nameSupply
}
prettyTypeWorker _bracks state TInt = (state, text "Int")
prettyTypeWorker _bracks state TChar = (state, text "Char")
prettyTypeWorker _bracks state TBool = (state, text "Bool")
prettyTypeWorker _bracks state (TList t)
= (newState, brackets doc)
where
(newState, doc) = prettyTypeWorker False state t
prettyTypeWorker bracks state (TFun t1 t2)
= (newState, doc)
where
(t1State, t1Doc) = prettyTypeWorker True state t1
(newState, t2Doc) = prettyTypeWorker False t1State t2
doc = parensIf bracks (t1Doc <+> text "->" <+> t2Doc)
prettyTypeWorker _bracks state (TTuple ts)
= (newState, doc)
where
(newState, tsDoc) = mapAccumL (prettyTypeWorker False) state ts
doc = parens $ cat $ punctuate comma tsDoc
prettyTypeWorker _bracks state (TypeOf binding ident)
= (state, doc)
where
doc = text "type" <> (parens $ prettyBinder binding <+> text ident)
-- less pretty printing of types. Type variables do not get nice
-- names, they are printed as their underlying numbers. This is
-- helpful for debugging the constraint solver.
uglyType :: Bool -> SolverType -> Doc
uglyType _bracks (TVar i)
= text "t" <> int i
uglyType _bracks TInt = text "Int"
uglyType _bracks TChar = text "Char"
uglyType _bracks TBool = text "Bool"
uglyType _bracks (TList t)
= brackets $ uglyType False t
uglyType bracks (TFun t1 t2)
= parensIf bracks (t1Doc <+> text "->" <+> t2Doc)
where
t1Doc = uglyType True t1
t2Doc = uglyType False t2
uglyType _bracks (TTuple ts)
= parens $ cat $ punctuate comma tsDoc
where
tsDoc = map (uglyType False) ts
uglyType _bracks (TypeOf binding ident)
= text "type" <> (parens $ prettyBinder binding <+> text ident)
prettyBinder :: Binding -> Doc
prettyBinder Free = text "free"
prettyBinder LamBound = text "lambda-bound"
prettyBinder LetBound = text "let-bound"
reservedIdent :: SolverType
reservedIdent = TypeOf LetBound "$"
uglyConstraint :: Constraint -> Doc
uglyConstraint (t1, t2)
= uglyType False t1 <+> text "=" <+> uglyType False t2
prettyConstraint :: Constraint -> Doc
prettyConstraint
= snd . prettyConstraintWorker initPrettyState
prettyConstraintWorker :: PrettyState -> Constraint -> (PrettyState, Doc)
prettyConstraintWorker state (TypeOf LetBound ident, t2)
= (newState, doc)
where
(newState, t2Doc) = prettyTypeWorker False state t2
doc = text ident <+> text "::" <+> t2Doc
prettyConstraintWorker state (t1, t2)
= (newState, doc)
where
(t1State, t1Doc) = prettyTypeWorker False state t1
(newState, t2Doc) = prettyTypeWorker False t1State t2
doc = t1Doc <+> text "=" <+> t2Doc
renderConstraints :: [Constraint] -> String
renderConstraints cs
= render $ vcat $ map prettyConstraint cs
renderConstraintsUgly :: [Constraint] -> String
renderConstraintsUgly cs
= render $ vcat $ map uglyConstraint cs
--------------------------------------------------------------------------------
toSolverType :: Type.Type -> SolverType
toSolverType (Type.TVar i) = TVar i
toSolverType Type.TInt = TInt
toSolverType Type.TChar = TChar
toSolverType Type.TBool = TBool
toSolverType (Type.TList t) = TList $ toSolverType t
toSolverType (Type.TFun t1 t2) = TFun (toSolverType t1) (toSolverType t2)
toSolverType (Type.TTuple ts) = TTuple $ map toSolverType ts