candid-0.4: src/Codec/Candid/Coerce.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE FlexibleContexts #-}
module Codec.Candid.Coerce
( coerceSeqDesc
, coerce
)
where
import Prettyprinter
import qualified Data.Vector as V
import qualified Data.ByteString.Lazy as BS
import qualified Data.Map as M
import Control.Monad.State.Lazy
import Control.Monad.Except
import Codec.Candid.FieldName
import Codec.Candid.Types
import Codec.Candid.TypTable
import Codec.Candid.Subtype
coerceSeqDesc :: [Value] -> SeqDesc -> SeqDesc -> Either String [Value]
coerceSeqDesc vs sd1 sd2 =
unrollTypeTable sd1 $ \ts1 ->
unrollTypeTable sd2 $ \ts2 ->
coerceSeq vs ts1 ts2
coerceSeq ::
(Pretty k1, Pretty k2, Ord k1, Ord k2) =>
[Value] ->
[Type (Ref k1 Type)] ->
[Type (Ref k2 Type)] ->
Either String [Value]
coerceSeq vs t1 t2 = runSubTypeM $ goSeq vs t1 t2
-- | This function implements the @V : T ~> V' : T'@ relation from the Candid spec.
--
-- Because values in this library are untyped, we have to pass what we know about
-- their type down, so that we can do the subtype check upon a reference.
-- The given type must match the value closely (as it is the case when decoding
-- from the wire) and this function may behave oddly if @v@ and @t1@ are not related.
--
-- Morally, this function looks only at @v@ and @t2@. It only needs @t1@ for
-- refences, and hence needs to take @t2@ apart for the recursive calls.
-- Practically, it's sometimes more concise to look at t2 instead of v.
coerce ::
(Pretty k1, Pretty k2, Ord k1, Ord k2) =>
Value ->
Type (Ref k1 Type) ->
Type (Ref k2 Type) ->
Either String Value
coerce v t1 t2 = runSubTypeM $ go v t1 t2
go ::
(Pretty k1, Pretty k2, Ord k1, Ord k2) =>
Value ->
Type (Ref k1 Type) ->
Type (Ref k2 Type) ->
SubTypeM k1 k2 Value
goSeq ::
(Pretty k1, Pretty k2, Ord k1, Ord k2) =>
[Value] ->
[Type (Ref k1 Type)] ->
[Type (Ref k2 Type)] ->
SubTypeM k1 k2 [Value]
-- Look through refs
go v (RefT (Ref _ t1)) t2 = go v t1 t2
go v t1 (RefT (Ref _ t2)) = go v t1 t2
-- Identity coercion for primitive values
go v NatT NatT = pure v
go v Nat8T Nat8T = pure v
go v Nat16T Nat16T = pure v
go v Nat32T Nat32T = pure v
go v Nat64T Nat64T = pure v
go v IntT IntT = pure v
go v Int8T Int8T = pure v
go v Int16T Int16T = pure v
go v Int32T Int32T = pure v
go v Int64T Int64T = pure v
go v Float32T Float32T = pure v
go v Float64T Float64T = pure v
go v BoolT BoolT = pure v
go v TextT TextT = pure v
go v NullT NullT = pure v
go v PrincipalT PrincipalT = pure v
-- Nat <: Int
go (NatV n) NatT IntT = pure $ IntV (fromIntegral n)
-- t <: reserved
go _ _ ReservedT = pure ReservedV
-- empty <: t (actually just a special case of `v :/ t`)
go v EmptyT _ = throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing empty"
-- vec t1 <: vec t2
go (VecV vs) (VecT t1) (VecT t2) = VecV <$> mapM (\v -> go v t1 t2) vs
-- Option: The normal rule
go (OptV Nothing) (OptT _) (OptT _) = pure NullV
go (OptV (Just v)) (OptT t1) (OptT t2) =
lift (runExceptT (go v t1 t2)) >>= \case
Right v' -> pure (OptV (Just v'))
Left _ -> pure (OptV Nothing)
-- Option: The constituent rule
go v t1 (OptT t2) | not (isOptLike t2) =
lift (runExceptT (go v t1 t2)) >>= \case
Right v' -> pure (OptV (Just v'))
Left _ -> pure (OptV Nothing)
-- Option: The fallback rule
go _ _ (OptT _) = pure (OptV Nothing)
-- Records
go rv (RecT fs1) (RecT fs2) = do
vm <- case rv of
TupV ts -> pure $ M.fromList $ zip [hashedField n | n <- [0..]] ts
RecV fvs -> pure $ M.fromList fvs
v -> throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing record"
let m1 = M.fromList fs1
fmap RecV $ forM fs2 $ \(fn, t2) -> (fn,) <$>
case (M.lookup fn vm, M.lookup fn m1) of
(Just v, Just t1) -> go v t1 t2
_ -> case unRef t2 of
OptT _ -> pure (OptV Nothing)
ReservedT -> pure ReservedV
t -> throwError $ show $ "Missing record field" <+> pretty fn <+> "of type" <+> pretty t
-- Variants
go (VariantV fn v) (VariantT fs1) (VariantT fs2) = do
let m1 = M.fromList fs1
let m2 = M.fromList fs2
case (M.lookup fn m1, M.lookup fn m2) of
(Just t1, Just t2) -> VariantV fn <$> go v t1 t2
(Nothing, _) -> throwError $ show $ "Wrongly typed variant missing field " <+> pretty fn
(_, Nothing) -> throwError $ show $ "Unexpected variant field" <+> pretty fn
-- Reference types
go v t1@(FuncT _) t2@(FuncT _) = isSubtypeOfM t1 t2 >> pure v
go v t1@(ServiceT _) t2@(ServiceT _) = isSubtypeOfM t1 t2 >> pure v
-- BlobT
go v BlobT BlobT = pure v
go (VecV vs) (VecT t) BlobT | isNat8 t = BlobV . BS.pack . V.toList <$> mapM goNat8 vs
where
goNat8 (Nat8V n) = pure n
goNat8 v = throwError $ show $ "Unexpected value" <+> pretty v <+> "while coercing vec nat8 to blob"
go (BlobV b) BlobT (VecT t) | isNat8 t = pure $ VecV $ V.fromList $ map (Nat8V . fromIntegral) $ BS.unpack b
go v t1 t2 = throwError $ show $ "Cannot coerce " <+> pretty v <+> ":" <+> pretty t1 <+> "to type " <+> pretty t2
goSeq _ _ [] = pure []
goSeq vs ts1 (RefT (Ref _ t) : ts) = goSeq vs ts1 (t:ts)
goSeq vs@[] ts1@[] (OptT _ : ts) = (OptV Nothing :) <$> goSeq vs ts1 ts
goSeq vs@[] ts1@[] (ReservedT : ts) = (ReservedV :) <$> goSeq vs ts1 ts
goSeq [] [] ts = throwError $ show $ "Argument type list too short, expecting types" <+> pretty ts
goSeq (v:vs) (t1:ts1) (t2:ts2) = do
v' <- go v t1 t2
vs' <- goSeq vs ts1 ts2
pure $ v' : vs'
goSeq _ _ _ = throwError $ "Illtyped input to goSeq"
unRef :: Type (Ref a Type) -> Type (Ref a Type)
unRef (RefT (Ref _ t)) = unRef t
unRef t = t
isNat8 :: Type (Ref a Type) -> Bool
isNat8 (RefT (Ref _ t)) = isNat8 t
isNat8 Nat8T = True
isNat8 _ = False
-- | `null <: t`?
isOptLike :: Type (Ref a Type) -> Bool
isOptLike (RefT (Ref _ t)) = isOptLike t
isOptLike NullT = True
isOptLike (OptT _) = True
isOptLike ReservedT = True
isOptLike _ = False