-
Notifications
You must be signed in to change notification settings - Fork 0
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Typechecker #53
base: master
Are you sure you want to change the base?
Typechecker #53
Conversation
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I know you still have yet to complete this PR with the type context/environment, but a couple of nits:
- If you don't need an argument/pattern match discriminee, "name" it
_
to indicate that it isn't needed. For instance,case something Just _ -> True ; Nothing -> False
. - If you find yourself manually reimplementing a basic list pattern like
map
orfold
to iterate through it, there's probably already a library function to do that for you. - When constructing messages for your
Left TypeError
s, try to include additional information (are there any relevant variable names? what is the context in which you encountered the type error, e.g., while checking aFork
statement or checking aBOp
?)
I can give some general tips if it's of interest: A nice monad transformer stack for type checking: import Control.Monad.Reader -- Reader for context
import Control.Monad.Except -- Except for error handling (synonymous with manually throwing around Either e a, as you are doing now)
type TC a = ReaderT Context (Except TypeError) a
runTC :: TC a -> Either TypeError a
runTC tca = runExcept $ runReaderT tca context
where
context = -- some initial context containing the procedure signatures -- I like using We need some type of errors: data TypeError = UnboundVariable Ident -- ^ variable @Ident@ is not ins cope
| TypeError SSMExp t1 t2 -- ^ The expression failed to type check, found t1 when t2 was expected
-- more variants as needed When you type check a language like this, you need to (as you point out yourself) maintain a context that gives you access to the types of any identifiers in scope. This context really consists of two parts - that which does not change (function signatures) and that which is dynamic (identifiers in scope). import qualified Data.Map as Map
data Context = Context { procedures :: Map Ident Type -- ^ types of procedures
, scopes :: [Map Ident Type] -- ^ types of variables (head of this list is the youngest scope)
}
lookupProcedure :: Ident -> TC Type
lookupProcedure id = do
e <- ask
case Map.lookup id (procedures e) of
Just t -> return t
Nothing -> throwError $ UnboundVariable id
{- | Look up the type of a variable. As our language has scopes (e.g if-branches & while loops), we must
dig through all the scopes to search for a type for the variable. We allow shadowing variables, so we start
looking for the type in the youngest scope and work our way towards the oldest. If no scope contains the variable,
it is clearly unbound and we encountered an error. -}
lookupVar :: Ident -> TC Type
lookupVar id = do
e <- ask
lookupVar' id (scopes e)
where
lookupVar' :: Ident -> [Map Ident Type] -> TC Type
lookupVar' id [] = throwError $ UnboundVariable id
lookupVar' id (c:cs) = case Map.lookup id c of
Just t -> return t
Nothing -> lookupVar' id xs We also need some way of extending this environment with additional type information. We also want to be very clear with the lifetime of this variable, so we include a computation that the variable should remain alive for. withVar :: Ident -> Type -> TC a -> TC a
withVar id t tca = local (\c -> c { withVar' id t (scopes e) }) tca
where
withVar' :: Ident -> Type -> [Map Ident Type] -> [Map Ident Type]
withVar' id t (c:cs) = Map.insert id t c : cs Now when we are type checking a procedure body, e.g, I would type check assertType :: SSMExp -> Type -> Type -> TC ()
assertType e t1 t2 =
if t1 == t2
then return ()
else throwError $ TypeError e t1 t2
stmts :: [Stmt] -> TC ()
stmts (x:xs) = case x of
NewRef id t e -> do
t' <- checkExp e -- typecheck e
assertType e t' t -- check that the type of the expression is the one we expect it to be (t)
withVar id t' $ stmts xs -- here we say to call @stmts xs@ with the extended environment
-- but here, after that call returned, the environment is just as it was before! :) The final crux is scoping. If we are type checking a -- | perform a computation with a new scope in the context. Context is restored once the computation terminates.
withNewScope :: TC a -> TC a
withNewScope tca = local (\c -> c { scopes = Map.empty : (scopes s) }) tca
stmts :: [Stmt] -> TC ()
stmts (x:xs) = case x of
If c thn els -> do
t <- checkExp c
assertType c t TBool
withNewScope $ stmts thn
withNewScope $ stmts els If I were to write the type checker I think I would take an approach like this one. It is possible to use I think writing a type checker for a language like ours (no polymorphic types etc, which makes checking quite straightforward) is a good exercise to get comfortable with monads. It was for me when I took a course in programming language technology :) |
Untested type checker. Might need a lot more debugging.
Pending tasks: