packages feed

indigo-0.3.0: src/Indigo/Internal/Object.hs

-- SPDX-FileCopyrightText: 2020 Tocqueville Group
--
-- SPDX-License-Identifier: LicenseRef-MIT-TQ

module Indigo.Internal.Object
  ( IndigoObjectF (..)
  , NamedFieldObj (..)
  , TypedFieldObj (..)
  , FieldTypes
  , Object
  , SomeObject (..)
  , namedToTypedRec
  , typedToNamedRec
  , namedToTypedFieldObj
  , typedToNamedFieldObj

  , IsObject
  , complexObjectDict
  , ComplexObjectC
  , castFieldConstructors
  ) where

import Data.Vinyl (RMap)
import Data.Vinyl.TypeLevel (AllConstrained)
import Data.Constraint (Dict(..))
import qualified GHC.Generics as G

import Indigo.Internal.Var (RefId)
import Indigo.Backend.Prelude
import Indigo.Lorentz
import Michelson.Typed.Haskell.Instr.Product
  ( GetFieldType, ConstructorFieldNames, GetFieldType
  , InstrDeconstructC, FieldConstructor (..), CastFieldConstructors (..))
import Michelson.Typed (IsPrimitiveValue)
import Util.Type (KnownList (..), KList (..))

----------------------------------------------------------------------------
-- IndigoObjectF
----------------------------------------------------------------------------

-- | A object that can be either
-- stored in the single stack cell or split into fields.
-- Fields are identified by their names.
--
-- @f@ is a functor to be applied to each of field names.
data IndigoObjectF f a where
  -- | Value stored on the stack, it might be
  -- either complex product type, like @(a, b)@, Storage, etc,
  -- or sum type like 'Either', or primitive like 'Int', 'Operation', etc.
  Cell :: KnownValue a => RefId -> IndigoObjectF f a
  -- | Decomposed product type, which is NOT stored
  -- as one cell on the stack.
  Decomposed :: ComplexObjectC a => Rec f (ConstructorFieldNames a) -> IndigoObjectF f a

-- | Auxiliary type family to convert list of field names
-- to list of field types
type family MapGFT a rs where
  MapGFT _ '[] = '[]
  MapGFT a (name ': xs) = GetFieldType a name ': MapGFT a xs

-- | Convert a list of fields from name-based list to type-based one
namedToTypedRec
  :: forall a f g .
    (forall name . f name -> g (GetFieldType a name))
  -> Rec f (ConstructorFieldNames a)
  -> Rec g (FieldTypes a)
namedToTypedRec fun = namedToTypedRecImpl
  where
    namedToTypedRecImpl :: Rec f rs -> Rec g (MapGFT a rs)
    namedToTypedRecImpl RNil = RNil
    namedToTypedRecImpl (v :& xs) = fun v :& namedToTypedRecImpl xs

-- | Convert a list of fields from type-based list to named-based one
typedToNamedRec :: forall a f g . KnownList (ConstructorFieldNames a)
  => (forall name . f (GetFieldType a name) -> g name)
  -> Rec f (FieldTypes a)
  -> Rec g (ConstructorFieldNames a)
typedToNamedRec fun = typedToNamedRecImpl
  where
    typedToNamedRecImpl :: forall rs . KnownList rs => Rec f (MapGFT a rs) -> Rec g rs
    typedToNamedRecImpl re = case (klist @rs, re) of
      (KNil, RNil)  -> RNil
      (KCons (_ :: Proxy nm) (_ :: Proxy rs'), v :& vs) -> fun v :& typedToNamedRecImpl vs

castFieldConstructors
  :: forall a st . CastFieldConstructors (FieldTypes a) (ConstructorFieldTypes a)
  => Rec (FieldConstructor st) (FieldTypes a) -> Rec (FieldConstructor st) (ConstructorFieldTypes a)
castFieldConstructors = castFieldConstructorsImpl

-- | Auxiliary datatype to define a Objiable.
-- Keeps field name as type param
data NamedFieldObj a name where
  NamedFieldObj
    :: IsObject (GetFieldType a name)
    => { unFieldObj :: Object (GetFieldType a name)
       }
    -> NamedFieldObj a name

type Object a = IndigoObjectF (NamedFieldObj a) a

data SomeObject where
  SomeObject :: IsObject a => Object a -> SomeObject

-- | Like 'NamedFieldObj', but this one doesn't keep name of a field
data TypedFieldObj a where
  TypedFieldObj :: IsObject a => Object a -> TypedFieldObj a

namedToTypedFieldObj :: forall a name . NamedFieldObj a name -> TypedFieldObj (GetFieldType a name)
namedToTypedFieldObj (NamedFieldObj f) = TypedFieldObj f

typedToNamedFieldObj :: forall a name . TypedFieldObj (GetFieldType a name) -> NamedFieldObj a name
typedToNamedFieldObj (TypedFieldObj f) = NamedFieldObj f

----------------------------------------------------------------------------
-- IsObject type class
----------------------------------------------------------------------------

class IsObject' (TypeDecision a) a => IsObject a
instance IsObject' (TypeDecision a) a => IsObject a

type FieldTypes a = MapGFT a (ConstructorFieldNames a)

type ToDeconstructC a =
  ( InstrDeconstructC a
  , KnownList (FieldTypes a)
  , AllConstrained KnownValue (FieldTypes a)
  )

type ToConstructC a =
  ( KnownValue a
  , InstrConstructC a
  , RMap (ConstructorFieldNames a)
  , RMap (ConstructorFieldTypes a)
  , RMap (FieldTypes a)
  , KnownList (ConstructorFieldNames a)
  , CastFieldConstructors (FieldTypes a) (ConstructorFieldTypes a)
  )

type ComplexObjectC a =
  ( ToDeconstructC a
  , ToConstructC a
  , AllConstrained IsObject (FieldTypes a)
  )

-- | Type class instantiated for all possible Indigo types
class KnownValue a => IsObject' (decision :: Decision) a where
  complexObjectDict' :: Maybe (Dict (ComplexObjectC a))

instance KnownValue a => IsObject' 'PrimitiveD a where
  complexObjectDict' = Nothing

instance KnownValue a => IsObject' 'SumTypeD a where
  complexObjectDict' = Nothing

instance ComplexObjectC a => IsObject' 'ProductTypeD a where
  complexObjectDict' = Just Dict

complexObjectDict :: forall a . IsObject a => Maybe (Dict (ComplexObjectC a))
complexObjectDict = complexObjectDict' @(TypeDecision a) @a

-- | Decide whether type is either primitive or ADT
type TypeDecision a = Decide (IsPrimitiveValue a) (IsSumType (G.Rep a))

data Decision
  = PrimitiveD
  | SumTypeD
  | ProductTypeD

type family Decide flagPrimitive flagSumType where
  Decide 'True _ = 'PrimitiveD
  Decide 'False 'True = 'SumTypeD
  Decide 'False 'False = 'ProductTypeD

type family IsSumType x where
  IsSumType (G.D1 _ x) = IsSumType x
  IsSumType (G.C1 _ x) = IsSumType x
  IsSumType (G.M1 _ _ x) = IsSumType x
  IsSumType (_ G.:*: _) = 'False
  IsSumType (G.Rec0 _) = 'False
  IsSumType G.V1 = 'False
  IsSumType G.U1 = 'False
  IsSumType (_ G.:+: _) = 'True