covenant-1.3.0: src/Covenant/Internal/Unification.hs
{-# LANGUAGE CPP #-}
module Covenant.Internal.Unification
( TypeAppError (..),
checkApp,
runUnifyM,
UnifyM,
-- These are exported for use with ASG helpers, largely (but not exclusively) the intro forms helper
unify,
substitute,
fixUp,
reconcile,
lookupDatatypeInfo,
concretifyFT,
)
where
#if __GLASGOW_HASKELL__==908
import Data.Foldable (foldl')
#endif
import Control.Applicative (Alternative ((<|>)))
import Control.Monad (foldM, unless, when)
import Control.Monad.Except (MonadError, catchError, throwError)
import Control.Monad.Reader (MonadReader, ReaderT (runReaderT), ask)
import Covenant.Data (DatatypeInfo)
import Covenant.Index (Index, intCount, intIndex)
import Covenant.Internal.Rename (RenameError, renameDatatypeInfo)
import Covenant.Internal.Type
( AbstractTy,
BuiltinFlatT,
CompT (CompT),
CompTBody (CompTBody),
DataDeclaration (OpaqueData),
Renamed (Rigid, Unifiable, Wildcard),
TyName,
ValT (Abstraction, BuiltinFlat, Datatype, ThunkT),
)
import Covenant.Type (CompT (CompN), CompTBody (ArgsAndResult))
import Data.Kind (Type)
import Data.Map (Map)
import Data.Map qualified as M
import Data.Map.Merge.Strict qualified as Merge
import Data.Map.Strict qualified as Map
import Data.Maybe (fromJust, mapMaybe)
import Data.Ord (comparing)
import Data.Set (Set)
import Data.Set qualified as Set
import Data.Text (Text)
import Data.Vector (Vector)
import Data.Vector qualified as Vector
import Data.Vector.NonEmpty (NonEmptyVector)
import Data.Vector.NonEmpty qualified as NonEmpty
import Data.Word (Word64)
import Optics.Core (ix, preview, view)
-- | Possible errors resulting from applications of arguments to functions.
--
-- @since 1.0.0
data TypeAppError
= -- | The final type after all arguments are applied is @forall a . a@.
LeakingUnifiable (Index "tyvar")
| -- | A wildcard (thus, a skolem) escaped its scope.
LeakingWildcard Word64 Int (Index "tyvar")
| -- | We were given too many arguments.
ExcessArgs (CompT Renamed) (Vector (Maybe (ValT Renamed)))
| -- | We weren't given enough arguments.
--
-- @since 1.1.0
InsufficientArgs Int (CompT Renamed) [Maybe (ValT Renamed)]
| -- | The expected type (first field) and actual type (second field) do not
-- unify.
DoesNotUnify (ValT Renamed) (ValT Renamed)
| -- | No datatype info associated with requested TyName
--
-- @since 1.1.0
NoDatatypeInfo TyName
| -- | No BB form. The only datatypes which should lack one are those isomorphic to `Void`.
--
-- @since 1.1.0
NoBBForm TyName
| -- | Datatype renaming failed.
--
-- @since 1.1.0
DatatypeInfoRenameFailed TyName RenameError
| -- | Something happened that definitely should not have. For right now, this means: The BB form of a datatype isn't a thunk
-- (but it might be useful to keep this around as a catchall for things that really shouldn't happen).
--
-- @since 1.1.0
ImpossibleHappened Text
| -- Could not reconcile two assignments with the same index
-- @since 1.2.0
CouldNotReconcile (Index "tyvar") (ValT Renamed) (ValT Renamed)
deriving stock
( -- | @since 1.0.0
Eq,
-- | @since 1.0.0
Show
)
{- This will probably only get used directly in testing and we'll use capabilities w/ the class everywhere else? -}
newtype UnifyM a = UnifyM (ReaderT (Map TyName (DatatypeInfo AbstractTy)) (Either TypeAppError) a)
deriving
( -- | @since 1.1.0
Functor,
Applicative,
Monad,
MonadReader (Map TyName (DatatypeInfo AbstractTy)),
MonadError TypeAppError
)
via (ReaderT (Map TyName (DatatypeInfo AbstractTy)) (Either TypeAppError))
runUnifyM :: Map TyName (DatatypeInfo AbstractTy) -> UnifyM a -> Either TypeAppError a
runUnifyM tyDict (UnifyM act) = runReaderT act tyDict
lookupDatatypeInfo ::
TyName ->
UnifyM (DatatypeInfo Renamed)
lookupDatatypeInfo tn =
ask >>= \tyDict -> case preview (ix tn) tyDict of
Nothing -> throwError . NoDatatypeInfo $ tn
Just dti -> renamedToUnify . renameDatatypeInfo $ dti
where
renamedToUnify :: Either RenameError (DatatypeInfo Renamed) -> UnifyM (DatatypeInfo Renamed)
renamedToUnify = either (throwError . DatatypeInfoRenameFailed tn) pure
lookupBBForm :: TyName -> UnifyM (ValT Renamed)
lookupBBForm tn =
lookupDatatypeInfo tn >>= \dti -> case view #bbForm dti of
Nothing -> throwError $ NoBBForm tn
Just bbForm -> pure bbForm
-- Opaque types do not (and cannot) have a BB form, which breaks unification machinery that assumes all inhabiated types
-- have such a form. We need to branch on the "Opacity" of a type in `expectDatatype` and this lets us do that
isOpaqueType :: TyName -> UnifyM Bool
isOpaqueType tn =
lookupDatatypeInfo tn >>= \dti -> case view #originalDecl dti of
OpaqueData {} -> pure True
_ -> pure False
-- | Given information about in-scope datatypes, a computation type, and a list
-- of arguments (some of which may be errors), try to construct the type of the
-- result of the application of those arguments to the computation.
--
-- @since 1.0.0
checkApp ::
Map TyName (DatatypeInfo AbstractTy) ->
CompT Renamed ->
[Maybe (ValT Renamed)] ->
Either TypeAppError (ValT Renamed)
checkApp tyDict f args = runUnifyM tyDict $ checkApp' f args
checkApp' ::
CompT Renamed ->
[Maybe (ValT Renamed)] ->
UnifyM (ValT Renamed)
checkApp' f@(CompT _ (CompTBody xs)) ys = do
let (curr, rest) = NonEmpty.uncons xs
numArgsExpected = NonEmpty.length xs - 1
numArgsActual = length ys
when (numArgsActual < numArgsExpected) $
throwError $
InsufficientArgs numArgsActual f ys
when (numArgsActual > numArgsExpected) $
throwError $
ExcessArgs f (Vector.fromList ys)
go curr (Vector.toList rest) ys
where
go ::
ValT Renamed ->
[ValT Renamed] ->
[Maybe (ValT Renamed)] ->
UnifyM (ValT Renamed)
go currParam restParams args = case restParams of
[] -> case args of
-- If we got here, currParam is the resulting type after all
-- substitutions have been applied.
[] -> fixUp currParam
_ -> throwError . ExcessArgs f . Vector.fromList $ args
_ -> case args of
[] -> throwError $ InsufficientArgs (length args) f args
(currArg : restArgs) -> do
newRestParams <- case currArg of
-- An error argument unifies with anything, as it's effectively
-- `forall a . a`. Furthermore, it requires no substitutional
-- changes. Thus, we can just skip it.
Nothing -> pure restParams
Just currArg' -> do
subs <- catchError (unify currParam currArg') (promoteUnificationError currParam currArg')
pure . Map.foldlWithKey' applySub restParams $ subs
case newRestParams of
[] -> throwError $ InsufficientArgs (length args) f args
(currParam' : restParams') -> go currParam' restParams' restArgs
-- Helpers
applySub ::
[ValT Renamed] ->
Index "tyvar" ->
ValT Renamed ->
[ValT Renamed]
applySub acc index sub = fmap (substitute index sub) acc
substitute ::
Index "tyvar" ->
ValT Renamed ->
ValT Renamed ->
ValT Renamed
substitute index toSub = \case
Abstraction t -> case t of
Unifiable ourIndex ->
if ourIndex == index
then toSub
else Abstraction t
_ -> Abstraction t
ThunkT (CompT abstractions (CompTBody xs)) ->
ThunkT . CompT abstractions . CompTBody . fmap (substitute index toSub) $ xs
BuiltinFlat t -> BuiltinFlat t
Datatype tn args -> Datatype tn $ substitute index toSub <$> args
-- Because unification is inherently recursive, if we find an error deep within
-- a type, the message will signify only the _part_ that fails to unify, not the
-- entire type. While potentially useful, this can be quite confusing,
-- especially with generated types. Thus, we use `catchError` with this
-- function, which effectively allows us to rename the types reported in
-- unification errors to whatever types 'wrap' them.
promoteUnificationError ::
ValT Renamed ->
ValT Renamed ->
TypeAppError ->
UnifyM a
promoteUnificationError topLevelExpected topLevelActual =
throwError . \case
DoesNotUnify _ _ -> DoesNotUnify topLevelExpected topLevelActual
err -> err
fixUp :: ValT Renamed -> UnifyM (ValT Renamed)
fixUp = \case
-- We're doing the equivalent of failing the `ST` trick
Abstraction (Wildcard scopeId trueLevel index) -> throwError . LeakingWildcard scopeId trueLevel $ index
-- We may have a result with fewer unifiables than we started with
-- This can be a problem, as we might be referring to unifiables that don't
-- exist anymore
ThunkT (CompT _ (CompTBody xs)) -> do
-- Figure out how many variables the thunk has to introduce now
let remainingUnifiables = NonEmpty.foldl' (\acc t -> acc <> collectUnifiables t) Set.empty xs
let requiredIntroductions = Set.size remainingUnifiables
-- We know that the size of a set can't be negative, but GHC doesn't.
let asCount = fromJust . preview intCount $ requiredIntroductions
-- Make enough indexes for us to use in one go
let indexesToUse = mapMaybe (preview intIndex) [0, 1 .. requiredIntroductions - 1]
-- Construct a mapping between old, possibly non-contiguous, unifiables and
-- our new ones
let renames =
zipWith
(\i replacement -> (i, Abstraction . Unifiable $ replacement))
(Set.toList remainingUnifiables)
indexesToUse
let fixed = fmap (\t -> foldl' (\acc (i, r) -> substitute i r acc) t renames) xs
pure . ThunkT . CompT asCount . CompTBody $ fixed
t -> pure t
collectUnifiables :: ValT Renamed -> Set (Index "tyvar")
collectUnifiables = \case
Abstraction t -> case t of
Unifiable index -> Set.singleton index
_ -> Set.empty
BuiltinFlat _ -> Set.empty
ThunkT (CompT _ (CompTBody xs)) -> NonEmpty.foldl' (\acc t -> acc <> collectUnifiables t) Set.empty xs
Datatype _ args -> Vector.foldl' (\acc t -> acc <> collectUnifiables t) Set.empty args
unify ::
ValT Renamed ->
ValT Renamed ->
UnifyM (Map (Index "tyvar") (ValT Renamed))
unify expected actual =
catchError
( case expected of
Abstraction t1 -> case t1 of
-- Unifiables unify with everything, and require a substitutional rewrite.
Unifiable index1 -> pure . Map.singleton index1 $ actual
Rigid level1 index1 -> expectRigid level1 index1
Wildcard scopeId1 _ index1 -> expectWildcard scopeId1 index1
ThunkT t1 -> expectThunk t1
BuiltinFlat t1 -> expectFlatBuiltin t1
Datatype tn xs -> expectDatatype tn xs
)
(promoteUnificationError expected actual)
where
unificationError :: forall (a :: Type). UnifyM a
unificationError = throwError . DoesNotUnify expected $ actual
noSubUnify :: forall (k :: Type) (a :: Type). UnifyM (Map k a)
noSubUnify = pure Map.empty
expectRigid ::
Int -> Index "tyvar" -> UnifyM (Map (Index "tyvar") (ValT Renamed))
-- Rigids behave identically to concrete types: they can unify with
-- themselves, or any other abstraction, but nothing else. No substitutional
-- rewrites are needed.
expectRigid level1 index1 = case actual of
Abstraction (Rigid level2 index2) ->
if level1 == level2 && index1 == index2
then noSubUnify
else unificationError
Abstraction _ -> noSubUnify
_ -> unificationError
expectWildcard ::
Word64 -> Index "tyvar" -> UnifyM (Map (Index "tyvar") (ValT Renamed))
-- Wildcards can unify with unifiables, as well as themselves, but nothing
-- else. No substitutional rewrites are needed.
expectWildcard scopeId1 index1 = case actual of
Abstraction (Unifiable _) -> noSubUnify
Abstraction (Wildcard scopeId2 _ index2) ->
if scopeId1 /= scopeId2 || index1 == index2
then noSubUnify
else unificationError
_ -> unificationError
expectThunk :: CompT Renamed -> UnifyM (Map (Index "tyvar") (ValT Renamed))
-- Thunks unify unconditionally with wildcards or unifiables. They unify
-- conditionally with other thunks, provided that we can unify each argument
-- with its counterpart in the same position, as well as their result types,
-- without conflicts.
expectThunk (CompT _ (CompTBody t1)) = case actual of
Abstraction (Rigid _ _) -> unificationError
Abstraction _ -> noSubUnify
ThunkT (CompT _ (CompTBody t2)) -> do
unless (comparing NonEmpty.length t1 t2 == EQ) unificationError
catchError
(foldM (\acc (l, r) -> unify l r >>= reconcile acc) Map.empty . NonEmpty.zip t1 $ t2)
(promoteUnificationError expected actual)
_ -> unificationError
expectFlatBuiltin :: BuiltinFlatT -> UnifyM (Map (Index "tyvar") (ValT Renamed))
-- 'Flat' builtins are always concrete. They can unify with themselves,
-- unifiables or wildcards, but nothing else. No substitutional rewrites are
-- needed.
expectFlatBuiltin t1 = case actual of
Abstraction (Rigid _ _) -> unificationError
Abstraction _ -> noSubUnify
BuiltinFlat t2 ->
if t1 == t2
then noSubUnify
else unificationError
_ -> unificationError
expectDatatype :: TyName -> Vector (ValT Renamed) -> UnifyM (Map (Index "tyvar") (ValT Renamed))
-- Datatypes unify with wildcards or unifiables, or other "suitable" instances of the same datatype.
-- Suitability with other datatypes is determined by converting to BB form, then concretifying
-- the BB form using the arguments to the actual datatype.
-- For example, the BB form of `Maybe` is: forall a r. r -> (a -> r) -> r
-- which, if we concretify while attempting to unify with `Maybe Int`, becomes: `forall r. r -> (Int -> r) -> r`
--
-- Opaque datatypes are a special exception and are treated analogously to Builtins: They unify only with themselves,
-- unifiables, or wildcards.
expectDatatype tn args = do
isOpaqueType tn >>= \case
False -> do
bbForm <- lookupBBForm tn
bbFormConcreteE <- concretify bbForm args
case actual of
Abstraction (Rigid _ _) -> unificationError
Abstraction _ -> noSubUnify
Datatype tn' args'
| tn' /= tn -> unificationError
| otherwise -> do
bbFormConcreteA <- concretify bbForm args'
unify bbFormConcreteE bbFormConcreteA
_ -> unificationError
True -> case actual of
Abstraction Rigid {} -> unificationError
Abstraction _ -> noSubUnify
-- Opaque datatypes cannot be parameterized, so we only need to check the TyName
Datatype tn' _args ->
if tn == tn'
then noSubUnify
else unificationError
_ -> unificationError
concretify :: ValT Renamed -> Vector (ValT Renamed) -> UnifyM (ValT Renamed)
concretify (ThunkT (CompT count (CompTBody fn))) args = fixUp $ ThunkT (CompT count (CompTBody newFn))
where
indexedArgs :: [(Index "tyvar", ValT Renamed)]
indexedArgs = Vector.toList $ Vector.imap (\i x -> (fromJust . preview intIndex $ i, x)) args
newFn :: NonEmptyVector (ValT Renamed)
newFn = go indexedArgs <$> fn
go :: [(Index "tyvar", ValT Renamed)] -> ValT Renamed -> ValT Renamed
go subs arg = foldl' (\val (i, concrete) -> substitute i concrete val) arg subs
concretify _ _ = throwError $ ImpossibleHappened "bbForm is not a thunk"
reconcile ::
Map (Index "tyvar") (ValT Renamed) ->
Map (Index "tyvar") (ValT Renamed) ->
UnifyM (Map (Index "tyvar") (ValT Renamed))
-- Note (Koz, 14/04/2025): This utter soup means the following:
--
-- - If the old map and the new map don't have any overlapping assignments,
-- just union them.
-- - Otherwise, for any assignment to a unifiable that is present in both
-- maps, ensure they assign to the same thing; if they do, it's fine,
-- otherwise we have a problem.
reconcile =
Merge.mergeA
Merge.preserveMissing
Merge.preserveMissing
(Merge.zipWithAMatched combineBindings)
where
combineBindings :: Index "tyvar" -> ValT Renamed -> ValT Renamed -> UnifyM (ValT Renamed)
combineBindings i old new =
if old == new
then pure old
else case old of
Abstraction (Unifiable _) -> pure new
_ -> case new of
Abstraction (Unifiable _) -> pure old
_ -> throwError $ CouldNotReconcile i old new
----- Extra stuff
concretifyFT ::
CompT Renamed ->
Vector (Maybe (ValT Renamed)) ->
CompT Renamed
concretifyFT (CompN cnt (ArgsAndResult fromFn res)) fromArgs = unfixedResult
where
unfixedResult :: CompT Renamed
unfixedResult = CompN cnt (ArgsAndResult subbedArgs subbedRes)
subbedArgs = substMany allSubstitutions <$> fromFn
subbedRes = substMany allSubstitutions res
substMany :: [(Index "tyvar", ValT Renamed)] -> ValT Renamed -> ValT Renamed
substMany subs val = foldl' (\acc (tv, ty) -> substitute tv ty acc) val subs
allUnifiables = Set.toList $ Vector.foldMap collectUnifiables fromFn
allSubstitutions = M.toList $ getInstantiations allUnifiables (Vector.toList fromFn) (Vector.toList fromArgs)
getInstantiations :: [Index "tyvar"] -> [ValT Renamed] -> [Maybe (ValT Renamed)] -> Map (Index "tyvar") (ValT Renamed)
getInstantiations [] _ _ = M.empty
getInstantiations _ [] _ = M.empty
getInstantiations _ _ [] = M.empty
getInstantiations vs (_ : fEs) (Nothing : aEs) = getInstantiations vs fEs aEs
getInstantiations (var : vars) fs@(fE : fEs) as@(aE' : aEs) =
-- somewhat subjective but I think doing it w/ fromJust makes the logic easier to follow here
let aE = fromJust aE'
in case instantiates (Unifiable var) aE fE of
Nothing -> getInstantiations [var] fEs aEs <> getInstantiations vars fs as
Just t -> M.insert var t $ getInstantiations vars fs as
instantiates ::
Renamed ->
ValT Renamed -> -- the "more concrete type", usually the actual argument from 'app'
ValT Renamed -> -- the "more polymorphic type', usually from the fn definition
Maybe (ValT Renamed)
instantiates var concrete abstract = case (concrete, abstract) of
(x, Abstraction a) -> if var == a then Just x else Nothing -- N.b. we need to be sure we only run this w/ unifiables as the first arg
(ThunkT (CompN _ concreteFn), ThunkT (CompN _ abstractFn)) ->
let concreteFn' = Vector.toList $ compTBodyToVec concreteFn
abstractFn' = Vector.toList $ compTBodyToVec abstractFn
in go concreteFn' abstractFn'
(Datatype tnC argsC, Datatype tnA argsA)
| tnC == tnA -> go (Vector.toList argsC) (Vector.toList argsA)
_ -> Nothing
where
go :: [ValT Renamed] -> [ValT Renamed] -> Maybe (ValT Renamed)
go [] _ = Nothing
go _ [] = Nothing
go (c : cs) (a : as) = instantiates var c a <|> go cs as
compTBodyToVec :: forall a. CompTBody a -> Vector (ValT a)
compTBodyToVec (ArgsAndResult args res) = Vector.snoc args res