joinCtxts :: (?globals :: Globals) => Span -> Ctxt Assumption -> Ctxt Assumption
  -> Checker (Ctxt Assumption, Ctxt Kind)
joinCtxts s ctxt1 ctxt2 = do
    -- All the type assumptions from ctxt1 whose variables appear in ctxt2
    -- and weaken all others
    ctxt  <- intersectCtxtsWithWeaken s ctxt1 ctxt2
    -- All the type assumptions from ctxt2 whose variables appear in ctxt1
    -- and weaken all others
    ctxt' <- intersectCtxtsWithWeaken s ctxt2 ctxt1

    -- Make an context with fresh coeffect variables for all
    -- the variables which are in both ctxt1 and ctxt2...
    (varCtxt, tyVars) <- freshVarsIn s (map fst ctxt) ctxt

    -- ... and make these fresh coeffects the upper-bound of the coeffects
    -- in ctxt and ctxt'
    _ <- zipWith3M_ (relateByLUB s) ctxt ctxt' varCtxt
    -- Return the common upper-bound context of ctxt1 and ctxt2
    return (varCtxt, tyVars)
  where
    zipWith3M_ :: Monad m => (a -> b -> c -> m d) -> [a] -> [b] -> [c] -> m [d]
    zipWith3M_ f _ _ [] = return []
    zipWith3M_ f _ [] _ = return []
    zipWith3M_ f [] _ _ = return []
    zipWith3M_ f (x:xs) (y:ys) (z:zs) = do
      w <- f x y z
      ws <- zipWith3M_ f xs ys zs
      return $ w : ws

{- |  intersect contexts and weaken anything not appear in both
        relative to the left context (this is not commutative) -}
joinCtxts :: (?globals :: Globals) => Span -> Ctxt Assumption -> Ctxt Assumption
  -> Checker (Ctxt Assumption, Ctxt Kind)
joinCtxts s ctxt1 ctxt2 = do
    -- All the type assumptions from ctxt1 whose variables appear in ctxt2
    -- and weaken all others
    ctxt  <- intersectCtxtsWithWeaken s ctxt1 ctxt2
    -- All the type assumptions from ctxt2 whose variables appear in ctxt1
    -- and weaken all others
    ctxt' <- intersectCtxtsWithWeaken s ctxt2 ctxt1

    -- Make an context with fresh coeffect variables for all
    -- the variables which are in both ctxt1 and ctxt2...
    (varCtxt, tyVars) <- freshVarsIn s (map fst ctxt) ctxt

    -- ... and make these fresh coeffects the upper-bound of the coeffects
    -- in ctxt and ctxt'
    _ <- zipWith3M_ (relateByLUB s) ctxt ctxt' varCtxt
    -- Return the common upper-bound context of ctxt1 and ctxt2
    return (varCtxt, tyVars)
  where
    zipWith3M_ :: Monad m => (a -> b -> c -> m d) -> [a] -> [b] -> [c] -> m [d]
    zipWith3M_ f _ _ [] = return []
    zipWith3M_ f _ [] _ = return []
    zipWith3M_ f [] _ _ = return []
    zipWith3M_ f (x:xs) (y:ys) (z:zs) = do
      w <- f x y z
      ws <- zipWith3M_ f xs ys zs
      return $ w : ws

{- |  intersect contexts and weaken anything not appear in both
        relative to the left context (this is not commutative) -}
joinCtxts s ctxt1 ctxt2 = do
    -- All the type assumptions from ctxt1 whose variables appear in ctxt2
    -- and weaken all others
    ctxt  <- intersectCtxtsWithWeaken s ctxt1 ctxt2
    -- All the type assumptions from ctxt2 whose variables appear in ctxt1
    -- and weaken all others
    ctxt' <- intersectCtxtsWithWeaken s ctxt2 ctxt1

    -- Make an context with fresh coeffect variables for all
    -- the variables which are in both ctxt1 and ctxt2...
    (varCtxt, tyVars) <- freshVarsIn s (map fst ctxt) ctxt

    -- ... and make these fresh coeffects the upper-bound of the coeffects
    -- in ctxt and ctxt'
    _ <- zipWith3M_ (relateByLUB s) ctxt ctxt' varCtxt
    -- Return the common upper-bound context of ctxt1 and ctxt2
    return (varCtxt, tyVars)
  where
    zipWith3M_ :: Monad m => (a -> b -> c -> m d) -> [a] -> [b] -> [c] -> m [d]
    zipWith3M_ f _ _ [] = return []
    zipWith3M_ f _ [] _ = return []
    zipWith3M_ f [] _ _ = return []
    zipWith3M_ f (x:xs) (y:ys) (z:zs) = do
      w <- f x y z
      ws <- zipWith3M_ f xs ys zs
      return $ w : ws

{- |  intersect contexts and weaken anything not appear in both
        relative to the left context (this is not commutative) -}