{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE LinearTypes #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE StandaloneKindSignatures #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}

-- | Unsafe coercions for linearly typed code.
--
-- Use this module to coerce non-linear functions to be linear or values
-- bound linearly to be another type. /All/ functions in this module are
-- unsafe.
--
-- Hence:
--
-- * Import this module qualifed as Unsafe.
-- * Do not use this unless you have to. Specifically, if you can write a
-- linear function @f :: A %1-> B@, do not write a non-linear version and coerce
-- it.
module Unsafe.Linear
  ( -- * Unsafe Coercions
    coerce,
    toLinear,
    toLinear2,
    toLinear3,
    toLinearN,
    ToLinearN (..),
  )
where

import Data.Kind (Constraint)
import Data.Type.Equality (type (~~))
import GHC.Exts (RuntimeRep (..), TYPE)
import GHC.TypeNats
import Unsafe.Coerce (UnsafeEquality (..), unsafeEqualityProof)

-- | Linearly typed @unsafeCoerce@
coerce :: forall a b. a %1 -> b
coerce a =
  case unsafeEqualityProof @a @b of
    UnsafeRefl -> a
{-# INLINE coerce #-}

-- | Converts an unrestricted function into a linear function
toLinear ::
  forall
    (r1 :: RuntimeRep)
    (r2 :: RuntimeRep)
    (a :: TYPE r1)
    (b :: TYPE r2)
    p
    x.
  (a %p -> b) %1 ->
  (a %x -> b)
toLinear f = case unsafeEqualityProof @p @x of
  UnsafeRefl -> f

-- | Like 'toLinear' but for two-argument functions
toLinear2 ::
  forall
    (r1 :: RuntimeRep)
    (r2 :: RuntimeRep)
    (r3 :: RuntimeRep)
    (a :: TYPE r1)
    (b :: TYPE r2)
    (c :: TYPE r3)
    p
    q
    x
    y.
  (a %p -> b %q -> c) %1 ->
  (a %x -> b %y -> c)
toLinear2 f = case unsafeEqualityProof @'(p, q) @'(x, y) of
  UnsafeRefl -> f

-- | Like 'toLinear' but for three-argument functions
toLinear3 ::
  forall
    (r1 :: RuntimeRep)
    (r2 :: RuntimeRep)
    (r3 :: RuntimeRep)
    (r4 :: RuntimeRep)
    (a :: TYPE r1)
    (b :: TYPE r2)
    (c :: TYPE r3)
    (d :: TYPE r4)
    p
    q
    r
    x
    y
    z.
  (a %p -> b %q -> c %r -> d) %1 ->
  (a %x -> b %y -> c %z -> d)
toLinear3 f = case unsafeEqualityProof @'(p, q, r) @'(x, y, z) of
  UnsafeRefl -> f

-- | @toLinearN@ subsumes the functionality of 'toLinear1', 'toLinear2', and
-- 'toLinear3'. In particular, @toLinearN \@n@ unsafely changes the
-- multiplicities of the first @n@ arrows from any multiplicity to any
-- other multiplicity. To be explicit about how each multiplicity is
-- being changed, you can use additional type arguments.
--
-- === Examples
--
-- @
-- toLinearN \@2 :: (a %m-> b %n-> Int) %1-> a %x-> b %y-> Int
-- toLinearN \@3 \@(_ %m-> _ -> _ %1-> _) \@(_ %1-> _ %1-> _ %x-> _)
--   :: (a %m-> b -> c %1-> d) %1-> (a %1-> b %1-> c %x-> d)
-- 'toLinear3' = toLinearN \@3
-- @
toLinearN :: forall n f g. (ToLinearN n f g) => f %1 -> g
-- See Note: Core size
toLinearN f = case unsafeLinearityProofN @n @f @g of
  UnsafeRefl -> f

-- | @ToLinearN n f g@ means that @f@ and @g@ are the same with the
-- possible exception of the multiplicities of the first @n@ arrows.
type ToLinearN :: forall {rep :: RuntimeRep}. Nat -> TYPE rep -> TYPE rep -> Constraint
class ToLinearN n f g where
  -- | Given that @f@ and @g@ are the same, with the possible exception of the
  -- multiplicities of the first @n@ arrows, @unsafeLinearityProofN \@n \@f \@g@
  -- is a fake proof that @f@ and @g@ are identical. This is used primarily in the
  -- definition of 'toLinearN', but it can also be used, for example, to coerce
  -- a container of functions:
  --
  -- @
  -- linearMany :: forall a b c. [a -> b -> c] %1-> [a %1-> b %1-> c]
  -- linearMany = castWithUnsafe (applyUnsafe (UnsafeRefl @[]) $
  --   unsafeLinearityProofN @2 @(a -> b -> c) @(a %1-> b %1-> c))
  --
  -- applyUnsafe :: UnsafeEquality f g -> UnsafeEquality x y -> UnsafeEquality (f x) (g y)
  -- applyUnsafe UnsafeRefl UnsafeRefl = UnsafeRefl
  --
  -- castWithUnsafe :: UnsafeEquality x y -> x %1-> y
  -- castWithUnsafe UnsafeRefl x = x
  -- @
  --
  -- The rather explicit handling of coercions seems to be necessary,
  -- unfortunately, presumably due to the way GHC eagerly rejects equality
  -- constraints it sees as definitely unsatisfiable.
  unsafeLinearityProofN :: UnsafeEquality f g

instance (ToLinearN' ni f g, ni ~ ToINat n) => ToLinearN n f g where
  unsafeLinearityProofN = prf @ni

-- | Plain old inductive natural numbers.
data INat = Z | S INat

-- | Convert a GHC 'Nat' to a real inductive natural number.
-- We use this because GHC 'Nat' offers a friendly API but
-- it's a terrible pain for implementation.
type ToINat :: Nat -> INat
type family ToINat n where
  ToINat 0 = 'Z
  ToINat n = 'S (ToINat (n - 1))

-- | The actual implementation of 'ToLinearN', using the inductive natural
-- number it's handed.
type ToLinearN' :: forall {rep :: RuntimeRep}. INat -> TYPE rep -> TYPE rep -> Constraint
class ToLinearN' arrs f g where
  prf :: UnsafeEquality f g

instance (a ~ b) => ToLinearN' 'Z (a :: TYPE rep) (b :: TYPE rep) where
  prf = UnsafeRefl

-- We use heterogeneous equality here to shift @rep ~ 'LiftedRep@ to the left
-- side of the fat arrow, so that seeing @'S k@ lets GHC /infer/ that @x@ and
-- @y@ are lifted types, rather than needing that information to come from
-- elsewhere.
instance
  ( ToLinearN' k fb gb,
    x ~~ ((a :: TYPE repa) %p -> (fb :: TYPE repb)),
    y ~~ (a %q -> (gb :: TYPE repb))
  ) =>
  ToLinearN' ('S k) (x :: TYPE rep) (y :: TYPE rep)
  where
  prf = case prf @k @fb @gb of
    UnsafeRefl -> case unsafeEqualityProof @p @q of
      UnsafeRefl -> UnsafeRefl

-- Note: Core size
--
-- In GHC 9.0, at least, using toLinearN to implement toLinear2 and toLinear3
-- produces more Core terms, though fewer types and coercions. Does this hamper
-- inlining? If not, we could actually use it so. Alternatively, we *might* be
-- able to reduce unsafeLinearityProofN to one case expression by using some
-- (quantified constraint?) tricks. But that will be complicated, if possible,
-- and probably not worth the trouble.
