disco-0.1.0.0: src/Disco/Typecheck/Solve.hs
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
-----------------------------------------------------------------------------
-- |
-- Module : Disco.Typecheck.Solve
-- Copyright : disco team and contributors
-- Maintainer : byorgey@gmail.com
--
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Constraint solver for the constraints generated during type
-- checking/inference.
-----------------------------------------------------------------------------
module Disco.Typecheck.Solve where
import Unbound.Generics.LocallyNameless (Alpha, Name, Subst, fv,
name2Integer, string2Name,
substs)
import Data.Coerce
import GHC.Generics (Generic)
import Control.Arrow ((&&&), (***))
import Control.Lens hiding (use, (%=), (.=))
import Control.Monad (unless, zipWithM)
import Data.Bifunctor (first, second)
import Data.Either (partitionEithers)
import Data.List (find, foldl', intersect,
partition)
import Data.Map (Map, (!))
import qualified Data.Map as M
import Data.Maybe (fromJust, fromMaybe,
mapMaybe)
import Data.Monoid (First (..))
import Data.Set (Set)
import qualified Data.Set as S
import Data.Tuple
import Disco.Effects.Fresh
import Disco.Effects.State
import Polysemy
import Polysemy.Error
import Polysemy.Output
import Disco.Messages
import Disco.Pretty hiding ((<>))
import Disco.Subst
import qualified Disco.Subst as Subst
import Disco.Typecheck.Constraints
import Disco.Typecheck.Graph (Graph)
import qualified Disco.Typecheck.Graph as G
import Disco.Typecheck.Unify
import Disco.Types
import Disco.Types.Qualifiers
import Disco.Types.Rules
--------------------------------------------------
-- Solver errors
-- | Type of errors which can be generated by the constraint solving
-- process.
data SolveError where
NoWeakUnifier :: SolveError
NoUnify :: SolveError
UnqualBase :: Qualifier -> BaseTy -> SolveError
Unqual :: Qualifier -> Type -> SolveError
QualSkolem :: Qualifier -> Name Type -> SolveError
deriving Show
instance Semigroup SolveError where
e <> _ = e
--------------------------------------------------
-- Error utilities
runSolve :: Sem (Fresh ': Error SolveError ': r) a -> Sem r (Either SolveError a)
runSolve = runError . runFresh
-- | Run a list of actions, and return the results from those which do
-- not throw an error. If all of them throw an error, rethrow the
-- first one.
filterErrors :: Member (Error e) r => [Sem r a] -> Sem r [a]
filterErrors ms = do
es <- mapM try ms
case partitionEithers es of
(e:_, []) -> throw e
(_, as) -> return as
-- | A variant of 'asum' which picks the first action that succeeds,
-- or re-throws the error of the last one if none of them
-- do. Precondition: the list must not be empty.
asum' :: Member (Error e) r => [Sem r a] -> Sem r a
asum' [] = error "Impossible: asum' []"
asum' [m] = m
asum' (m:ms) = m `catch` (\_ -> asum' ms)
--------------------------------------------------
-- Simple constraints
data SimpleConstraint where
(:<:) :: Type -> Type -> SimpleConstraint
(:=:) :: Type -> Type -> SimpleConstraint
deriving (Show, Eq, Ord, Generic, Alpha, Subst Type)
instance Pretty SimpleConstraint where
pretty = \case
ty1 :<: ty2 -> pretty ty1 <+> "<:" <+> pretty ty2
ty1 :=: ty2 -> pretty ty1 <+> "=" <+> pretty ty2
--------------------------------------------------
-- Simplifier types
-- Uses TH to generate lenses so it has to go here before other stuff.
---------------------------------
-- Variable maps
-- | Information about a particular type variable. More information
-- may be added in the future (e.g. polarity).
data TyVarInfo = TVI
{ _tyVarIlk :: First Ilk -- ^ The ilk (unification or skolem) of the variable, if known
, _tyVarSort :: Sort -- ^ The sort (set of qualifiers) of the type variable.
}
deriving (Show)
makeLenses ''TyVarInfo
instance Pretty TyVarInfo where
pretty (TVI (First ilk) s) = maybe (pure "?") pretty ilk <> "%" <> pretty s
-- | Create a 'TyVarInfo' given an 'Ilk' and a 'Sort'.
mkTVI :: Ilk -> Sort -> TyVarInfo
mkTVI = TVI . First . Just
-- | We can learn different things about a type variable from
-- different places; the 'Semigroup' instance allows combining
-- information about a type variable into a single record.
instance Semigroup TyVarInfo where
TVI i1 s1 <> TVI i2 s2 = TVI (i1 <> i2) (s1 <> s2)
-- | A 'TyVarInfoMap' records what we know about each type variable;
-- it is a mapping from type variable names to 'TyVarInfo' records.
newtype TyVarInfoMap = VM { unVM :: Map (Name Type) TyVarInfo }
deriving (Show)
instance Pretty TyVarInfoMap where
pretty (VM m) = pretty m
-- | Utility function for acting on a 'TyVarInfoMap' by acting on the
-- underlying 'Map'.
onVM ::
(Map (Name Type) TyVarInfo -> Map (Name Type) TyVarInfo) ->
TyVarInfoMap -> TyVarInfoMap
onVM f (VM m) = VM (f m)
-- | Look up a given variable name in a 'TyVarInfoMap'.
lookupVM :: Name Type -> TyVarInfoMap -> Maybe TyVarInfo
lookupVM v = M.lookup v . unVM
-- | Remove the mapping for a particular variable name (if it exists)
-- from a 'TyVarInfoMap'.
deleteVM :: Name Type -> TyVarInfoMap -> TyVarInfoMap
deleteVM = onVM . M.delete
-- | Given a list of type variable names, add them all to the
-- 'TyVarInfoMap' as 'Skolem' variables (with a trivial sort).
addSkolems :: [Name Type] -> TyVarInfoMap -> TyVarInfoMap
addSkolems vs = onVM $ \vm -> foldl' (flip (\v -> M.insert v (mkTVI Skolem mempty))) vm vs
-- | The @Semigroup@ instance for 'TyVarInfoMap' unions the two maps,
-- combining the info records for any variables occurring in both
-- maps.
instance Semigroup TyVarInfoMap where
VM sm1 <> VM sm2 = VM (M.unionWith (<>) sm1 sm2)
instance Monoid TyVarInfoMap where
mempty = VM M.empty
mappend = (<>)
-- | Get the sort of a particular variable recorded in a
-- 'TyVarInfoMap'. Returns the trivial (empty) sort for a variable
-- not in the map.
getSort :: TyVarInfoMap -> Name Type -> Sort
getSort (VM m) v = maybe topSort (view tyVarSort) (M.lookup v m)
-- | Get the 'Ilk' of a variable recorded in a 'TyVarInfoMap'.
-- Returns @Nothing@ if the variable is not in the map, or if its
-- ilk is not known.
getIlk :: TyVarInfoMap -> Name Type -> Maybe Ilk
getIlk (VM m) v = (m ^? ix v . tyVarIlk) >>= getFirst
-- | Extend the sort of a type variable by combining its existing sort
-- with the given one. Has no effect if the variable is not already
-- in the map.
extendSort :: Name Type -> Sort -> TyVarInfoMap -> TyVarInfoMap
extendSort x s = onVM (at x . _Just . tyVarSort %~ (`S.union` s))
---------------------------------
-- Simplifier state
-- The simplification stage maintains a mutable state consisting of
-- the current qualifier map (containing wanted qualifiers for type
-- variables), the list of remaining SimpleConstraints, and the
-- current substitution. It also keeps track of seen constraints, so
-- expansion of recursive types can stop when encountering a
-- previously seen constraint.
data SimplifyState = SS
{ _ssVarMap :: TyVarInfoMap
, _ssConstraints :: [SimpleConstraint]
, _ssSubst :: S
, _ssSeen :: Set SimpleConstraint
}
makeLenses ''SimplifyState
lkup :: (Ord k, Show k, Show (Map k a)) => String -> Map k a -> k -> a
lkup messg m k = fromMaybe (error errMsg) (M.lookup k m)
where
errMsg = unlines
[ "Key lookup error:"
, " Key = " ++ show k
, " Map = " ++ show m
, " Location: " ++ messg
]
--------------------------------------------------
-- Top-level solver algorithm
solveConstraint
:: Members '[Fresh, Error SolveError, Output Message] r
=> TyDefCtx -> Constraint -> Sem r S
solveConstraint tyDefns c = do
-- Step 1. Open foralls (instantiating with skolem variables) and
-- collect wanted qualifiers; also expand disjunctions. Result in a
-- list of possible constraint sets; each one consists of equational
-- and subtyping constraints in addition to qualifiers.
debug "Solving:"
debugPretty c
debug "------------------------------"
debug "Decomposing constraints..."
qcList <- decomposeConstraint c
-- Now try continuing with each set and pick the first one that has
-- a solution.
asum' (map (uncurry (solveConstraintChoice tyDefns)) qcList)
solveConstraintChoice
:: Members '[Fresh, Error SolveError, Output Message] r
=> TyDefCtx -> TyVarInfoMap -> [SimpleConstraint] -> Sem r S
solveConstraintChoice tyDefns quals cs = do
debugPretty quals
debug $ vcat (map pretty' cs)
-- Step 2. Check for weak unification to ensure termination. (a la
-- Traytel et al).
let toEqn (t1 :<: t2) = (t1,t2)
toEqn (t1 :=: t2) = (t1,t2)
_ <- note NoWeakUnifier $ weakUnify tyDefns (map toEqn cs)
-- Step 3. Simplify constraints, resulting in a set of atomic
-- subtyping constraints. Also simplify/update qualifier set
-- accordingly.
debug "------------------------------"
debug "Running simplifier..."
(vm, atoms, theta_simp) <- simplify tyDefns quals cs
debug "Done running simplifier. Results:"
debugPretty vm
debug $ vcat $ map (pretty' . (\(x,y) -> TyAtom x :<: TyAtom y)) atoms
debugPretty theta_simp
-- Step 4. Turn the atomic constraints into a directed constraint
-- graph.
debug "------------------------------"
debug "Generating constraint graph..."
-- Some variables might have qualifiers but not participate in any
-- equality or subtyping relations (see issue #153); make sure to
-- extract them and include them in the constraint graph as isolated
-- vertices
let mkAVar (v, First (Just Skolem)) = AVar (S v)
mkAVar (v, _ ) = AVar (U v)
vars = S.fromList . map (mkAVar . second (view tyVarIlk)) . M.assocs . unVM $ vm
g = mkConstraintGraph vars atoms
debugPretty g
-- Step 5.
-- Check for any weakly connected components containing more
-- than one skolem, or a skolem and a base type; such components are
-- not allowed. Other WCCs with a single skolem simply unify to
-- that skolem.
debug "------------------------------"
debug "Checking WCCs for skolems..."
(g', theta_skolem) <- checkSkolems tyDefns vm g
debugPretty theta_skolem
-- We don't need to ensure that theta_skolem respects sorts since
-- checkSkolems will only unify skolem vars with unsorted variables.
-- Step 6. Eliminate cycles from the graph, turning each strongly
-- connected component into a single node, unifying all the atoms in
-- each component.
debug "------------------------------"
debug "Collapsing SCCs..."
(g'', theta_cyc) <- elimCycles tyDefns g'
debugPretty g''
debugPretty theta_cyc
-- Check that the resulting substitution respects sorts...
let sortOK (x, TyAtom (ABase ty)) = hasSort ty (getSort vm x)
sortOK (_, TyAtom (AVar (U _))) = True
sortOK p = error $ "Impossible! sortOK " ++ show p
unless (all sortOK (Subst.toList theta_cyc))
$ throw NoUnify
-- ... and update the sort map if we unified any type variables.
-- Just make sure that if theta_cyc maps x |-> y, then y picks up
-- the sort of x.
debug "Old sort map:"
debugPretty vm
let vm' = foldr updateVarMap vm (Subst.toList theta_cyc)
where
updateVarMap :: (Name Type, Type) -> TyVarInfoMap -> TyVarInfoMap
updateVarMap (x, TyAtom (AVar (U y))) vmm = extendSort y (getSort vmm x) vmm
updateVarMap _ vmm = vmm
debug "Updated sort map:"
debugPretty vm
-- Steps 7 & 8: solve the graph, iteratively finding satisfying
-- assignments for each type variable based on its successor and
-- predecessor base types in the graph; then unify all the type
-- variables in any remaining weakly connected components.
debug "------------------------------"
debug "Solving for type variables..."
theta_sol <- solveGraph vm' g''
debugPretty theta_sol
debug "------------------------------"
debug "Composing final substitution..."
let theta_final = theta_sol @@ theta_cyc @@ theta_skolem @@ theta_simp
debugPretty theta_final
return theta_final
--------------------------------------------------
-- Step 1. Constraint decomposition.
decomposeConstraint
:: Members '[Fresh, Error SolveError] r
=> Constraint -> Sem r [(TyVarInfoMap, [SimpleConstraint])]
decomposeConstraint (CSub t1 t2) = return [(mempty, [t1 :<: t2])]
decomposeConstraint (CEq t1 t2) = return [(mempty, [t1 :=: t2])]
decomposeConstraint (CQual q ty) = (:[]) . (, []) <$> decomposeQual ty q
decomposeConstraint (CAnd cs) = map mconcat . sequence <$> mapM decomposeConstraint cs
decomposeConstraint CTrue = return [mempty]
decomposeConstraint (CAll ty) = do
(vars, c) <- unbind ty
let c' = substs (mkSkolems vars) c
(map . first . addSkolems) vars <$> decomposeConstraint c'
where
mkSkolems :: [Name Type] -> [(Name Type, Type)]
mkSkolems = map (id &&& TySkolem)
decomposeConstraint (COr cs) = concat <$> filterErrors (map decomposeConstraint cs)
decomposeQual
:: Members '[Fresh, Error SolveError] r
=> Type -> Qualifier -> Sem r TyVarInfoMap
decomposeQual (TyAtom a) q = checkQual q a
-- XXX Really we should be able to check by induction whether a
-- user-defined type has a certain sort.
decomposeQual ty@(TyCon (CUser _) _) q = throw $ Unqual q ty
decomposeQual ty@(TyCon c tys) q
= case qualRules c q of
Nothing -> throw $ Unqual q ty
Just qs -> mconcat <$> zipWithM (maybe (return mempty) . decomposeQual) tys qs
checkQual
:: Members '[Fresh, Error SolveError] r
=> Qualifier -> Atom -> Sem r TyVarInfoMap
checkQual q (AVar (U v)) = return . VM . M.singleton v $ mkTVI Unification (S.singleton q)
checkQual q (AVar (S v)) = throw $ QualSkolem q v
checkQual q (ABase bty) =
case hasQual bty q of
True -> return mempty
False -> throw $ UnqualBase q bty
--------------------------------------------------
-- Step 3. Constraint simplification.
-- | This step does unification of equality constraints, as well as
-- structural decomposition of subtyping constraints. For example,
-- if we have a constraint (x -> y) <: (z -> Int), then we can
-- decompose it into two constraints, (z <: x) and (y <: Int); if we
-- have a constraint v <: (a,b), then we substitute v ↦ (x,y) (where
-- x and y are fresh type variables) and continue; and so on.
--
-- After this step, the remaining constraints will all be atomic
-- constraints, that is, only of the form (v1 <: v2), (v <: b), or
-- (b <: v), where v is a type variable and b is a base type.
simplify
:: Members '[Error SolveError, Output Message] r
=> TyDefCtx -> TyVarInfoMap -> [SimpleConstraint] -> Sem r (TyVarInfoMap, [(Atom, Atom)], S)
simplify tyDefns origVM cs
= (\(SS vm' cs' s' _) -> (vm', map extractAtoms cs', s'))
-- contFreshMT :: Monad m => FreshMT m a -> Integer -> m a
-- "Run a FreshMT computation given a starting index for fresh name generation."
<$> runFresh' n (execState (SS origVM cs idS S.empty) simplify')
where
fvNums :: Alpha a => [a] -> [Integer]
fvNums = map (name2Integer :: Name Type -> Integer) . toListOf fv
-- Find first unused integer in constraint free vars and sort map
-- domain, and use it to start the fresh var generation, so we don't
-- generate any "fresh" names that interfere with existing names
n1 = maximum0 . fvNums $ cs
n = succ . maximum . (n1:) . fvNums . M.keys . unVM $ origVM
maximum0 [] = 0
maximum0 xs = maximum xs
-- Extract the type atoms from an atomic constraint.
extractAtoms :: SimpleConstraint -> (Atom, Atom)
extractAtoms (TyAtom a1 :<: TyAtom a2) = (a1, a2)
extractAtoms c = error $ "Impossible: simplify left non-atomic or non-subtype constraint " ++ show c
-- Iterate picking one simplifiable constraint and simplifying it
-- until none are left.
simplify'
:: Members '[State SimplifyState, Fresh, Error SolveError, Output Message] r
=> Sem r ()
simplify' = do
-- q <- gets fst
-- debug (pretty q)
-- debug ""
mc <- pickSimplifiable
case mc of
Nothing -> return ()
Just s -> do
debug $ "Simplifying:" <+> pretty' s
simplifyOne s
simplify'
-- Pick out one simplifiable constraint, removing it from the list
-- of constraints in the state. Return Nothing if no more
-- constraints can be simplified.
pickSimplifiable
:: Members '[State SimplifyState, Fresh, Error SolveError] r
=> Sem r (Maybe SimpleConstraint)
pickSimplifiable = do
curCs <- use ssConstraints
case pick simplifiable curCs of
Nothing -> return Nothing
Just (a,as) -> do
ssConstraints .= as
return (Just a)
-- Pick the first element from a list satisfying the given
-- predicate, returning the element and the list with the element
-- removed.
pick :: (a -> Bool) -> [a] -> Maybe (a,[a])
pick _ [] = Nothing
pick p (a:as)
| p a = Just (a,as)
| otherwise = second (a:) <$> pick p as
-- Check if a constraint can be simplified. An equality
-- constraint can always be "simplified" via unification. A
-- subtyping constraint can be simplified if either it involves a
-- type constructor (in which case we can decompose it), or if it
-- involves two base types (in which case it can be removed if the
-- relationship holds).
simplifiable :: SimpleConstraint -> Bool
simplifiable (_ :=: _) = True
simplifiable (TyCon {} :<: TyCon {}) = True
simplifiable (TyVar {} :<: TyCon {}) = True
simplifiable (TyCon {} :<: TyVar {}) = True
simplifiable (TyCon (CUser _) _ :<: _) = True
simplifiable (_ :<: TyCon (CUser _) _) = True
simplifiable (TyAtom (ABase _) :<: TyAtom (ABase _)) = True
simplifiable _ = False
-- Simplify the given simplifiable constraint. If the constraint
-- has already been seen before (due to expansion of a recursive
-- type), just throw it away and stop.
simplifyOne
:: Members '[State SimplifyState, Fresh, Error SolveError] r
=> SimpleConstraint -> Sem r ()
simplifyOne c = do
seen <- use ssSeen
case c `S.member` seen of
True -> return ()
False -> do
ssSeen %= S.insert c
simplifyOne' c
simplifyOne'
:: Members '[State SimplifyState, Fresh, Error SolveError] r
=> SimpleConstraint -> Sem r ()
-- If we have an equality constraint, run unification on it. The
-- resulting substitution is applied to the remaining constraints
-- as well as prepended to the current substitution.
simplifyOne' (ty1 :=: ty2) =
case unify tyDefns [(ty1, ty2)] of
Nothing -> throw NoUnify
Just s' -> extendSubst s'
-- If we see a constraint of the form (T <: a), where T is a
-- user-defined type and a is a type variable, then just turn it
-- into an equality (T = a). This is sound but probably not
-- complete. The alternative seems quite complicated, possibly
-- even undecidable. See https://github.com/disco-lang/disco/issues/207 .
simplifyOne' (ty1@(TyCon (CUser _) _) :<: ty2@TyVar{})
= simplifyOne' (ty1 :=: ty2)
-- Otherwise, expand the user-defined type and continue.
simplifyOne' (TyCon (CUser t) ts :<: ty2) =
case M.lookup t tyDefns of
Nothing -> error $ show t ++ " not in ty defn map!"
Just (TyDefBody _ body) ->
ssConstraints %= ((body ts :<: ty2) :)
-- Turn a <: T into a = T. See comment above.
simplifyOne' (ty1@TyVar{} :<: ty2@(TyCon (CUser _) _))
= simplifyOne' (ty1 :=: ty2)
simplifyOne' (ty1 :<: TyCon (CUser t) ts) =
case M.lookup t tyDefns of
Nothing -> error $ show t ++ " not in ty defn map!"
Just (TyDefBody _ body) ->
ssConstraints %= ((ty1 :<: body ts) :)
-- Given a subtyping constraint between two type constructors,
-- decompose it if the constructors are the same (or fail if they
-- aren't), taking into account the variance of each argument to
-- the constructor. Container types are a special case;
-- recursively generate a subtyping constraint for their
-- constructors as well.
simplifyOne' (TyCon c1@(CContainer ctr1) tys1 :<: TyCon (CContainer ctr2) tys2) =
ssConstraints %=
(( (TyAtom ctr1 :<: TyAtom ctr2)
: zipWith3 variance (arity c1) tys1 tys2
)
++)
simplifyOne' (TyCon c1 tys1 :<: TyCon c2 tys2)
| c1 /= c2 = throw NoUnify
| otherwise =
ssConstraints %= (zipWith3 variance (arity c1) tys1 tys2 ++)
-- Given a subtyping constraint between a variable and a type
-- constructor, expand the variable into the same constructor
-- applied to fresh type variables.
simplifyOne' con@(TyVar a :<: TyCon c _) = expandStruct a c con
simplifyOne' con@(TyCon c _ :<: TyVar a ) = expandStruct a c con
-- Given a subtyping constraint between two base types, just check
-- whether the first is indeed a subtype of the second. (Note
-- that we only pattern match here on type atoms, which could
-- include variables, but this will only ever get called if
-- 'simplifiable' was true, which checks that both are base
-- types.)
simplifyOne' (TyAtom (ABase b1) :<: TyAtom (ABase b2)) = do
case isSubB b1 b2 of
True -> return ()
False -> throw NoUnify
simplifyOne' (s :<: t) =
error $ "Impossible! simplifyOne' " ++ show s ++ " :<: " ++ show t
expandStruct
:: Members '[State SimplifyState, Fresh, Error SolveError] r
=> Name Type -> Con -> SimpleConstraint -> Sem r ()
expandStruct a c con = do
as <- mapM (const (TyVar <$> fresh (string2Name "a"))) (arity c)
let s' = a |-> TyCon c as
ssConstraints %= (con:)
extendSubst s'
-- 1. compose s' with current subst
-- 2. apply s' to constraints
-- 3. apply s' to qualifier map and decompose
extendSubst
:: Members '[State SimplifyState, Fresh, Error SolveError] r
=> S -> Sem r ()
extendSubst s' = do
ssSubst %= (s'@@)
ssConstraints %= applySubst s'
substVarMap s'
substVarMap
:: Members '[State SimplifyState, Fresh, Error SolveError] r
=> S -> Sem r ()
substVarMap s' = do
vm <- use ssVarMap
-- 1. Get quals for each var in domain of s' and match them with
-- the types being substituted for those vars.
let tySorts :: [(Type, Sort)]
tySorts = map (second (view tyVarSort)) . mapMaybe (traverse (`lookupVM` vm) . swap) $ Subst.toList s'
tyQualList :: [(Type, Qualifier)]
tyQualList = concatMap (sequenceA . second S.toList) tySorts
-- 2. Decompose the resulting qualifier constraints
vm' <- mconcat <$> mapM (uncurry decomposeQual) tyQualList
-- 3. delete domain of s' from vm and merge in decomposed quals.
ssVarMap .= vm' <> foldl' (flip deleteVM) vm (dom s')
-- The above works even when unifying two variables. Say we have
-- the TyVarInfoMap
--
-- a |-> {add}
-- b |-> {sub}
--
-- and we get back theta = [a |-> b]. The domain of theta
-- consists solely of a, so we look up a in the TyVarInfoMap and get
-- {add}. We therefore generate the constraint 'add (theta a)'
-- = 'add b' which can't be decomposed at all, and hence yields
-- the TyVarInfoMap {b |-> {add}}. We then delete a from the
-- original TyVarInfoMap and merge the result with the new TyVarInfoMap,
-- yielding {b |-> {sub,add}}.
-- Create a subtyping constraint based on the variance of a type
-- constructor argument position: in the usual order for
-- covariant, and reversed for contravariant.
variance Co ty1 ty2 = ty1 :<: ty2
variance Contra ty1 ty2 = ty2 :<: ty1
--------------------------------------------------
-- Step 4: Build constraint graph
-- | Given a list of atoms and atomic subtype constraints (each pair
-- @(a1,a2)@ corresponds to the constraint @a1 <: a2@) build the
-- corresponding constraint graph.
mkConstraintGraph :: (Show a, Ord a) => Set a -> [(a, a)] -> Graph a
mkConstraintGraph as cs = G.mkGraph nodes (S.fromList cs)
where
nodes = as `S.union` S.fromList (cs ^.. traverse . each)
--------------------------------------------------
-- Step 5: Check skolems
-- | Check for any weakly connected components containing more than
-- one skolem, or a skolem and a base type, or a skolem and any
-- variables with nontrivial sorts; such components are not allowed.
-- If there are any WCCs with a single skolem, no base types, and
-- only unsorted variables, just unify them all with the skolem and
-- remove those components.
checkSkolems
:: Members '[Error SolveError, Output Message] r
=> TyDefCtx -> TyVarInfoMap -> Graph Atom -> Sem r (Graph UAtom, S)
checkSkolems tyDefns vm graph = do
let skolemWCCs :: [Set Atom]
skolemWCCs = filter (any isSkolem) $ G.wcc graph
ok wcc = S.size (S.filter isSkolem wcc) <= 1
&& all (\case { ABase _ -> False
; AVar (S _) -> True
; AVar (U v) -> maybe True (S.null . view tyVarSort) (lookupVM v vm) })
wcc
(good, bad) = partition ok skolemWCCs
unless (null bad) $ throw NoUnify
-- take all good sets and
-- (1) delete them from the graph
-- (2) unify them all with the skolem
unifyWCCs graph idS good
where
noSkolems :: Atom -> UAtom
noSkolems (ABase b) = UB b
noSkolems (AVar (U v)) = UV v
noSkolems (AVar (S v)) = error $ "Skolem " ++ show v ++ " remaining in noSkolems"
unifyWCCs g s [] = return (G.map noSkolems g, s)
unifyWCCs g s (u:us) = do
debug $ "Unifying" <+> pretty' (u:us) <> "..."
let g' = foldl' (flip G.delete) g u
ms' = unifyAtoms tyDefns (S.toList u)
case ms' of
Nothing -> throw NoUnify
Just s' -> unifyWCCs g' (atomToTypeSubst s' @@ s) us
--------------------------------------------------
-- Step 6: Eliminate cycles
-- | Eliminate cycles in the constraint set by collapsing each
-- strongly connected component to a single node, (unifying all the
-- types in the SCC). A strongly connected component is a maximal
-- set of nodes where every node is reachable from every other by a
-- directed path; since we are using directed edges to indicate a
-- subtyping constraint, this means every node must be a subtype of
-- every other, and the only way this can happen is if all are in
-- fact equal.
--
-- Of course, this step can fail if the types in a SCC are not
-- unifiable. If it succeeds, it returns the collapsed graph (which
-- is now guaranteed to be acyclic, i.e. a DAG) and a substitution.
elimCycles
:: Members '[Error SolveError] r
=> TyDefCtx -> Graph UAtom -> Sem r (Graph UAtom, S)
elimCycles tyDefns = elimCyclesGen uatomToTypeSubst (unifyUAtoms tyDefns)
elimCyclesGen
:: forall a b r. (Subst a a, Ord a, Members '[Error SolveError] r)
=> (Substitution a -> Substitution b) -> ([a] -> Maybe (Substitution a))
-> Graph a -> Sem r (Graph a, Substitution b)
elimCyclesGen genSubst genUnify g
= note NoUnify
$ (G.map fst &&& (genSubst . compose . S.map snd . G.nodes)) <$> g'
where
g' :: Maybe (Graph (a, Substitution a))
g' = G.sequenceGraph $ G.map unifySCC (G.condensation g)
unifySCC :: Set a -> Maybe (a, Substitution a)
unifySCC uatoms = case S.toList uatoms of
[] -> error "Impossible! unifySCC on the empty set"
as@(a:_) -> (flip applySubst a &&& id) <$> genUnify as
------------------------------------------------------------
-- Steps 7 and 8: Constraint resolution
------------------------------------------------------------
-- | Rels stores the set of base types and variables related to a
-- given variable in the constraint graph (either predecessors or
-- successors, but not both).
data Rels = Rels
{ baseRels :: Set BaseTy
, varRels :: Set (Name Type)
}
deriving (Show, Eq)
-- | A RelMap associates each variable to its sets of base type and
-- variable predecessors and successors in the constraint graph.
newtype RelMap = RelMap { unRelMap :: Map (Name Type, Dir) Rels}
instance Pretty RelMap where
pretty (RelMap rm) = vcat (map prettyVar byVar)
where
vars = S.map fst (M.keysSet rm)
byVar = map (\x -> (rm!(x,SubTy), x, rm!(x,SuperTy))) (S.toList vars)
prettyVar (subs, x, sups) = hsep [prettyRel subs, "<:", pretty x, "<:", prettyRel sups]
prettyRel rs = pretty (baseRels rs) <> ", " <> pretty (varRels rs)
-- | Modify a @RelMap@ to record the fact that we have solved for a
-- type variable. In particular, delete the variable from the
-- @RelMap@ as a key, and also update the relative sets of every
-- other variable to remove this variable and add the base type we
-- chose for it.
substRel :: Name Type -> BaseTy -> RelMap -> RelMap
substRel x ty
= RelMap
. M.delete (x,SuperTy)
. M.delete (x,SubTy)
. M.map (\r@(Rels b v) -> if x `S.member` v then Rels (S.insert ty b) (S.delete x v) else r)
. unRelMap
-- | Essentially dirtypesBySort vm rm dir t s x finds all the
-- dir-types (sub- or super-) of t which have sort s, relative to
-- the variables in x. This is \overbar{T}_S^X (resp. \underbar...)
-- from Traytel et al.
dirtypesBySort :: TyVarInfoMap -> RelMap -> Dir -> BaseTy -> Sort -> Set (Name Type) -> [BaseTy]
dirtypesBySort vm (RelMap relMap) dir t s x
-- Keep only those supertypes t' of t
= keep (dirtypes dir t) $ \t' ->
-- which have the right sort, and such that
hasSort t' s &&
-- for all variables beta \in x,
forAll x (\beta ->
-- there is at least one type t'' which is a subtype of t'
-- which would be a valid solution for beta, that is,
exists (dirtypes (other dir) t') $ \t'' ->
-- t'' has the sort beta is supposed to have, and
hasSort t'' (getSort vm beta) &&
-- t'' is a supertype of every base type predecessor of beta.
forAll (baseRels (lkup "dirtypesBySort, beta rel" relMap (beta, other dir)))
(isDirB dir t''))
-- The above comments are written assuming dir = Super; of course,
-- if dir = Sub then just swap "super" and "sub" everywhere.
where
forAll, exists :: Foldable t => t a -> (a -> Bool) -> Bool
forAll = flip all
exists = flip any
keep = flip filter
-- | Sort-aware infimum or supremum.
limBySort :: TyVarInfoMap -> RelMap -> Dir -> [BaseTy] -> Sort -> Set (Name Type) -> Maybe BaseTy
limBySort vm rm dir ts s x
= (\is -> find (\lim -> all (\u -> isDirB dir u lim) is) is)
. isects
. map (\t -> dirtypesBySort vm rm dir t s x)
$ ts
where
isects = foldr1 intersect
lubBySort, glbBySort :: TyVarInfoMap -> RelMap -> [BaseTy] -> Sort -> Set (Name Type) -> Maybe BaseTy
lubBySort vm rm = limBySort vm rm SuperTy
glbBySort vm rm = limBySort vm rm SubTy
-- | From the constraint graph, build the sets of sub- and super- base
-- types of each type variable, as well as the sets of sub- and
-- supertype variables. For each type variable x in turn, try to
-- find a common supertype of its base subtypes which is consistent
-- with the sort of x and with the sorts of all its sub-variables,
-- as well as symmetrically a common subtype of its supertypes, etc.
-- Assign x one of the two: if it has only successors, assign it
-- their inf; otherwise, assign it the sup of its predecessors. If
-- it has both, we have a choice of whether to assign it the sup of
-- predecessors or inf of successors; both lead to a sound &
-- complete algorithm. We choose to assign it the sup of its
-- predecessors in this case, since it seems nice to default to
-- "simpler" types lower down in the subtyping chain.
solveGraph
:: Members '[Fresh, Error SolveError, Output Message] r
=> TyVarInfoMap -> Graph UAtom -> Sem r S
solveGraph vm g = atomToTypeSubst . unifyWCC <$> go topRelMap
where
unifyWCC :: Substitution BaseTy -> Substitution Atom
unifyWCC s = compose (map mkEquateSubst wccVarGroups) @@ fmap ABase s
where
wccVarGroups :: [Set (Name Type)]
wccVarGroups = map (S.map getVar) . filter (all uisVar) . applySubst s $ G.wcc g
getVar (UV v) = v
getVar (UB b) = error
$ "Impossible! Base type " ++ show b ++ " in solveGraph.getVar"
mkEquateSubst :: Set (Name Type) -> Substitution Atom
mkEquateSubst = mkEquations . S.toList
mkEquations (a:as) = Subst.fromList . map (\v -> (coerce v, AVar (U a))) $ as
mkEquations [] = error "Impossible! Empty set of names in mkEquateSubst"
-- After picking concrete base types for all the type
-- variables we can, the only thing possibly remaining in
-- the graph are components containing only type variables
-- and no base types. It is sound, and simplifies the
-- generated types considerably, to simply unify any type
-- variables which are related by subtyping constraints.
-- That is, we collect all the type variables in each
-- weakly connected component and unify them.
--
-- As an example where this final step makes a difference,
-- consider a term like @\x. (\y.y) x@. A fresh type
-- variable is generated for the type of @x@, and another
-- for the type of @y@; the application of @(\y.y)@ to @x@
-- induces a subtyping constraint between the two type
-- variables. The most general type would be something
-- like @forall a b. (a <: b) => a -> b@, but we want to
-- avoid generating unnecessary subtyping constraints (the
-- type system might not even support subtyping qualifiers
-- like this). Instead, we unify the two type variables
-- and the resulting type is @forall a. a -> a@.
-- Get the successor and predecessor sets for all the type variables.
topRelMap :: RelMap
topRelMap
= RelMap
. M.map (uncurry Rels . (S.fromAscList *** S.fromAscList)
. partitionEithers . map uatomToEither . S.toList)
$ M.mapKeys (,SuperTy) subMap `M.union` M.mapKeys (,SubTy) superMap
subMap, superMap :: Map (Name Type) (Set UAtom)
(subMap, superMap) = (onlyVars *** onlyVars) $ G.cessors g
onlyVars :: Map UAtom (Set UAtom) -> Map (Name Type) (Set UAtom)
onlyVars = M.mapKeys fromVar . M.filterWithKey (\a _ -> uisVar a)
where
fromVar (UV x) = x
fromVar _ = error "Impossible! UB but uisVar."
go
:: Members '[Fresh, Error SolveError, Output Message] r
=> RelMap -> Sem r (Substitution BaseTy)
go relMap@(RelMap rm) = debugPretty relMap >> case as of
-- No variables left that have base type constraints.
[] -> return idS
-- Solve one variable at a time. See below.
(a:_) -> do
debug $ "Solving for" <+> pretty' a
case solveVar a of
Nothing -> do
debug $ "Couldn't solve for" <+> pretty' a
throw NoUnify
-- If we solved for a, delete it from the maps, apply the
-- resulting substitution to the remainder (updating the
-- relMap appropriately), and recurse. The substitution we
-- want will be the composition of the substitution for a
-- with the substitution generated by the recursive call.
--
-- Note we don't need to delete a from the TyVarInfoMap; we
-- never use the set of keys from the TyVarInfoMap for
-- anything (indeed, some variables might not be keys if
-- they have an empty sort), so it doesn't matter if old
-- variables hang around in it.
Just s -> do
debugPretty s
(@@ s) <$> go (substRel a (fromJust $ Subst.lookup (coerce a) s) relMap)
where
-- NOTE we can't solve a bunch in parallel! Might end up
-- assigning them conflicting solutions if some depend on
-- others. For example, consider the situation
--
-- Z
-- |
-- a3
-- / \
-- a1 N
--
-- If we try to solve in parallel we will end up assigning a1
-- -> Z (since it only has base types as an upper bound) and
-- a3 -> N (since it has both upper and lower bounds, and by
-- default we pick the lower bound), but this is wrong since
-- we should have a1 < a3.
--
-- If instead we solve them one at a time, we could e.g. first
-- solve a1 -> Z, and then we would find a3 -> Z as well.
-- Alternately, if we first solve a3 -> N then we will have a1
-- -> N as well. Both are acceptable.
--
-- In fact, this exact graph comes from (^x.x+1) which was
-- erroneously being inferred to have type Z -> N when I first
-- wrote the code.
-- Get only the variables we can solve on this pass, which
-- have base types in their predecessor or successor set. If
-- there are no such variables, then start picking any
-- remaining variables with a sort and pick types for them
-- (disco doesn't have qualified polymorphism so we can't just
-- leave them).
asBase
= map fst
. filter (not . S.null . baseRels . lkup "solveGraph.go.as" rm)
$ M.keys rm
as = case asBase of
[] -> filter ((/= topSort) . getSort vm) . map fst $ M.keys rm
_ -> asBase
-- Solve for a variable, failing if it has no solution, otherwise returning
-- a substitution for it.
solveVar :: Name Type -> Maybe (Substitution BaseTy)
solveVar v =
case ((v,SuperTy), (v,SubTy)) & over both (S.toList . baseRels . lkup "solveGraph.solveVar" rm) of
-- No sub- or supertypes; the only way this can happen is
-- if it has a nontrivial sort.
--
-- Traytel et al. don't seem to have a rule saying what to
-- do in this case (see Fig. 16 on p. 16 of their long
-- version). We used to just pick a type that inhabits
-- the sort, but this is wrong; see
-- https://github.com/disco-lang/disco/issues/192.
--
-- For now, let's assume that any situation in which we
-- have no base sub- or supertypes but we do have
-- nontrivial sorts means that we are dealing with numeric
-- types; so we can just call N a base subtype and go from there.
([], []) ->
-- Debug.trace (show v ++ " has no sub- or supertypes. Assuming N as a subtype.")
(coerce v |->) <$> lubBySort vm relMap [N] (getSort vm v)
(varRels (lkup "solveVar none, rels" rm (v,SubTy)))
-- Only supertypes. Just assign a to their inf, if one exists.
(bsupers, []) ->
-- Debug.trace (show v ++ " has only supertypes (" ++ show bsupers ++ ")") $
(coerce v |->) <$> glbBySort vm relMap bsupers (getSort vm v)
(varRels (lkup "solveVar bsupers, rels" rm (v,SuperTy)))
-- Only subtypes. Just assign a to their sup.
([], bsubs) ->
-- Debug.trace (show v ++ " has only subtypes (" ++ show bsubs ++ ")") $
-- Debug.trace ("sortmap: " ++ show vm) $
-- Debug.trace ("relmap: " ++ show relMap) $
-- Debug.trace ("sort for " ++ show v ++ ": " ++ show (getSort vm v)) $
-- Debug.trace ("relvars: " ++ show (varRels (relMap ! (v,SubTy)))) $
(coerce v |->) <$> lubBySort vm relMap bsubs (getSort vm v)
(varRels (lkup "solveVar bsubs, rels" rm (v,SubTy)))
-- Both successors and predecessors. Both must have a
-- valid bound, and the bounds must not overlap. Assign a
-- to the sup of its predecessors.
(bsupers, bsubs) -> do
ub <- glbBySort vm relMap bsupers (getSort vm v)
(varRels (rm ! (v,SuperTy)))
lb <- lubBySort vm relMap bsubs (getSort vm v)
(varRels (rm ! (v,SubTy)))
case isSubB lb ub of
True -> Just (coerce v |-> lb)
False -> Nothing