comments and better type errors

This commit is contained in:
crumbtoo
2023-12-18 11:11:22 -07:00
parent f7e850c61a
commit 0470912983
3 changed files with 66 additions and 17 deletions

View File

@@ -1,3 +1,7 @@
{-|
Module : Core.HindleyMilner
Description : Hindley-Milner inference
-}
{-# LANGUAGE LambdaCase #-} {-# LANGUAGE LambdaCase #-}
module Core.HindleyMilner module Core.HindleyMilner
( infer ( infer
@@ -15,28 +19,65 @@ import Control.Monad.State
import Core.Syntax import Core.Syntax
---------------------------------------------------------------------------------- ----------------------------------------------------------------------------------
-- | Annotated typing context -- I have a feeling we're going to want this in the
-- future.
type Context b = [(b, Type)] type Context b = [(b, Type)]
-- | Unannotated typing context, AKA our beloved Γ.
type Context' = Context Name type Context' = Context Name
-- TODO: Errorful monad? -- TODO: Errorful monad?
data TypeError = TyErrCouldNotUnify Type Type
-- | Type error enum.
data TypeError
-- | Two types could not be unified
= TyErrCouldNotUnify Type Type
-- | @x@ could not be unified with @t@ because @x@ occurs in @t@
| TyErrRecursiveType Name Type | TyErrRecursiveType Name Type
deriving Show -- | Untyped, potentially undefined variable
| TyErrUntypedVariable Name
deriving (Show, Eq)
-- | Synonym for @Either TypeError@
type HMError = Either TypeError
-- | Infer the type of an expression under some context.
--
-- >>> let g1 = [("id", TyVar "a" :-> TyVar "a")]
-- >>> let g2 = [("id", (TyVar "a" :-> TyVar "a") :-> TyVar "a" :-> TyVar "a")]
-- >>> infer g1 [coreExpr|id 3|]
-- Right TyInt
-- >>> infer g2 [coreExpr|id 3|]
-- Left (TyErrCouldNotUnify (TyVar "a" :-> TyVar "a") TyInt)
infer :: Context' -> Expr' -> Either TypeError Type infer :: Context' -> Expr' -> Either TypeError Type
infer g e = foldr (uncurry subst) t <$> unify cs where infer g e = do
(t,cs) = gather g e (t,cs) <- gather g e
foldr (uncurry subst) t <$> unify cs
-- | A @Constraint@ between two types describes the requirement that the pair
-- must unify
type Constraint = (Type, Type) type Constraint = (Type, Type)
gather :: Context' -> Expr' -> (Type, [Constraint]) -- | Type of an expression under some context, and gather the constraints
gather = \g e -> let (t,(cs,_)) = runState (go g e) ([],0) in (t,cs) where -- necessary to unify. Note that this is not the same as @infer@, as the
go :: Context' -> Expr' -> State ([Constraint], Int) Type -- expression will likely be given a fresh type variable along with a
-- constraint, rather than the solved type.
--
-- For example, if the context says "@id@ has type a -> a," in an application of
-- @id 3@, the whole application is assigned type @$a0@ and the constraint that
-- @id@ must unify with type @Int -> $a0@ is generated.
--
-- >>> gather [("id", TyVar "a" :-> TyVar "a")] [coreExpr|id 3|]
-- (TyVar "$a0",[(TyVar "a" :-> TyVar "a",TyInt :-> TyVar "$a0")])
gather :: Context' -> Expr' -> HMError (Type, [Constraint])
gather = \g e -> runStateT (go g e) ([],0) <&> \ (t,(cs,_)) -> (t,cs) where
go :: Context' -> Expr' -> StateT ([Constraint], Int) HMError Type
go g = \case go g = \case
LitE (IntL _) -> pure TyInt LitE (IntL _) -> pure TyInt
Var k -> maybe e pure $ lookup k g Var k -> lift $ maybe e Right $ lookup k g
where e = error $ "variable `" <> k <> "' untyped in Γ" where e = Left (TyErrUntypedVariable k)
App f x -> do App f x -> do
tf <- go g f tf <- go g f
tx <- go g x tx <- go g x
@@ -44,18 +85,23 @@ gather = \g e -> let (t,(cs,_)) = runState (go g e) ([],0) in (t,cs) where
addConstraint tf (tx :-> tfx) addConstraint tf (tx :-> tfx)
pure tfx pure tfx
uniqueVar :: State ([Constraint], Int) Type uniqueVar :: StateT ([Constraint], Int) HMError Type
uniqueVar = do uniqueVar = do
n <- use _2 n <- use _2
_2 %= succ _2 %= succ
pure (TyVar $ '$' : 'a' : show n) pure (TyVar $ '$' : 'a' : show n)
addConstraint :: Type -> Type -> State ([Constraint], Int) () addConstraint :: Type -> Type -> StateT ([Constraint], Int) HMError ()
addConstraint t u = _1 %= ((t, u):) addConstraint t u = _1 %= ((t, u):)
unify :: [Constraint] -> Either TypeError Context' -- | Unify a list of constraints, meaning that pairs between types are turned
-- into pairs of type variables and types. A useful thought model is to think of
-- it like solving an equation such that the unknown variable is the left-hand
-- side.
unify :: [Constraint] -> HMError Context'
unify = go mempty where unify = go mempty where
go :: Context' -> [Constraint] -> Either TypeError Context' go :: Context' -> [Constraint] -> HMError Context'
-- nothing left! return accumulated context -- nothing left! return accumulated context
go g [] = Right g go g [] = Right g
@@ -90,7 +136,10 @@ unify = go mempty where
| x == y = True | x == y = True
occurs _ = False occurs _ = False
subst :: String -> Type -> Type -> Type -- | The expression @subst x t e@ substitutes all occurences of @x@ in @e@ with
-- @t@
subst :: Name -> Type -> Type -> Type
subst x t (TyVar y) | x == y = t subst x t (TyVar y) | x == y = t
subst x t (a :-> b) = subst x t a :-> subst x t b subst x t (a :-> b) = subst x t a :-> subst x t b
subst _ _ e = e subst _ _ e = e

View File

@@ -44,7 +44,7 @@ import Lens.Micro
---------------------------------------------------------------------------------- ----------------------------------------------------------------------------------
data Expr b = Var Name data Expr b = Var Name
| Con Tag Int -- Con Tag Arity | Con Tag Int -- ^ Con Tag Arity
| Case (Expr b) [Alter b] | Case (Expr b) [Alter b]
| Lam [b] (Expr b) | Lam [b] (Expr b)
| Let Rec [Binding b] (Expr b) | Let Rec [Binding b] (Expr b)

View File

@@ -15,5 +15,5 @@ spec :: Spec
spec = do spec = do
it "should infer `id 3` :: Int" $ it "should infer `id 3` :: Int" $
let g = [ ("id", TyVar "a" :-> TyVar "a") ] let g = [ ("id", TyVar "a" :-> TyVar "a") ]
in infer g [coreExpr|id 3|] `shouldBe` Just TyInt in infer g [coreExpr|id 3|] `shouldBe` Right TyInt