cuddle-0.5.0.0: src/Codec/CBOR/Cuddle/CDDL/Resolve.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ViewPatterns #-}
-- | Module containing tools for 'resolving' CDDL
--
-- Resolving the CDDL is a process of simplifying the representation to make
-- further operations, such as CBOR generation or validation, easier. We operate
-- with a number of passes:
--
-- 1. First, we deal with any rule extensions and create a single map from
-- identifiers to (potentially parametrised) entities.
-- 2. Second, we flatten the structure to a 'CTree', which discards a lot of the
-- extrenuous information.
-- 3. Then we resolve identifiers. Specifically, we do three things:
-- - Resolve identifiers that map to the postlude.
-- - Differentiate between generic args and references to top-level rules.
-- - Validate that all references exist. Note that we cannot resolve all
-- references since they may be circular.
-- 4. Finally, we monomorphise, synthesizing instances of rules with their
-- generic arguments bound.
module Codec.CBOR.Cuddle.CDDL.Resolve (
buildResolvedCTree,
buildRefCTree,
asMap,
buildMonoCTree,
fullResolveCDDL,
MonoRef (..),
NameResolutionFailure (..),
)
where
import Capability.Accessors (Field (..), Lift (..))
import Capability.Error (HasThrow, MonadError (..), throw)
import Capability.Reader (HasReader, MonadReader (..), ask)
import Capability.Reader qualified as Reader (local)
import Capability.Sink (HasSink)
import Capability.Source (HasSource)
import Capability.State (HasState, MonadState (..), modify)
import Codec.CBOR.Cuddle.CDDL
import Codec.CBOR.Cuddle.CDDL.CTree (
CTree,
CTreeRoot,
CTreeRoot' (CTreeRoot),
ParametrisedWith (..),
)
import Codec.CBOR.Cuddle.CDDL.CTree qualified as CTree
import Codec.CBOR.Cuddle.CDDL.Postlude (PTerm (..))
import Control.Monad.Except (ExceptT (..), runExceptT)
import Control.Monad.Reader (Reader, ReaderT (..), runReader)
import Control.Monad.State.Strict (StateT (..))
import Data.Functor.Identity (Identity (..))
import Data.Generics.Product
import Data.Generics.Sum
import Data.Hashable
#if __GLASGOW_HASKELL__ < 910
import Data.List (foldl')
#endif
import Data.List.NonEmpty qualified as NE
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import GHC.Generics (Generic)
import Optics.Core
--------------------------------------------------------------------------------
-- 1. Rule extensions
--------------------------------------------------------------------------------
type CDDLMap = Map.Map Name (Parametrised TypeOrGroup)
type Parametrised a = ParametrisedWith [Name] a
toParametrised :: a -> Maybe GenericParam -> Parametrised a
toParametrised a Nothing = Unparametrised a
toParametrised a (Just (GenericParam gps)) = Parametrised a (NE.toList gps)
parameters :: Parametrised a -> [Name]
parameters (Unparametrised _) = mempty
parameters (Parametrised _ ps) = ps
asMap :: CDDL -> CDDLMap
asMap cddl = foldl' go Map.empty rules
where
rules = cddlTopLevel cddl
go x (TopLevelComment _) = x
go x (TopLevelRule r) = assignOrExtend x r
assignOrExtend :: CDDLMap -> Rule -> CDDLMap
assignOrExtend m (Rule n gps assign tog _) = case assign of
-- Equals assignment
AssignEq -> Map.insert n (toParametrised tog gps) m
AssignExt -> Map.alter (extend tog gps) n m
extend ::
TypeOrGroup ->
Maybe GenericParam ->
Maybe (Parametrised TypeOrGroup) ->
Maybe (Parametrised TypeOrGroup)
extend tog _gps (Just existing) = case (underlying existing, tog) of
(TOGType _, TOGType (Type0 new)) ->
Just $
existing
& field @"underlying"
% _Ctor @"TOGType"
% _Ctor @"Type0"
%~ (<> new)
-- From the CDDL spec, I don't see how one is meant to extend a group.
-- According to the description, it's meant to add a group choice, but the
-- assignment to a group takes a 'GrpEntry', not a Group, and there is no
-- ability to add a choice. For now, we simply ignore attempt at
-- extension.
(TOGGroup _, TOGGroup _new) -> Just existing
(TOGType _, _) -> error "Attempting to extend a type with a group"
(TOGGroup _, _) -> error "Attempting to extend a group with a type"
extend tog gps Nothing = Just $ toParametrised tog gps
--------------------------------------------------------------------------------
-- 2. Conversion to CTree
--------------------------------------------------------------------------------
-- | Indicates that an item may be referenced rather than defined.
data OrRef a
= -- | The item is inlined directly
It a
| -- | Reference to another node with possible generic arguments supplied
Ref Name [CTree.Node OrRef]
deriving (Show, Functor)
type RefCTree = CTreeRoot OrRef
deriving instance Show (CTree OrRef)
deriving instance Show (CTreeRoot OrRef)
-- | Build a CTree incorporating references.
--
-- This translation cannot fail.
buildRefCTree :: CDDLMap -> RefCTree
buildRefCTree rules = CTreeRoot $ fmap toCTreeRule rules
where
toCTreeRule ::
Parametrised TypeOrGroup ->
ParametrisedWith [Name] (CTree.Node OrRef)
toCTreeRule = fmap toCTreeTOG
toCTreeTOG :: TypeOrGroup -> CTree.Node OrRef
toCTreeTOG (TOGType t0) = toCTreeT0 t0
toCTreeTOG (TOGGroup ge) = toCTreeGroupEntry ge
toCTreeT0 :: Type0 -> CTree.Node OrRef
toCTreeT0 (Type0 (t1 NE.:| [])) = toCTreeT1 t1
toCTreeT0 (Type0 xs) = It . CTree.Choice $ toCTreeT1 <$> xs
toCTreeT1 :: Type1 -> CTree.Node OrRef
toCTreeT1 (Type1 t2 Nothing _) = toCTreeT2 t2
toCTreeT1 (Type1 t2 (Just (op, t2')) _) = case op of
RangeOp bound ->
It $
CTree.Range
{ CTree.from = toCTreeT2 t2
, CTree.to = toCTreeT2 t2'
, CTree.inclusive = bound
}
CtrlOp ctlop ->
It $
CTree.Control
{ CTree.op = ctlop
, CTree.target = toCTreeT2 t2
, CTree.controller = toCTreeT2 t2'
}
toCTreeT2 :: Type2 -> CTree.Node OrRef
toCTreeT2 (T2Value v) = It $ CTree.Literal v
toCTreeT2 (T2Name n garg) =
Ref n (fromGenArgs garg)
toCTreeT2 (T2Group t0) =
-- This behaviour seems questionable, but I don't really see how better to
-- interpret the spec here.
toCTreeT0 t0
toCTreeT2 (T2Map g) = toCTreeMap g
toCTreeT2 (T2Array g) = toCTreeArray g
toCTreeT2 (T2Unwrapped n margs) =
It . CTree.Unwrap $
Ref n (fromGenArgs margs)
toCTreeT2 (T2Enum g) = toCTreeEnum g
toCTreeT2 (T2EnumRef n margs) = Ref n $ fromGenArgs margs
toCTreeT2 (T2Tag Nothing t0) =
-- Currently not validating tags
toCTreeT0 t0
toCTreeT2 (T2Tag (Just tag) t0) =
It . CTree.Tag tag $ toCTreeT0 t0
toCTreeT2 (T2DataItem 7 (Just mmin)) =
toCTreeDataItem mmin
toCTreeT2 (T2DataItem _maj _mmin) =
-- We don't validate numerical items yet
It $ CTree.Postlude PTAny
toCTreeT2 T2Any = It $ CTree.Postlude PTAny
toCTreeDataItem 20 =
It . CTree.Literal $ Value (VBool False) mempty
toCTreeDataItem 21 =
It . CTree.Literal $ Value (VBool True) mempty
toCTreeDataItem 25 =
It $ CTree.Postlude PTHalf
toCTreeDataItem 26 =
It $ CTree.Postlude PTFloat
toCTreeDataItem 27 =
It $ CTree.Postlude PTDouble
toCTreeDataItem 23 =
It $ CTree.Postlude PTUndefined
toCTreeDataItem _ =
It $ CTree.Postlude PTAny
toCTreeGroupEntry :: GroupEntry -> CTree.Node OrRef
toCTreeGroupEntry (GroupEntry (Just occi) _ (GEType mmkey t0)) =
It $
CTree.Occur
{ CTree.item = toKVPair mmkey t0
, CTree.occurs = occi
}
toCTreeGroupEntry (GroupEntry Nothing _ (GEType mmkey t0)) = toKVPair mmkey t0
toCTreeGroupEntry (GroupEntry (Just occi) _ (GERef n margs)) =
It $
CTree.Occur
{ CTree.item = Ref n (fromGenArgs margs)
, CTree.occurs = occi
}
toCTreeGroupEntry (GroupEntry Nothing _ (GERef n margs)) = Ref n (fromGenArgs margs)
toCTreeGroupEntry (GroupEntry (Just occi) _ (GEGroup g)) =
It $
CTree.Occur
{ CTree.item = groupToGroup g
, CTree.occurs = occi
}
toCTreeGroupEntry (GroupEntry Nothing _ (GEGroup g)) = groupToGroup g
fromGenArgs :: Maybe GenericArg -> [CTree.Node OrRef]
fromGenArgs = maybe [] (\(GenericArg xs) -> NE.toList $ fmap toCTreeT1 xs)
-- Interpret a group as an enumeration. Note that we float out the
-- choice options
toCTreeEnum :: Group -> CTree.Node OrRef
toCTreeEnum (Group (a NE.:| [])) =
It . CTree.Enum . It . CTree.Group $ toCTreeGroupEntry <$> gcGroupEntries a
toCTreeEnum (Group xs) =
It . CTree.Choice $
It . CTree.Enum . It . CTree.Group . fmap toCTreeGroupEntry <$> groupEntries
where
groupEntries = fmap gcGroupEntries xs
-- Embed a group in another group, again floating out the choice options
groupToGroup :: Group -> CTree.Node OrRef
groupToGroup (Group (a NE.:| [])) =
It . CTree.Group $ fmap toCTreeGroupEntry (gcGroupEntries a)
groupToGroup (Group xs) =
It . CTree.Choice $
fmap (It . CTree.Group . fmap toCTreeGroupEntry) (gcGroupEntries <$> xs)
toKVPair :: Maybe MemberKey -> Type0 -> CTree.Node OrRef
toKVPair Nothing t0 = toCTreeT0 t0
toKVPair (Just mkey) t0 =
It $
CTree.KV
{ CTree.key = toCTreeMemberKey mkey
, CTree.value = toCTreeT0 t0
, -- TODO Handle cut semantics
CTree.cut = False
}
-- Interpret a group as a map. Note that we float out the choice options
toCTreeMap :: Group -> CTree.Node OrRef
toCTreeMap (Group (a NE.:| [])) = It . CTree.Map $ fmap toCTreeGroupEntry (gcGroupEntries a)
toCTreeMap (Group xs) =
It
. CTree.Choice
$ fmap (It . CTree.Map . fmap toCTreeGroupEntry) (gcGroupEntries <$> xs)
-- Interpret a group as an array. Note that we float out the choice
-- options
toCTreeArray :: Group -> CTree.Node OrRef
toCTreeArray (Group (a NE.:| [])) =
It . CTree.Array $ fmap toCTreeGroupEntry (gcGroupEntries a)
toCTreeArray (Group xs) =
It . CTree.Choice $
fmap (It . CTree.Array . fmap toCTreeGroupEntry) (gcGroupEntries <$> xs)
toCTreeMemberKey :: MemberKey -> CTree.Node OrRef
toCTreeMemberKey (MKValue v) = It $ CTree.Literal v
toCTreeMemberKey (MKBareword (Name n _)) = It $ CTree.Literal (Value (VText n) mempty)
toCTreeMemberKey (MKType t1) = toCTreeT1 t1
--------------------------------------------------------------------------------
-- 3. Name resolution
--------------------------------------------------------------------------------
data NameResolutionFailure
= UnboundReference Name
| MismatchingArgs Name [Name]
| ArgsToPostlude PTerm [CTree.Node OrRef]
deriving (Show)
postludeBinding :: Map.Map Name PTerm
postludeBinding =
Map.fromList
[ (Name "bool" mempty, PTBool)
, (Name "uint" mempty, PTUInt)
, (Name "nint" mempty, PTNInt)
, (Name "int" mempty, PTInt)
, (Name "half" mempty, PTHalf)
, (Name "float" mempty, PTFloat)
, (Name "double" mempty, PTDouble)
, (Name "bytes" mempty, PTBytes)
, (Name "bstr" mempty, PTBytes)
, (Name "text" mempty, PTText)
, (Name "tstr" mempty, PTText)
, (Name "any" mempty, PTAny)
, (Name "nil" mempty, PTNil)
, (Name "null" mempty, PTNil)
]
data BindingEnv poly f g = BindingEnv
{ global :: Map.Map Name (poly (CTree.Node f))
-- ^ Global name bindings via 'RuleDef'
, local :: Map.Map Name (CTree.Node g)
-- ^ Local bindings for generic parameters
}
deriving (Generic)
data DistRef a
= DIt a
| -- | Reference to a generic parameter
GenericRef Name
| -- | Reference to a rule definition, possibly with generic arguments
RuleRef Name [CTree.Node DistRef]
deriving (Eq, Generic, Functor, Show)
instance Hashable a => Hashable (DistRef a)
deriving instance Show (CTree DistRef)
deriving instance Eq (CTree DistRef)
instance Hashable (CTree DistRef)
deriving instance Show (CTreeRoot DistRef)
deriving instance Eq (CTreeRoot DistRef)
instance Hashable (CTreeRoot DistRef)
resolveRef ::
BindingEnv (ParametrisedWith [Name]) OrRef OrRef ->
CTree.Node OrRef ->
Either NameResolutionFailure (DistRef (CTree DistRef))
resolveRef env (It a) = DIt <$> resolveCTree env a
resolveRef env (Ref n args) = case Map.lookup n postludeBinding of
Just pterm -> case args of
[] -> Right . DIt $ CTree.Postlude pterm
xs -> Left $ ArgsToPostlude pterm xs
Nothing -> case Map.lookup n (global env) of
Just (parameters -> params') ->
if length params' == length args
then
let localBinds = Map.fromList $ zip params' args
newEnv = env & field @"local" %~ Map.union localBinds
in RuleRef n <$> traverse (resolveRef newEnv) args
else Left $ MismatchingArgs n params'
Nothing -> case Map.lookup n (local env) of
Just _ -> Right $ GenericRef n
Nothing -> Left $ UnboundReference n
resolveCTree ::
BindingEnv (ParametrisedWith [Name]) OrRef OrRef ->
CTree OrRef ->
Either NameResolutionFailure (CTree DistRef)
resolveCTree e = CTree.traverseCTree (resolveRef e)
buildResolvedCTree ::
CTreeRoot OrRef ->
Either NameResolutionFailure (CTreeRoot DistRef)
buildResolvedCTree (CTreeRoot ct) = CTreeRoot <$> traverse go ct
where
initBindingEnv = BindingEnv ct mempty
go pn =
let args = parameters pn
localBinds = Map.fromList $ zip args (flip Ref [] <$> args)
env = initBindingEnv & field @"local" %~ Map.union localBinds
in traverse (resolveRef env) pn
--------------------------------------------------------------------------------
-- 4. Monomorphisation
--------------------------------------------------------------------------------
data MonoRef a
= MIt a
| MRuleRef Name
deriving (Functor, Show)
deriving instance Show (CTree MonoRef)
deriving instance
Show (poly (CTree.Node MonoRef)) =>
Show (CTreeRoot' poly MonoRef)
type MonoEnv = BindingEnv (ParametrisedWith [Name]) DistRef MonoRef
-- | We introduce additional bindings in the state
type MonoState = Map.Map Name (CTree.Node MonoRef)
-- | Monad to run the monomorphisation process. We need some additional
-- capabilities for this, so 'Either' doesn't fully cut it anymore.
newtype MonoM a = MonoM
{ runMonoM ::
ExceptT
NameResolutionFailure
(StateT MonoState (Reader MonoEnv))
a
}
deriving (Functor, Applicative, Monad)
deriving
(HasThrow "nameResolution" NameResolutionFailure)
via MonadError
( ExceptT
NameResolutionFailure
(StateT MonoState (Reader MonoEnv))
)
deriving
( HasSource
"local"
(Map.Map Name (CTree.Node MonoRef))
, HasReader
"local"
(Map.Map Name (CTree.Node MonoRef))
)
via Field
"local"
()
( Lift
( ExceptT
NameResolutionFailure
(Lift (StateT MonoState (MonadReader (Reader MonoEnv))))
)
)
deriving
( HasSource
"global"
(Map.Map Name (ParametrisedWith [Name] (CTree.Node DistRef)))
, HasReader
"global"
(Map.Map Name (ParametrisedWith [Name] (CTree.Node DistRef)))
)
via Field
"global"
()
( Lift
( ExceptT
NameResolutionFailure
(Lift (StateT MonoState (MonadReader (Reader MonoEnv))))
)
)
deriving
( HasSource "synth" MonoState
, HasSink "synth" MonoState
, HasState "synth" MonoState
)
via Lift
( ExceptT
NameResolutionFailure
(MonadState (StateT MonoState (Reader MonoEnv)))
)
throwNR :: NameResolutionFailure -> MonoM a
throwNR = throw @"nameResolution"
-- | Synthesize a monomorphic rule definition, returning the name
synthMono :: Name -> [CTree.Node DistRef] -> MonoM Name
synthMono n@(Name origName _) args =
let fresh =
-- % is not a valid CBOR name, so this should avoid conflict
Name (origName <> "%" <> T.pack (show $ hash args)) mempty
in do
-- Lookup the original name in the global bindings
globalBinds <- ask @"global"
case Map.lookup n globalBinds of
Just (Unparametrised _) -> throwNR $ MismatchingArgs n []
Just (Parametrised r params') ->
if length params' == length args
then do
rargs <- traverse resolveGenericRef args
let localBinds = Map.fromList $ zip params' rargs
Reader.local @"local" (Map.union localBinds) $ do
foo <- resolveGenericRef r
modify @"synth" $ Map.insert fresh foo
else throwNR $ MismatchingArgs n params'
Nothing -> throwNR $ UnboundReference n
pure fresh
resolveGenericRef ::
CTree.Node DistRef ->
MonoM (MonoRef (CTree MonoRef))
resolveGenericRef (DIt a) = MIt <$> resolveGenericCTree a
resolveGenericRef (RuleRef n margs) =
case margs of
[] -> pure $ MRuleRef n
args -> do
fresh <- synthMono n args
pure $ MRuleRef fresh
resolveGenericRef (GenericRef n) = do
localBinds <- ask @"local"
case Map.lookup n localBinds of
Just node -> pure node
Nothing -> throwNR $ UnboundReference n
resolveGenericCTree ::
CTree DistRef ->
MonoM (CTree MonoRef)
resolveGenericCTree = CTree.traverseCTree resolveGenericRef
-- | Monomorphise the CTree
--
-- Concretely, for each reference in the tree to a generic rule, we synthesize a
-- new monomorphic instance of that rule at top-level with the correct
-- parameters applied.
monoCTree ::
CTreeRoot' Identity DistRef ->
MonoM (CTreeRoot' Identity MonoRef)
monoCTree (CTreeRoot ct) = CTreeRoot <$> traverse go ct
where
go = traverse resolveGenericRef
buildMonoCTree ::
CTreeRoot DistRef ->
Either NameResolutionFailure (CTreeRoot' Identity MonoRef)
buildMonoCTree (CTreeRoot ct) = do
let a1 = runExceptT $ runMonoM (monoCTree monoC)
a2 = runStateT a1 mempty
(er, newBindings) = runReader a2 initBindingEnv
CTreeRoot r <- er
pure . CTreeRoot $ Map.union r $ fmap Identity newBindings
where
initBindingEnv = BindingEnv ct mempty
monoC =
CTreeRoot $
Map.mapMaybe
( \case
Unparametrised f -> Just $ Identity f
Parametrised _ _ -> Nothing
)
ct
--------------------------------------------------------------------------------
-- Combined resolution
--------------------------------------------------------------------------------
fullResolveCDDL :: CDDL -> Either NameResolutionFailure (CTreeRoot' Identity MonoRef)
fullResolveCDDL cddl = do
let refCTree = buildRefCTree (asMap cddl)
rCTree <- buildResolvedCTree refCTree
buildMonoCTree rCTree