-- | Implementation of unification and other core type system building
-- blocks.
module Language.Futhark.TypeChecker.Unify
  ( Constraint (..),
    Usage (..),
    mkUsage,
    mkUsage',
    Level,
    Constraints,
    MonadUnify (..),
    Rigidity (..),
    RigidSource (..),
    BreadCrumbs,
    sizeFree,
    noBreadCrumbs,
    hasNoBreadCrumbs,
    dimNotes,
    zeroOrderType,
    arrayElemType,
    mustHaveConstr,
    mustHaveField,
    mustBeOneOf,
    equalityType,
    normType,
    normTypeFully,
    unify,
    unifyMostCommon,
    doUnification,
  )
where

import Control.Monad
import Control.Monad.Except
import Control.Monad.Identity
import Control.Monad.Reader
import Control.Monad.State
import Data.List qualified as L
import Data.Map.Strict qualified as M
import Data.Maybe
import Data.Set qualified as S
import Data.Text qualified as T
import Futhark.Util (topologicalSort)
import Futhark.Util.Pretty
import Language.Futhark
import Language.Futhark.Traversals
import Language.Futhark.TypeChecker.Monad hiding (BoundV)
import Language.Futhark.TypeChecker.Types

-- | A piece of information that describes what process the type
-- checker currently performing.  This is used to give better error
-- messages for unification errors.
data BreadCrumb
  = MatchingTypes StructType StructType
  | MatchingFields [Name]
  | MatchingConstructor Name
  | Matching (Doc ())

instance Pretty BreadCrumb where
  pretty (MatchingTypes t1 t2) =
    "When matching type"
      </> indent 2 (pretty t1)
      </> "with"
      </> indent 2 (pretty t2)
  pretty (MatchingFields fields) =
    "When matching types of record field"
      <+> dquotes (mconcat $ punctuate "." $ map pretty fields)
      <> dot
  pretty (MatchingConstructor c) =
    "When matching types of constructor" <+> dquotes (pretty c) <> dot
  pretty (Matching s) =
    unAnnotate s

-- | Unification failures can occur deep down inside complicated types
-- (consider nested records).  We leave breadcrumbs behind us so we
-- can report the path we took to find the mismatch.
newtype BreadCrumbs = BreadCrumbs [BreadCrumb]

-- | An empty path.
noBreadCrumbs :: BreadCrumbs
noBreadCrumbs = BreadCrumbs []

-- | Is the path empty?
hasNoBreadCrumbs :: BreadCrumbs -> Bool
hasNoBreadCrumbs (BreadCrumbs xs) = null xs

-- | Drop a breadcrumb on the path behind you.
breadCrumb :: BreadCrumb -> BreadCrumbs -> BreadCrumbs
breadCrumb (MatchingFields xs) (BreadCrumbs (MatchingFields ys : bcs)) =
  BreadCrumbs $ MatchingFields (ys ++ xs) : bcs
breadCrumb bc (BreadCrumbs bcs) =
  BreadCrumbs $ bc : bcs

instance Pretty BreadCrumbs where
  pretty (BreadCrumbs []) = mempty
  pretty (BreadCrumbs bcs) = line <> stack (map pretty bcs)

-- | A usage that caused a type constraint.
data Usage = Usage (Maybe T.Text) Loc
  deriving (Show)

-- | Construct a 'Usage' from a location and a description.
mkUsage :: (Located a) => a -> T.Text -> Usage
mkUsage = flip (Usage . Just) . locOf

-- | Construct a 'Usage' that has just a location, but no particular
-- description.
mkUsage' :: (Located a) => a -> Usage
mkUsage' = Usage Nothing . locOf

instance Pretty Usage where
  pretty (Usage Nothing loc) = "use at " <> textwrap (locText loc)
  pretty (Usage (Just s) loc) = textwrap s <+> "at" <+> textwrap (locText loc)

instance Located Usage where
  locOf (Usage _ loc) = locOf loc

-- | The level at which a type variable is bound.  Higher means
-- deeper.  We can only unify a type variable at level @i@ with a type
-- @t@ if all type names that occur in @t@ are at most at level @i@.
type Level = Int

-- | A constraint on a yet-ambiguous type variable.
data Constraint
  = NoConstraint Liftedness Usage
  | ParamType Liftedness Loc
  | Constraint StructRetType Usage
  | Overloaded [PrimType] Usage
  | HasFields Liftedness (M.Map Name StructType) Usage
  | Equality Usage
  | HasConstrs Liftedness (M.Map Name [StructType]) Usage
  | ParamSize Loc
  | -- | Is not actually a type, but a term-level size,
    -- possibly already set to something specific.
    Size (Maybe Exp) Usage
  | -- | A size that does not unify with anything -
    -- created from the result of applying a function
    -- whose return size is existential, or otherwise
    -- hiding a size.
    UnknownSize Loc RigidSource
  deriving (Show)

instance Located Constraint where
  locOf (NoConstraint _ usage) = locOf usage
  locOf (ParamType _ usage) = locOf usage
  locOf (Constraint _ usage) = locOf usage
  locOf (Overloaded _ usage) = locOf usage
  locOf (HasFields _ _ usage) = locOf usage
  locOf (Equality usage) = locOf usage
  locOf (HasConstrs _ _ usage) = locOf usage
  locOf (ParamSize loc) = locOf loc
  locOf (Size _ usage) = locOf usage
  locOf (UnknownSize loc _) = locOf loc

-- | Mapping from fresh type variables, instantiated from the type
-- schemes of polymorphic functions, to (possibly) specific types as
-- determined on application and the location of that application, or
-- a partial constraint on their type.
type Constraints = M.Map VName (Level, Constraint)

lookupSubst :: VName -> Constraints -> Maybe (Subst StructRetType)
lookupSubst v constraints = case snd <$> M.lookup v constraints of
  Just (Constraint t _) -> Just $ Subst [] $ applySubst (`lookupSubst` constraints) t
  Just (Size (Just d) _) ->
    Just $ ExpSubst $ applySubst (`lookupSubst` constraints) d
  _ -> Nothing

-- | The source of a rigid size.
data RigidSource
  = -- | A function argument that is not a constant or variable name.
    RigidArg (Maybe (QualName VName)) T.Text
  | -- | An existential return size.
    RigidRet (Maybe (QualName VName))
  | -- | Similarly to 'RigidRet', but produce by a loop.
    RigidLoop
  | -- | Produced by a complicated slice expression.
    RigidSlice (Maybe Size) T.Text
  | -- | Produced by a complicated range expression.
    RigidRange
  | -- | Mismatch in branches.
    RigidCond StructType StructType
  | -- | Invented during unification.
    RigidUnify
  | -- | A name used in a size went out of scope.
    RigidOutOfScope Loc VName
  deriving (Eq, Ord, Show)

-- | The ridigity of a size variable.  All rigid sizes are tagged with
-- information about how they were generated.
data Rigidity = Rigid RigidSource | Nonrigid
  deriving (Eq, Ord, Show)

prettySource :: Loc -> Loc -> RigidSource -> Doc ()
prettySource ctx loc (RigidRet Nothing) =
  "is unknown size returned by function at"
    <+> pretty (locStrRel ctx loc)
    <> "."
prettySource ctx loc (RigidRet (Just fname)) =
  "is unknown size returned by"
    <+> dquotes (pretty fname)
    <+> "at"
    <+> pretty (locStrRel ctx loc)
    <> "."
prettySource ctx loc (RigidArg fname arg) =
  "is value of argument"
    </> indent 2 (shorten (pretty arg))
    </> "passed to"
    <+> fname'
    <+> "at"
    <+> pretty (locStrRel ctx loc)
    <> "."
  where
    fname' = maybe "function" (dquotes . pretty) fname
prettySource ctx loc (RigidSlice d slice) =
  "is size produced by slice"
    </> indent 2 (shorten (pretty slice))
    </> d_desc
    <> "at"
      <+> pretty (locStrRel ctx loc)
    <> "."
  where
    d_desc = case d of
      Just d' -> "of dimension of size " <> dquotes (pretty d') <> " "
      Nothing -> mempty
prettySource ctx loc RigidLoop =
  "is unknown size of value returned at" <+> pretty (locStrRel ctx loc) <> "."
prettySource ctx loc RigidRange =
  "is unknown length of range at" <+> pretty (locStrRel ctx loc) <> "."
prettySource ctx loc (RigidOutOfScope boundloc v) =
  "is an unknown size arising from "
    <> dquotes (prettyName v)
    <> " going out of scope at "
    <> pretty (locStrRel ctx loc)
    <> "."
      </> "Originally bound at "
    <> pretty (locStrRel ctx boundloc)
    <> "."
prettySource _ _ RigidUnify =
  "is an artificial size invented during unification of functions with anonymous sizes."
prettySource ctx loc (RigidCond t1 t2) =
  "is unknown due to conditional expression at "
    <> pretty (locStrRel ctx loc)
    <> "."
      </> "One branch returns array of type: "
    <> align (pretty t1)
      </> "The other an array of type:       "
    <> align (pretty t2)

-- | Retrieve notes describing the purpose or origin of the given
-- t'Size'.  The location is used as the *current* location, for the
-- purpose of reporting relative locations.
dimNotes :: (Located a, MonadUnify m) => a -> Exp -> m Notes
dimNotes ctx (Var d _ _) = do
  c <- M.lookup (qualLeaf d) <$> getConstraints
  case c of
    Just (_, UnknownSize loc rsrc) ->
      pure . aNote $
        dquotes (pretty d) <+> prettySource (locOf ctx) loc rsrc
    _ -> pure mempty
dimNotes _ _ = pure mempty

typeNotes :: (Located a, MonadUnify m) => a -> StructType -> m Notes
typeNotes ctx =
  fmap mconcat
    . mapM (dimNotes ctx . flip sizeFromName mempty . qualName)
    . S.toList
    . fvVars
    . freeInType

typeVarNotes :: (MonadUnify m) => VName -> m Notes
typeVarNotes v = maybe mempty (note . snd) . M.lookup v <$> getConstraints
  where
    note (HasConstrs _ cs _) =
      aNote $
        prettyName v
          <+> "="
          <+> hsep (map ppConstr (M.toList cs))
          <+> "..."
    note (Overloaded ts _) =
      aNote $ prettyName v <+> "must be one of" <+> mconcat (punctuate ", " (map pretty ts))
    note (HasFields _ fs _) =
      aNote $
        prettyName v
          <+> "="
          <+> braces (mconcat (punctuate ", " (map ppField (M.toList fs))))
    note _ = mempty

    ppConstr (c, _) = "#" <> pretty c <+> "..." <+> "|"
    ppField (f, _) = prettyName f <> ":" <+> "..."

-- | Monads that which to perform unification must implement this type
-- class.
class (Monad m) => MonadUnify m where
  getConstraints :: m Constraints
  putConstraints :: Constraints -> m ()
  modifyConstraints :: (Constraints -> Constraints) -> m ()
  modifyConstraints f = do
    x <- getConstraints
    putConstraints $ f x

  newTypeVar :: (Monoid als, Located a) => a -> Name -> m (TypeBase dim als)
  newDimVar :: Usage -> Rigidity -> Name -> m VName
  newRigidDim :: (Located a) => a -> RigidSource -> Name -> m VName
  newRigidDim loc = newDimVar (mkUsage' loc) . Rigid
  newFlexibleDim :: Usage -> Name -> m VName
  newFlexibleDim usage = newDimVar usage Nonrigid

  curLevel :: m Level

  matchError ::
    (Located loc) =>
    loc ->
    Notes ->
    BreadCrumbs ->
    StructType ->
    StructType ->
    m a

  unifyError ::
    (Located loc) =>
    loc ->
    Notes ->
    BreadCrumbs ->
    Doc () ->
    m a

-- | Replace all type variables with their substitution.
normTypeFully :: (Substitutable a, MonadUnify m) => a -> m a
normTypeFully t = do
  constraints <- getConstraints
  pure $ applySubst (`lookupSubst` constraints) t

-- | Replace any top-level type variable with its substitution.
normType :: (MonadUnify m) => StructType -> m StructType
normType t@(Scalar (TypeVar _ (QualName [] v) [])) = do
  constraints <- getConstraints
  case snd <$> M.lookup v constraints of
    Just (Constraint (RetType [] t') _) -> normType t'
    _ -> pure t
normType t = pure t

rigidConstraint :: Constraint -> Bool
rigidConstraint ParamType {} = True
rigidConstraint ParamSize {} = True
rigidConstraint UnknownSize {} = True
rigidConstraint _ = False

unsharedConstructorsMsg :: M.Map Name t -> M.Map Name t -> Doc a
unsharedConstructorsMsg cs1 cs2 =
  "Unshared constructors:" <+> commasep (map (("#" <>) . pretty) missing) <> "."
  where
    missing =
      filter (`notElem` M.keys cs1) (M.keys cs2)
        ++ filter (`notElem` M.keys cs2) (M.keys cs1)

-- | Is the given type variable the name of an abstract type or type
-- parameter, which we cannot substitute?
isRigid :: VName -> Constraints -> Bool
isRigid v constraints =
  maybe True (rigidConstraint . snd) $ M.lookup v constraints

-- | If the given type variable is nonrigid, what is its level?
isNonRigid :: VName -> Constraints -> Maybe Level
isNonRigid v constraints = do
  (lvl, c) <- M.lookup v constraints
  guard $ not $ rigidConstraint c
  pure lvl

type UnifySizes m =
  BreadCrumbs -> [VName] -> (VName -> Maybe Int) -> Exp -> Exp -> m ()

flipUnifySizes :: UnifySizes m -> UnifySizes m
flipUnifySizes onDims bcs bound nonrigid t1 t2 =
  onDims bcs bound nonrigid t2 t1

unifyWith ::
  (MonadUnify m) =>
  UnifySizes m ->
  Usage ->
  [VName] ->
  BreadCrumbs ->
  StructType ->
  StructType ->
  m ()
unifyWith onDims usage = subunify False
  where
    swap True x y = (y, x)
    swap False x y = (x, y)

    subunify ord bound bcs t1 t2 = do
      constraints <- getConstraints

      t1' <- normType t1
      t2' <- normType t2

      let nonrigid v = isNonRigid v constraints

          failure = matchError (srclocOf usage) mempty bcs t1' t2'

          link ord' =
            linkVarToType linkDims usage bound bcs
            where
              -- We may have to flip the order of future calls to
              -- onDims inside linkVarToType.
              linkDims
                | ord' = flipUnifySizes onDims
                | otherwise = onDims

          unifyTypeArg bcs' (TypeArgDim d1) (TypeArgDim d2) =
            onDims' bcs' (swap ord d1 d2)
          unifyTypeArg bcs' (TypeArgType t) (TypeArgType arg_t) =
            subunify ord bound bcs' t arg_t
          unifyTypeArg bcs' _ _ =
            unifyError
              usage
              mempty
              bcs'
              "Cannot unify a type argument with a dimension argument (or vice versa)."

          onDims' bcs' (d1, d2) =
            onDims
              bcs'
              bound
              nonrigid
              (applySubst (`lookupSubst` constraints) d1)
              (applySubst (`lookupSubst` constraints) d2)

      case (t1', t2') of
        (Scalar (Prim pt1), Scalar (Prim pt2))
          | pt1 == pt2 -> pure ()
        ( Scalar (Record fs),
          Scalar (Record arg_fs)
          )
            | M.keys fs == M.keys arg_fs ->
                unifySharedFields onDims usage bound bcs fs arg_fs
            | otherwise -> do
                let missing =
                      filter (`notElem` M.keys arg_fs) (M.keys fs)
                        ++ filter (`notElem` M.keys fs) (M.keys arg_fs)
                unifyError usage mempty bcs $
                  "Unshared fields:" <+> commasep (map pretty missing) <> "."
        ( Scalar (TypeVar _ (QualName _ tn) targs),
          Scalar (TypeVar _ (QualName _ arg_tn) arg_targs)
          )
            | tn == arg_tn,
              length targs == length arg_targs -> do
                let bcs' = breadCrumb (Matching "When matching type arguments.") bcs
                zipWithM_ (unifyTypeArg bcs') targs arg_targs
        ( Scalar (TypeVar _ (QualName [] v1) []),
          Scalar (TypeVar _ (QualName [] v2) [])
          ) ->
            case (nonrigid v1, nonrigid v2) of
              (Nothing, Nothing) -> failure
              (Just lvl1, Nothing) -> link ord v1 lvl1 t2'
              (Nothing, Just lvl2) -> link (not ord) v2 lvl2 t1'
              (Just lvl1, Just lvl2)
                | lvl1 <= lvl2 -> link ord v1 lvl1 t2'
                | otherwise -> link (not ord) v2 lvl2 t1'
        (Scalar (TypeVar _ (QualName [] v1) []), _)
          | Just lvl <- nonrigid v1 ->
              link ord v1 lvl t2'
        (_, Scalar (TypeVar _ (QualName [] v2) []))
          | Just lvl <- nonrigid v2 ->
              link (not ord) v2 lvl t1'
        ( Scalar (Arrow _ p1 d1 a1 (RetType b1_dims b1)),
          Scalar (Arrow _ p2 d2 a2 (RetType b2_dims b2))
          )
            | uncurry (<) $ swap ord d1 d2 -> do
                unifyError usage mempty bcs . withIndexLink "unify-consuming-param" $
                  "Parameters"
                    </> indent 2 (pretty d1 <> pretty a1)
                    </> "and"
                    </> indent 2 (pretty d2 <> pretty a2)
                    </> "are incompatible regarding consuming their arguments."
            | uncurry (<) $ swap ord (uniqueness b2) (uniqueness b1) -> do
                unifyError usage mempty bcs . withIndexLink "unify-return-uniqueness" $
                  "Return types"
                    </> indent 2 (pretty b1)
                    </> "and"
                    </> indent 2 (pretty b2)
                    </> "have incompatible uniqueness."
            | otherwise -> do
                -- Introduce the existentials as size variables so they
                -- are subject to unification.  We will remove them again
                -- afterwards.
                let (r1, r2) =
                      swap
                        ord
                        (Size Nothing $ Usage Nothing mempty)
                        (UnknownSize mempty RigidUnify)
                lvl <- curLevel
                modifyConstraints (M.fromList (map (,(lvl, r1)) b1_dims) <>)
                modifyConstraints (M.fromList (map (,(lvl, r2)) b2_dims) <>)

                let bound' = bound <> mapMaybe pname [p1, p2] <> b1_dims <> b2_dims
                subunify
                  (not ord)
                  bound
                  (breadCrumb (Matching "When matching parameter types.") bcs)
                  a1
                  a2
                subunify
                  ord
                  bound'
                  (breadCrumb (Matching "When matching return types.") bcs)
                  (toStruct b1')
                  (toStruct b2')

                -- Delete the size variables we introduced to represent
                -- the existential sizes.
                modifyConstraints $ \m -> L.foldl' (flip M.delete) m (b1_dims <> b2_dims)
            where
              (b1', b2') =
                -- Replace one parameter name with the other in the
                -- return type, in case of dependent types.  I.e.,
                -- we want type '(n: i32) -> [n]i32' to unify with
                -- type '(x: i32) -> [x]i32'.
                case (p1, p2) of
                  (Named p1', Named p2') ->
                    let f v
                          | v == p2' = Just $ ExpSubst $ sizeFromName (qualName p1') mempty
                          | otherwise = Nothing
                     in (b1, applySubst f b2)
                  (_, _) ->
                    (b1, b2)

              pname (Named x) = Just x
              pname Unnamed = Nothing
        (Array {}, Array {})
          | Shape (t1_d : _) <- arrayShape t1',
            Shape (t2_d : _) <- arrayShape t2',
            Just t1'' <- peelArray 1 t1',
            Just t2'' <- peelArray 1 t2' -> do
              onDims' bcs (swap ord t1_d t2_d)
              subunify ord bound bcs t1'' t2''
        ( Scalar (Sum cs),
          Scalar (Sum arg_cs)
          )
            | M.keys cs == M.keys arg_cs ->
                unifySharedConstructors onDims usage bound bcs cs arg_cs
            | otherwise ->
                unifyError usage mempty bcs $ unsharedConstructorsMsg arg_cs cs
        _ -> failure

anyBound :: [VName] -> ExpBase Info VName -> Bool
anyBound bound e = any (`S.member` fvVars (freeInExp e)) bound

unifySizes :: (MonadUnify m) => Usage -> UnifySizes m
unifySizes usage bcs bound nonrigid e1 e2
  | Just es <- similarExps e1 e2 =
      mapM_ (uncurry $ unifySizes usage bcs bound nonrigid) es
unifySizes usage bcs bound nonrigid (Var v1 _ _) e2
  | Just lvl1 <- nonrigid (qualLeaf v1),
    not (anyBound bound e2) || (qualLeaf v1 `elem` bound) =
      linkVarToDim usage bcs (qualLeaf v1) lvl1 e2
unifySizes usage bcs bound nonrigid e1 (Var v2 _ _)
  | Just lvl2 <- nonrigid (qualLeaf v2),
    not (anyBound bound e1) || (qualLeaf v2 `elem` bound) =
      linkVarToDim usage bcs (qualLeaf v2) lvl2 e1
unifySizes usage bcs _ _ e1 e2 = do
  notes <- (<>) <$> dimNotes usage e2 <*> dimNotes usage e2
  unifyError usage notes bcs $
    "Sizes"
      <+> dquotes (pretty e1)
      <+> "and"
      <+> dquotes (pretty e2)
      <+> "do not match."

-- | Unifies two types.
unify :: (MonadUnify m) => Usage -> StructType -> StructType -> m ()
unify usage = unifyWith (unifySizes usage) usage mempty noBreadCrumbs

occursCheck ::
  (MonadUnify m) =>
  Usage ->
  BreadCrumbs ->
  VName ->
  StructType ->
  m ()
occursCheck usage bcs vn tp =
  when (vn `S.member` typeVars tp) $
    unifyError usage mempty bcs $
      "Occurs check: cannot instantiate"
        <+> prettyName vn
        <+> "with"
        <+> pretty tp
        <> "."

scopeCheck ::
  (MonadUnify m) =>
  Usage ->
  BreadCrumbs ->
  VName ->
  Level ->
  StructType ->
  m ()
scopeCheck usage bcs vn max_lvl tp = do
  constraints <- getConstraints
  checkType constraints tp
  where
    checkType constraints t =
      mapM_ (check constraints) $ typeVars t <> fvVars (freeInType t)

    check constraints v
      | Just (lvl, c) <- M.lookup v constraints,
        lvl > max_lvl =
          if rigidConstraint c
            then scopeViolation v
            else modifyConstraints $ M.insert v (max_lvl, c)
      | otherwise =
          pure ()

    scopeViolation v = do
      notes <- typeNotes usage tp
      unifyError usage notes bcs $
        "Cannot unify type"
          </> indent 2 (pretty tp)
          </> "with"
          <+> dquotes (prettyName vn)
          <+> "(scope violation)."
          </> "This is because"
          <+> dquotes (prettyName v)
          <+> "is rigidly bound in a deeper scope."

-- Expressions witnessed by type, topologically sorted.
topWit :: TypeBase Exp u -> [Exp]
topWit = topologicalSort depends . witnessedExps
  where
    witnessedExps t = execState (traverseDims onDim t) mempty
      where
        onDim _ PosImmediate e = modify (e :)
        onDim _ _ _ = pure ()
    depends a b = any (sameExp b) $ subExps a

sizeFree ::
  (MonadUnify m) =>
  SrcLoc ->
  (Exp -> Maybe VName) ->
  TypeBase Size u ->
  m (TypeBase Size u, [VName])
sizeFree tloc expKiller orig_t = do
  runReaderT (toBeReplaced orig_t $ onType orig_t) mempty `runStateT` mempty
  where
    lookReplacement e repl = snd <$> L.find (sameExp e . fst) repl
    expReplace mapping e
      | Just e' <- lookReplacement e mapping = e'
      | otherwise = runIdentity $ astMap mapper e
      where
        mapper = identityMapper {mapOnExp = pure . expReplace mapping}

    replacing e = do
      e' <- asks (`expReplace` e)
      case expKiller e' of
        Nothing -> pure e'
        Just cause -> do
          vn <- lift $ lift $ newRigidDim tloc (RigidOutOfScope (locOf e) cause) "d"
          modify (vn :)
          pure $ sizeFromName (qualName vn) (srclocOf e)

    toBeReplaced t m' = foldl f m' $ topWit t
      where
        f m e = do
          e' <- replacing e
          local ((e, e') :) m

    onScalar (Record fs) =
      Record <$> traverse onType fs
    onScalar (Sum cs) =
      Sum <$> (traverse . traverse) onType cs
    onScalar (Arrow as pn d argT (RetType dims retT)) = do
      argT' <- onType argT
      old_bound <- get
      retT' <- toBeReplaced retT $ onType retT
      rl <- state $ L.partition (`notElem` old_bound)
      let dims' = dims <> rl
      pure $ Arrow as pn d argT' (RetType dims' retT')
    onScalar (TypeVar u v args) =
      TypeVar u v <$> mapM onTypeArg args
      where
        onTypeArg (TypeArgDim d) = TypeArgDim <$> replacing d
        onTypeArg (TypeArgType ty) = TypeArgType <$> onType ty
    onScalar (Prim pt) = pure $ Prim pt

    onType ::
      (MonadUnify m) =>
      TypeBase Size u ->
      ReaderT [(Exp, Exp)] (StateT [VName] m) (TypeBase Size u)
    onType (Array u shape scalar) =
      Array u <$> traverse replacing shape <*> onScalar scalar
    onType (Scalar ty) =
      Scalar <$> onScalar ty

linkVarToType ::
  (MonadUnify m) =>
  UnifySizes m ->
  Usage ->
  [VName] ->
  BreadCrumbs ->
  VName ->
  Level ->
  StructType ->
  m ()
linkVarToType onDims usage bound bcs vn lvl tp_unnorm = do
  -- We have to expand anyway for the occurs check, so we might as
  -- well link the fully expanded type.
  tp <- normTypeFully tp_unnorm
  occursCheck usage bcs vn tp
  scopeCheck usage bcs vn lvl tp

  let link = do
        let (witnessed, not_witnessed) = determineSizeWitnesses tp
            used v = v `S.member` witnessed || v `S.member` not_witnessed
            (ext_witnessed, ext_not_witnessed) =
              L.partition (`elem` witnessed) $ filter used bound

            -- Any size that uses an ext_not_witnessed variable must
            -- be replaced with a fresh existential.
            problematic e =
              L.find (`elem` ext_not_witnessed) $
                S.toList $
                  fvVars $
                    freeInExp e

        (tp', ext_new) <- sizeFree (srclocOf usage) problematic tp

        modifyConstraints $
          M.insert vn (lvl, Constraint (RetType (ext_new <> ext_witnessed) tp') usage)

  let unliftedBcs unlifted_usage =
        breadCrumb
          ( Matching $
              "When verifying that"
                <+> dquotes (prettyName vn)
                <+> textwrap "is not instantiated with a function type, due to"
                <+> pretty unlifted_usage
          )
          bcs

  constraints <- getConstraints
  case snd <$> M.lookup vn constraints of
    Just (NoConstraint Unlifted unlift_usage) -> do
      link

      arrayElemTypeWith usage (unliftedBcs unlift_usage) tp
      when (any (`elem` bound) (fvVars (freeInType tp))) $
        unifyError usage mempty bcs $
          "Type variable"
            <+> prettyName vn
            <+> "cannot be instantiated with type containing anonymous sizes:"
            </> indent 2 (pretty tp)
            </> textwrap "This is usually because the size of an array returned by a higher-order function argument cannot be determined statically.  This can also be due to the return size being a value parameter.  Add type annotation to clarify."
    Just (Equality _) -> do
      link
      equalityType usage tp
    Just (Overloaded ts old_usage)
      | tp `notElem` map (Scalar . Prim) ts -> do
          link
          case tp of
            Scalar (TypeVar _ (QualName [] v) [])
              | not $ isRigid v constraints ->
                  linkVarToTypes usage v ts
            _ ->
              unifyError usage mempty bcs $
                "Cannot instantiate"
                  <+> dquotes (prettyName vn)
                  <+> "with type"
                  </> indent 2 (pretty tp)
                  </> "as"
                  <+> dquotes (prettyName vn)
                  <+> "must be one of"
                  <+> commasep (map pretty ts)
                  </> "due to"
                  <+> pretty old_usage
                  <> "."
    Just (HasFields l required_fields old_usage) -> do
      when (l == Unlifted) $ arrayElemTypeWith usage (unliftedBcs old_usage) tp
      case tp of
        Scalar (Record tp_fields)
          | all (`M.member` tp_fields) $ M.keys required_fields -> do
              required_fields' <- mapM normTypeFully required_fields
              let tp' = Scalar $ Record $ required_fields <> tp_fields -- Crucially left-biased.
                  ext = filter (`S.member` fvVars (freeInType tp')) bound
              modifyConstraints $
                M.insert vn (lvl, Constraint (RetType ext tp') usage)
              unifySharedFields onDims usage bound bcs required_fields' tp_fields
        Scalar (TypeVar _ (QualName [] v) []) -> do
          case M.lookup v constraints of
            Just (_, HasFields _ tp_fields _) ->
              unifySharedFields onDims usage bound bcs required_fields tp_fields
            Just (_, NoConstraint {}) -> pure ()
            Just (_, Equality {}) -> pure ()
            _ -> do
              notes <- (<>) <$> typeVarNotes vn <*> typeVarNotes v
              noRecordType notes
          link
          modifyConstraints $
            M.insertWith
              combineFields
              v
              (lvl, HasFields l required_fields old_usage)
          where
            combineFields (_, HasFields l1 fs1 usage1) (_, HasFields l2 fs2 _) =
              (lvl, HasFields (l1 `min` l2) (M.union fs1 fs2) usage1)
            combineFields hasfs _ = hasfs
        _ ->
          unifyError usage mempty bcs $
            "Cannot instantiate"
              <+> dquotes (prettyName vn)
              <+> "with type"
              </> indent 2 (pretty tp)
              </> "as"
              <+> dquotes (prettyName vn)
              <+> "must be a record with fields"
              </> indent 2 (pretty (Record required_fields))
              </> "due to"
              <+> pretty old_usage
              <> "."
    -- See Note [Linking variables to sum types]
    Just (HasConstrs l required_cs old_usage) -> do
      when (l == Unlifted) $ arrayElemTypeWith usage (unliftedBcs old_usage) tp
      case tp of
        Scalar (Sum ts)
          | all (`M.member` ts) $ M.keys required_cs -> do
              let tp' = Scalar $ Sum $ required_cs <> ts -- Crucially left-biased.
                  ext = filter (`S.member` fvVars (freeInType tp')) bound
              modifyConstraints $
                M.insert vn (lvl, Constraint (RetType ext tp') usage)
              unifySharedConstructors onDims usage bound bcs required_cs ts
          | otherwise ->
              unsharedConstructors required_cs ts =<< typeVarNotes vn
        Scalar (TypeVar _ (QualName [] v) []) -> do
          case M.lookup v constraints of
            Just (_, HasConstrs _ v_cs _) ->
              unifySharedConstructors onDims usage bound bcs required_cs v_cs
            Just (_, NoConstraint {}) -> pure ()
            Just (_, Equality {}) -> pure ()
            _ -> do
              notes <- (<>) <$> typeVarNotes vn <*> typeVarNotes v
              noSumType notes
          link
          modifyConstraints $
            M.insertWith
              combineConstrs
              v
              (lvl, HasConstrs l required_cs old_usage)
          where
            combineConstrs (_, HasConstrs l1 cs1 usage1) (_, HasConstrs l2 cs2 _) =
              (lvl, HasConstrs (l1 `min` l2) (M.union cs1 cs2) usage1)
            combineConstrs hasCs _ = hasCs
        _ -> noSumType =<< typeVarNotes vn
    _ -> link
  where
    unsharedConstructors cs1 cs2 notes =
      unifyError
        usage
        notes
        bcs
        (unsharedConstructorsMsg cs1 cs2)
    noSumType notes =
      unifyError
        usage
        notes
        bcs
        "Cannot unify a sum type with a non-sum type."
    noRecordType notes =
      unifyError
        usage
        notes
        bcs
        "Cannot unify a record type with a non-record type."

linkVarToDim ::
  (MonadUnify m) =>
  Usage ->
  BreadCrumbs ->
  VName ->
  Level ->
  Exp ->
  m ()
linkVarToDim usage bcs vn lvl e = do
  constraints <- getConstraints

  mapM_ (checkVar constraints) $ fvVars $ freeInExp e

  modifyConstraints $ M.insert vn (lvl, Size (Just e) usage)
  where
    checkVar _ dim'
      | vn == dim' = do
          notes <- dimNotes usage e
          unifyError usage notes bcs $
            "Occurs check: cannot instantiate"
              <+> dquotes (prettyName vn)
              <+> "with"
              <+> dquotes (pretty e)
              <+> "."
    checkVar constraints dim'
      | Just (dim_lvl, c) <- dim' `M.lookup` constraints,
        dim_lvl >= lvl =
          case c of
            ParamSize {} -> do
              notes <- dimNotes usage e
              unifyError usage notes bcs $
                "Cannot link size"
                  <+> dquotes (prettyName vn)
                  <+> "to"
                  <+> dquotes (pretty e)
                  <+> "(scope violation)."
                  </> "This is because"
                  <+> dquotes (pretty $ qualName dim')
                  <+> "is not in scope when"
                  <+> dquotes (prettyName vn)
                  <+> "is introduced."
            _ -> modifyConstraints $ M.insert dim' (lvl, c)
    checkVar _ _ = pure ()

-- | Assert that this type must be one of the given primitive types.
mustBeOneOf :: (MonadUnify m) => [PrimType] -> Usage -> StructType -> m ()
mustBeOneOf [req_t] usage t = unify usage (Scalar (Prim req_t)) t
mustBeOneOf ts usage t = do
  t' <- normType t
  constraints <- getConstraints
  let isRigid' v = isRigid v constraints

  case t' of
    Scalar (TypeVar _ (QualName [] v) [])
      | not $ isRigid' v -> linkVarToTypes usage v ts
    Scalar (Prim pt) | pt `elem` ts -> pure ()
    _ -> failure
  where
    failure =
      unifyError usage mempty noBreadCrumbs $
        "Cannot unify type"
          <+> dquotes (pretty t)
          <+> "with any of "
          <> commasep (map pretty ts)
          <> "."

linkVarToTypes :: (MonadUnify m) => Usage -> VName -> [PrimType] -> m ()
linkVarToTypes usage vn ts = do
  vn_constraint <- M.lookup vn <$> getConstraints
  case vn_constraint of
    Just (lvl, Overloaded vn_ts vn_usage) ->
      case ts `L.intersect` vn_ts of
        [] ->
          unifyError usage mempty noBreadCrumbs $
            "Type constrained to one of"
              <+> commasep (map pretty ts)
              <+> "but also one of"
              <+> commasep (map pretty vn_ts)
              <+> "due to"
              <+> pretty vn_usage
              <> "."
        ts' -> modifyConstraints $ M.insert vn (lvl, Overloaded ts' usage)
    Just (_, HasConstrs _ _ vn_usage) ->
      unifyError usage mempty noBreadCrumbs $
        "Type constrained to one of"
          <+> commasep (map pretty ts)
          <> ", but also inferred to be sum type due to"
            <+> pretty vn_usage
          <> "."
    Just (_, HasFields _ _ vn_usage) ->
      unifyError usage mempty noBreadCrumbs $
        "Type constrained to one of"
          <+> commasep (map pretty ts)
          <> ", but also inferred to be record due to"
            <+> pretty vn_usage
          <> "."
    Just (lvl, _) -> modifyConstraints $ M.insert vn (lvl, Overloaded ts usage)
    Nothing ->
      unifyError usage mempty noBreadCrumbs $
        "Cannot constrain type to one of" <+> commasep (map pretty ts)

-- | Assert that this type must support equality.
equalityType ::
  (MonadUnify m, Pretty (Shape dim), Pretty u) =>
  Usage ->
  TypeBase dim u ->
  m ()
equalityType usage t = do
  unless (orderZero t) $
    unifyError usage mempty noBreadCrumbs $
      "Type " <+> dquotes (pretty t) <+> "does not support equality (may contain function)."
  mapM_ mustBeEquality $ typeVars t
  where
    mustBeEquality vn = do
      constraints <- getConstraints
      case M.lookup vn constraints of
        Just (_, Constraint (RetType [] (Scalar (TypeVar _ (QualName [] vn') []))) _) ->
          mustBeEquality vn'
        Just (_, Constraint (RetType _ vn_t) cusage)
          | not $ orderZero vn_t ->
              unifyError usage mempty noBreadCrumbs $
                "Type"
                  <+> dquotes (pretty t)
                  <+> "does not support equality."
                  </> "Constrained to be higher-order due to"
                  <+> pretty cusage
                  <+> "."
          | otherwise -> pure ()
        Just (lvl, NoConstraint _ _) ->
          modifyConstraints $ M.insert vn (lvl, Equality usage)
        Just (_, Overloaded _ _) ->
          pure () -- All primtypes support equality.
        Just (_, Equality {}) ->
          pure ()
        _ ->
          unifyError usage mempty noBreadCrumbs $
            "Type" <+> prettyName vn <+> "does not support equality."

zeroOrderTypeWith ::
  (MonadUnify m) =>
  Usage ->
  BreadCrumbs ->
  StructType ->
  m ()
zeroOrderTypeWith usage bcs t = do
  unless (orderZero t) $
    unifyError usage mempty bcs $
      "Type" </> indent 2 (pretty t) </> "found to be functional."
  mapM_ mustBeZeroOrder . S.toList . typeVars =<< normType t
  where
    mustBeZeroOrder vn = do
      constraints <- getConstraints
      case M.lookup vn constraints of
        Just (lvl, NoConstraint _ _) ->
          modifyConstraints $ M.insert vn (lvl, NoConstraint Unlifted usage)
        Just (lvl, HasFields _ fs _) ->
          modifyConstraints $ M.insert vn (lvl, HasFields Unlifted fs usage)
        Just (lvl, HasConstrs _ cs _) ->
          modifyConstraints $ M.insert vn (lvl, HasConstrs Unlifted cs usage)
        Just (_, ParamType Lifted ploc) ->
          unifyError usage mempty bcs $
            "Type parameter"
              <+> dquotes (prettyName vn)
              <+> "at"
              <+> pretty (locStr ploc)
              <+> "may be a function."
        _ -> pure ()

-- | Assert that this type must be zero-order.
zeroOrderType ::
  (MonadUnify m) => Usage -> T.Text -> StructType -> m ()
zeroOrderType usage desc =
  zeroOrderTypeWith usage $ breadCrumb bc noBreadCrumbs
  where
    bc = Matching $ "When checking" <+> textwrap desc

arrayElemTypeWith ::
  (MonadUnify m, Pretty (Shape dim), Pretty u) =>
  Usage ->
  BreadCrumbs ->
  TypeBase dim u ->
  m ()
arrayElemTypeWith usage bcs t = do
  unless (orderZero t) $
    unifyError usage mempty bcs $
      "Type" </> indent 2 (pretty t) </> "found to be functional."
  mapM_ mustBeZeroOrder . S.toList . typeVars $ t
  where
    mustBeZeroOrder vn = do
      constraints <- getConstraints
      case M.lookup vn constraints of
        Just (lvl, NoConstraint _ _) ->
          modifyConstraints $ M.insert vn (lvl, NoConstraint Unlifted usage)
        Just (_, ParamType l ploc)
          | l `elem` [Lifted, SizeLifted] ->
              unifyError usage mempty bcs $
                "Type parameter"
                  <+> dquotes (prettyName vn)
                  <+> "bound at"
                  <+> pretty (locStr ploc)
                  <+> "is lifted and cannot be an array element."
        _ -> pure ()

-- | Assert that this type must be valid as an array element.
arrayElemType ::
  (MonadUnify m, Pretty (Shape dim), Pretty u) =>
  Usage ->
  T.Text ->
  TypeBase dim u ->
  m ()
arrayElemType usage desc =
  arrayElemTypeWith usage $ breadCrumb bc noBreadCrumbs
  where
    bc = Matching $ "When checking" <+> textwrap desc

unifySharedFields ::
  (MonadUnify m) =>
  UnifySizes m ->
  Usage ->
  [VName] ->
  BreadCrumbs ->
  M.Map Name StructType ->
  M.Map Name StructType ->
  m ()
unifySharedFields onDims usage bound bcs fs1 fs2 =
  forM_ (M.toList $ M.intersectionWith (,) fs1 fs2) $ \(f, (t1, t2)) ->
    unifyWith onDims usage bound (breadCrumb (MatchingFields [f]) bcs) t1 t2

unifySharedConstructors ::
  (MonadUnify m) =>
  UnifySizes m ->
  Usage ->
  [VName] ->
  BreadCrumbs ->
  M.Map Name [StructType] ->
  M.Map Name [StructType] ->
  m ()
unifySharedConstructors onDims usage bound bcs cs1 cs2 =
  forM_ (M.toList $ M.intersectionWith (,) cs1 cs2) $ \(c, (f1, f2)) ->
    unifyConstructor c f1 f2
  where
    unifyConstructor c f1 f2
      | length f1 == length f2 = do
          let bcs' = breadCrumb (MatchingConstructor c) bcs
          zipWithM_ (unifyWith onDims usage bound bcs') f1 f2
      | otherwise =
          unifyError usage mempty bcs $
            "Cannot unify constructor" <+> dquotes (prettyName c) <> "."

-- | In @mustHaveConstr usage c t fs@, the type @t@ must have a
-- constructor named @c@ that takes arguments of types @ts@.
mustHaveConstr ::
  (MonadUnify m) =>
  Usage ->
  Name ->
  StructType ->
  [StructType] ->
  m ()
mustHaveConstr usage c t fs = do
  constraints <- getConstraints
  case t of
    Scalar (TypeVar _ (QualName _ tn) [])
      | Just (lvl, NoConstraint l _) <- M.lookup tn constraints -> do
          mapM_ (scopeCheck usage noBreadCrumbs tn lvl) fs
          modifyConstraints $ M.insert tn (lvl, HasConstrs l (M.singleton c fs) usage)
      | Just (lvl, HasConstrs l cs _) <- M.lookup tn constraints ->
          case M.lookup c cs of
            Nothing ->
              modifyConstraints $
                M.insert tn (lvl, HasConstrs l (M.insert c fs cs) usage)
            Just fs'
              | length fs == length fs' -> zipWithM_ (unify usage) fs fs'
              | otherwise ->
                  unifyError usage mempty noBreadCrumbs $
                    "Different arity for constructor" <+> dquotes (pretty c) <> "."
    Scalar (Sum cs) ->
      case M.lookup c cs of
        Nothing ->
          unifyError usage mempty noBreadCrumbs $
            "Constuctor" <+> dquotes (pretty c) <+> "not present in type."
        Just fs'
          | length fs == length fs' -> zipWithM_ (unify usage) fs fs'
          | otherwise ->
              unifyError usage mempty noBreadCrumbs $
                "Different arity for constructor" <+> dquotes (pretty c) <+> "."
    _ ->
      unify usage t $ Scalar $ Sum $ M.singleton c fs

mustHaveFieldWith ::
  (MonadUnify m) =>
  UnifySizes m ->
  Usage ->
  [VName] ->
  BreadCrumbs ->
  Name ->
  StructType ->
  m StructType
mustHaveFieldWith onDims usage bound bcs l t = do
  constraints <- getConstraints
  l_type <- newTypeVar (locOf usage) "t"
  case t of
    Scalar (TypeVar _ (QualName _ tn) [])
      | Just (lvl, NoConstraint {}) <- M.lookup tn constraints -> do
          scopeCheck usage bcs tn lvl l_type
          modifyConstraints $ M.insert tn (lvl, HasFields Lifted (M.singleton l l_type) usage)
          pure l_type
      | Just (lvl, HasFields lifted fields _) <- M.lookup tn constraints -> do
          case M.lookup l fields of
            Just t' -> unifyWith onDims usage bound bcs l_type t'
            Nothing ->
              modifyConstraints $
                M.insert
                  tn
                  (lvl, HasFields lifted (M.insert l l_type fields) usage)
          pure l_type
    Scalar (Record fields)
      | Just t' <- M.lookup l fields -> do
          unify usage l_type t'
          pure t'
      | otherwise ->
          unifyError usage mempty bcs $
            "Attempt to access field"
              <+> dquotes (pretty l)
              <+> " of value of type"
              <+> pretty (toStructural t)
              <> "."
    _ -> do
      unify usage t $ Scalar $ Record $ M.singleton l l_type
      pure l_type

-- | Assert that some type must have a field with this name and type.
mustHaveField ::
  (MonadUnify m) =>
  Usage ->
  Name ->
  StructType ->
  m StructType
mustHaveField usage = mustHaveFieldWith (unifySizes usage) usage mempty noBreadCrumbs

newDimOnMismatch ::
  (MonadUnify m) =>
  Loc ->
  StructType ->
  StructType ->
  m (StructType, [VName])
newDimOnMismatch loc t1 t2 = do
  (t, seen) <- runStateT (matchDims onDims t1 t2) mempty
  pure (t, M.elems seen)
  where
    r = RigidCond t1 t2
    same (e1, e2) =
      maybe False (all same) $ similarExps e1 e2
    onDims _ d1 d2
      | same (d1, d2) = pure d1
      | otherwise = do
          -- Remember mismatches we have seen before and reuse the
          -- same new size.
          maybe_d <- gets $ M.lookup (d1, d2)
          case maybe_d of
            Just d -> pure $ sizeFromName (qualName d) $ srclocOf loc
            Nothing -> do
              d <- lift $ newRigidDim loc r "differ"
              modify $ M.insert (d1, d2) d
              pure $ sizeFromName (qualName d) $ srclocOf loc

-- | Like unification, but creates new size variables where mismatches
-- occur.  Returns the new dimensions thus created.
unifyMostCommon ::
  (MonadUnify m) =>
  Usage ->
  StructType ->
  StructType ->
  m (StructType, [VName])
unifyMostCommon usage t1 t2 = do
  -- We are ignoring the dimensions here, because any mismatches
  -- should be turned into fresh size variables.
  let allOK _ _ _ _ _ = pure ()
  unifyWith allOK usage mempty noBreadCrumbs t1 t2
  t1' <- normTypeFully t1
  t2' <- normTypeFully t2
  newDimOnMismatch (locOf usage) t1' t2'

-- Simple MonadUnify implementation.

type UnifyMState = (Constraints, Int)

newtype UnifyM a = UnifyM (StateT UnifyMState (Except TypeError) a)
  deriving
    ( Monad,
      Functor,
      Applicative,
      MonadState UnifyMState,
      MonadError TypeError
    )

newVar :: Name -> UnifyM VName
newVar name = do
  (x, i) <- get
  put (x, i + 1)
  pure $ VName (mkTypeVarName name i) i

instance MonadUnify UnifyM where
  getConstraints = gets fst
  putConstraints x = modify $ \(_, i) -> (x, i)

  newTypeVar loc name = do
    v <- newVar name
    modifyConstraints $ M.insert v (0, NoConstraint Lifted $ Usage Nothing $ locOf loc)
    pure $ Scalar $ TypeVar mempty (qualName v) []

  newDimVar usage rigidity name = do
    dim <- newVar name
    case rigidity of
      Rigid src ->
        modifyConstraints $
          M.insert dim (0, UnknownSize (locOf usage) src)
      Nonrigid ->
        modifyConstraints $
          M.insert dim (0, Size Nothing usage)
    pure dim

  curLevel = pure 1

  unifyError loc notes bcs doc =
    throwError $ TypeError (locOf loc) notes $ doc <> pretty bcs

  matchError loc notes bcs t1 t2 =
    throwError $ TypeError (locOf loc) notes $ doc <> pretty bcs
    where
      doc =
        "Types"
          </> indent 2 (pretty t1)
          </> "and"
          </> indent 2 (pretty t2)
          </> "do not match."

runUnifyM :: [TypeParam] -> [TypeParam] -> UnifyM a -> Either TypeError a
runUnifyM rigid_tparams nonrigid_tparams (UnifyM m) =
  runExcept $ evalStateT m (constraints, 0)
  where
    constraints =
      M.fromList $
        map nonrigid nonrigid_tparams <> map rigid rigid_tparams
    nonrigid (TypeParamDim p loc) = (p, (1, Size Nothing $ Usage Nothing $ locOf loc))
    nonrigid (TypeParamType l p loc) = (p, (1, NoConstraint l $ Usage Nothing $ locOf loc))
    rigid (TypeParamDim p loc) = (p, (0, ParamSize $ locOf loc))
    rigid (TypeParamType l p loc) = (p, (0, ParamType l $ locOf loc))

-- | Perform a unification of two types outside a monadic context.
-- The first list of type parameters are rigid but may have liftedness
-- constraints; the second list of type parameters are allowed to be
-- instantiated. All other types are considered rigid with no
-- constraints.
doUnification ::
  Loc ->
  [TypeParam] ->
  [TypeParam] ->
  StructType ->
  StructType ->
  Either TypeError StructType
doUnification loc rigid_tparams nonrigid_tparams t1 t2 =
  runUnifyM rigid_tparams nonrigid_tparams $ do
    unify (Usage Nothing (locOf loc)) t1 t2
    normTypeFully t2

-- Note [Linking variables to sum types]
--
-- Consider the case when unifying a result type
--
--   i32 -> ?[n].(#foo [n]bool)
--
-- with
--
--   i32 -> ?[k].a
--
-- where 'a' has a HasConstrs constraint saying that it must have at
-- least a constructor of type '#foo [0]bool'.
--
-- This unification should succeed, but we must not merely link 'a' to
-- '#foo [n]bool', as 'n' is not free.  Instead we should instantiate
-- 'a' to be a concrete sum type (because now we know exactly which
-- constructor labels it must have), and unify each of its constructor
-- payloads with the corresponding expected payload.
