packages feed

cuddle-1.8.0.0: src/Codec/CBOR/Cuddle/CDDL/Resolve.hs

{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeData #-}
{-# LANGUAGE TypeFamilies #-}
{-# 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,
  NameResolutionFailure (..),
  MonoReferenced,
  MonoSimplePhase,
  showSimple,
  XXCTree (..),
)
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 as CDDL
import Codec.CBOR.Cuddle.CDDL.CTree (
  CTree (..),
  CTreeRoot (..),
  PTerm (..),
  XXCTree,
  foldCTree,
 )
import Codec.CBOR.Cuddle.CDDL.CTree qualified as CTree
import Control.Monad.Except (ExceptT (..), runExceptT)
import Control.Monad.Reader (Reader, ReaderT (..), runReader)
import Control.Monad.State.Strict (StateT (..))
import Data.Generics.Product
import Data.Generics.Sum
import Data.Hashable
#if __GLASGOW_HASKELL__ < 910
import Data.List (foldl')
#endif
import Codec.CBOR.Cuddle.CDDL.CTreePhase (CTreePhase, XRule (..))
import Codec.CBOR.Cuddle.CDDL.Custom.Core (RuleTerm)
import Codec.CBOR.Cuddle.CDDL.Custom.Generator (
  CBORGen,
  GenPhase,
  XXCTree (..),
  withLocalGenBindings,
 )
import Codec.CBOR.Cuddle.CDDL.Custom.Validator (
  TermValidator,
  ValidatorPhase,
  XXCTree (..),
  withLocalValidateBindings,
 )
import Codec.CBOR.Cuddle.IndexMappable (IndexMappable (..))
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
import Prettyprinter (Pretty (..), encloseSep, layoutCompact)
import Prettyprinter.Render.Text (renderStrict)

data ProvidedParameters a = ProvidedParameters
  { parameters :: [Name]
  , underlying :: a
  }
  deriving (Generic, Functor, Show, Eq, Foldable, Traversable)

instance Hashable a => Hashable (ProvidedParameters a)

--------------------------------------------------------------------------------
-- 1. Rule extensions
--------------------------------------------------------------------------------

newtype PartialCTreeRoot i = PartialCTreeRoot (Map.Map Name (ProvidedParameters (CTree i)))
  deriving (Generic)

data CDDLMapEntry = CDDLMapEntry
  { cmeProvidedParameters :: ProvidedParameters (TypeOrGroup CTreePhase)
  , cmeCustomGenerator :: Maybe (CBORGen RuleTerm)
  , cmeCustomValidator :: Maybe TermValidator
  }
  deriving (Generic)

type CDDLMap = Map.Map Name CDDLMapEntry

toParametrised ::
  TypeOrGroup CTreePhase ->
  Maybe (GenericParameters CTreePhase) ->
  ProvidedParameters (TypeOrGroup CTreePhase)
toParametrised a Nothing = ProvidedParameters [] a
toParametrised a (Just (GenericParameters gps)) = ProvidedParameters (gpName <$> NE.toList gps) a

asMap :: CDDL CTreePhase -> CDDLMap
asMap cddl = foldl' go Map.empty rules
  where
    rules = cddlTopLevel cddl
    go x (XXTopLevel _) = x
    go x (TopLevelRule r) = assignOrExtend x r

    assignOrExtend :: CDDLMap -> Rule CTreePhase -> CDDLMap
    assignOrExtend m (Rule n gps assign tog (CTreeXRule g v)) = case assign of
      -- Equals assignment
      AssignEq -> Map.insert n (CDDLMapEntry (toParametrised tog gps) g v) m
      AssignExt ->
        Map.alter (extend tog gps) n m

    extend ::
      TypeOrGroup CTreePhase ->
      Maybe (GenericParameters CTreePhase) ->
      Maybe CDDLMapEntry ->
      Maybe CDDLMapEntry
    extend tog _gps (Just cme) = case (underlying $ cmeProvidedParameters cme, tog) of
      (TOGType _, TOGType (Type0 new)) ->
        Just $
          cme
            & #cmeProvidedParameters
            % 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 cme
      (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 $ CDDLMapEntry (toParametrised tog gps) Nothing Nothing

--------------------------------------------------------------------------------
-- 2. Conversion to CTree
--------------------------------------------------------------------------------

type data OrReferenced

data instance XXCTree OrReferenced
  = -- | Reference to another node with possible generic arguments supplied
    OrRef Name [CTree OrReferenced]
  | OGenerator (CBORGen RuleTerm) (CTree OrReferenced)
  | OValidator TermValidator (CTree OrReferenced)

type data OrReferencedSimple

data instance XXCTree OrReferencedSimple = DGOrRef Name [CTree OrReferencedSimple]
  deriving (Eq, Show)

instance IndexMappable CTree OrReferenced OrReferencedSimple where
  mapIndex = foldCTree mapExt mapIndex
    where
      mapExt (OrRef n xs) = CTreeE $ DGOrRef n (mapIndex <$> xs)
      mapExt (OGenerator _ x) = mapIndex x
      mapExt (OValidator _ x) = mapIndex x

instance IndexMappable CTree MonoReferenced GenPhase where
  mapIndex = foldCTree mapExt mapIndex
    where
      mapExt (MRuleRef n) = CTreeE $ GenRef n
      mapExt (MGenerator g x) = CTreeE $ GenGenerator g (mapIndex x)
      mapExt (MValidator _ x) = mapIndex x

instance IndexMappable CTreeRoot MonoReferenced GenPhase where
  mapIndex (CTreeRoot m) = CTreeRoot $ mapIndex <$> m

instance IndexMappable CTree MonoReferenced ValidatorPhase where
  mapIndex = foldCTree mapExt mapIndex
    where
      mapExt (MRuleRef n) = CTreeE $ VRuleRef n
      mapExt (MGenerator _ x) = mapIndex x
      mapExt (MValidator v x) = CTreeE $ VValidator v (mapIndex x)

instance IndexMappable CTreeRoot MonoReferenced ValidatorPhase where
  mapIndex (CTreeRoot m) = CTreeRoot $ mapIndex <$> m

-- | Build a CTree incorporating references.
--
-- This translation cannot fail.
buildRefCTree :: CDDLMap -> PartialCTreeRoot OrReferenced
buildRefCTree rules = PartialCTreeRoot $ toCTreeRule <$> rules
  where
    toCTreeRule :: CDDLMapEntry -> ProvidedParameters (CTree OrReferenced)
    toCTreeRule CDDLMapEntry {..} =
      applyValidator . applyGenerator . toCTreeTOG <$> cmeProvidedParameters
      where
        applyGenerator = case cmeCustomGenerator of
          Just g -> CTreeE . OGenerator g
          Nothing -> id
        applyValidator = case cmeCustomValidator of
          Just v -> CTreeE . OValidator v
          Nothing -> id

    toCTreeTOG :: TypeOrGroup CTreePhase -> CTree OrReferenced
    toCTreeTOG (TOGType t0) = toCTreeT0 t0
    toCTreeTOG (TOGGroup ge) = toCTreeGroupEntry ge

    toCTreeT0 :: Type0 CTreePhase -> CTree OrReferenced
    toCTreeT0 (Type0 (t1 NE.:| [])) = toCTreeT1 t1
    toCTreeT0 (Type0 xs) = CTree.Choice $ toCTreeT1 <$> xs

    toCTreeT1 :: Type1 CTreePhase -> CTree OrReferenced
    toCTreeT1 (Type1 t2 Nothing _) = toCTreeT2 t2
    toCTreeT1 (Type1 t2 (Just (op, t2')) _) = case op of
      RangeOp bound ->
        CTree.Range
          { CTree.from = toCTreeT2 t2
          , CTree.to = toCTreeT2 t2'
          , CTree.inclusive = bound
          }
      CtrlOp ctlop ->
        CTree.Control
          { CTree.op = ctlop
          , CTree.target = toCTreeT2 t2
          , CTree.controller = toCTreeT2 t2'
          }

    toCTreeT2 :: Type2 CTreePhase -> CTree OrReferenced
    toCTreeT2 (T2Value v) = CTree.Literal v
    toCTreeT2 (T2Name n garg) = CTreeE $ OrRef 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) =
      CTree.Unwrap . CTreeE $
        OrRef n (fromGenArgs margs)
    toCTreeT2 (T2Enum g) = toCTreeEnum g
    toCTreeT2 (T2EnumRef n margs) = CTreeE . OrRef n $ fromGenArgs margs
    toCTreeT2 (T2Tag Nothing t0) =
      -- Currently not validating tags
      toCTreeT0 t0
    toCTreeT2 (T2Tag (Just tag) t0) =
      CTree.Tag tag $ toCTreeT0 t0
    toCTreeT2 (T2DataItem 7 (Just mmin)) =
      toCTreeDataItem mmin
    toCTreeT2 (T2DataItem _maj _mmin) =
      -- We don't validate numerical items yet
      CTree.Postlude PTAny
    toCTreeT2 T2Any = CTree.Postlude PTAny
    toCTreeT2 (XXType2 v) = case v of {}

    toCTreeDataItem 20 =
      CTree.Literal $ Value (VBool False) mempty
    toCTreeDataItem 21 =
      CTree.Literal $ Value (VBool True) mempty
    toCTreeDataItem 25 =
      CTree.Postlude PTHalf
    toCTreeDataItem 26 =
      CTree.Postlude PTFloat
    toCTreeDataItem 27 =
      CTree.Postlude PTDouble
    toCTreeDataItem 23 =
      CTree.Postlude PTUndefined
    toCTreeDataItem _ =
      CTree.Postlude PTAny

    toCTreeGroupEntry :: GroupEntry CTreePhase -> CTree OrReferenced
    toCTreeGroupEntry (GroupEntry (Just occi) (GEType mmkey t0) _) =
      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) _) =
      CTree.Occur
        { CTree.item = CTreeE $ OrRef n (fromGenArgs margs)
        , CTree.occurs = occi
        }
    toCTreeGroupEntry (GroupEntry Nothing (GERef n margs) _) = CTreeE $ OrRef n (fromGenArgs margs)
    toCTreeGroupEntry (GroupEntry (Just occi) (GEGroup g) _) =
      CTree.Occur
        { CTree.item = groupToGroup g
        , CTree.occurs = occi
        }
    toCTreeGroupEntry (GroupEntry Nothing (GEGroup g) _) = groupToGroup g

    fromGenArgs :: Maybe (GenericArg CTreePhase) -> [CTree OrReferenced]
    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 CTreePhase -> CTree OrReferenced
    toCTreeEnum (CDDL.Group (a NE.:| [])) =
      CTree.Enum . CTree.Group $ toCTreeGroupEntry <$> gcGroupEntries a
    toCTreeEnum (CDDL.Group xs) =
      CTree.Choice $ CTree.Enum . CTree.Group . fmap toCTreeGroupEntry <$> groupEntries
      where
        groupEntries = fmap gcGroupEntries xs

    -- Embed a group in another group, again floating out the choice options
    groupToGroup :: Group CTreePhase -> CTree OrReferenced
    groupToGroup (CDDL.Group (a NE.:| [])) =
      CTree.Group $ fmap toCTreeGroupEntry (gcGroupEntries a)
    groupToGroup (CDDL.Group xs) =
      CTree.Choice $ fmap (CTree.Group . fmap toCTreeGroupEntry) (gcGroupEntries <$> xs)

    toKVPair :: Maybe (MemberKey CTreePhase) -> Type0 CTreePhase -> CTree OrReferenced
    toKVPair Nothing t0 = toCTreeT0 t0
    toKVPair (Just mkey) t0 =
      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 CTreePhase -> CTree OrReferenced
    toCTreeMap (CDDL.Group (a NE.:| [])) = CTree.Map $ fmap toCTreeGroupEntry (gcGroupEntries a)
    toCTreeMap (CDDL.Group xs) =
      CTree.Choice $
        fmap (CTree.Map . fmap toCTreeGroupEntry) (gcGroupEntries <$> xs)

    -- Interpret a group as an array. Note that we float out the choice
    -- options
    toCTreeArray :: Group CTreePhase -> CTree OrReferenced
    toCTreeArray (CDDL.Group (a NE.:| [])) =
      CTree.Array $ fmap toCTreeGroupEntry (gcGroupEntries a)
    toCTreeArray (CDDL.Group xs) =
      CTree.Choice $
        fmap (CTree.Array . fmap toCTreeGroupEntry) (gcGroupEntries <$> xs)

    toCTreeMemberKey :: MemberKey CTreePhase -> CTree OrReferenced
    toCTreeMemberKey (MKValue v) = CTree.Literal v
    toCTreeMemberKey (MKBareword n) = CTree.Literal (Value (VText $ unName n) mempty)
    toCTreeMemberKey (MKType t1) = toCTreeT1 t1

--------------------------------------------------------------------------------
-- 3. Name resolution
--------------------------------------------------------------------------------

data NameResolutionFailure
  = UnboundReference Name
  | MismatchingArgs Name [Name]
  | ArgsToPostlude PTerm [CTree OrReferencedSimple]
  deriving (Show, Eq)

postludeBinding :: Map.Map Name PTerm
postludeBinding =
  Map.fromList
    [ ("bool", PTBool)
    , ("uint", PTUInt)
    , ("nint", PTNInt)
    , ("int", PTInt)
    , ("half", PTHalf)
    , ("float", PTFloat)
    , ("double", PTDouble)
    , ("bytes", PTBytes)
    , ("bstr", PTBytes)
    , ("text", PTText)
    , ("tstr", PTText)
    , ("any", PTAny)
    , ("nil", PTNil)
    , ("null", PTNil)
    ]

data BindingEnv i j = BindingEnv
  { global :: Map.Map (Name) (ProvidedParameters (CTree i))
  -- ^ Global name bindings via 'RuleDef'
  , local :: Map.Map (Name) (CTree j)
  -- ^ Local bindings for generic parameters
  }
  deriving (Generic)

type data DistReferenced

data DistRef i
  = -- | Reference to a generic parameter
    GenericRef Name
  | -- | Reference to a rule definition, possibly with generic arguments
    RuleRef Name [CTree i]
  deriving (Generic)

deriving instance Eq (CTree.Node i) => Eq (DistRef i)

deriving instance Show (CTree.Node i) => Show (DistRef i)

instance Hashable (CTree.Node i) => Hashable (DistRef i)

instance Pretty (XXCTree i) => Pretty (DistRef i) where
  pretty (GenericRef n) = pretty n
  pretty (RuleRef rule []) = pretty rule
  pretty (RuleRef rule args) = pretty rule <> encloseSep "<" ">" "," (pretty <$> args)

data instance XXCTree DistReferenced
  = DRef (DistRef DistReferenced)
  | DGenerator (CBORGen RuleTerm) (CTree DistReferenced)
  | DValidator TermValidator (CTree DistReferenced)

type data DistReferencedNoGen

newtype instance XXCTree DistReferencedNoGen = DHRef (DistRef DistReferencedNoGen)
  deriving (Eq, Hashable, Show)
  deriving newtype (Pretty)

resolveRef ::
  BindingEnv OrReferenced OrReferenced ->
  CTree.Node OrReferenced ->
  Either NameResolutionFailure (CTree DistReferenced)
resolveRef env (OrRef n args) = case Map.lookup n postludeBinding of
  Just pterm -> case args of
    [] -> Right $ CTree.Postlude pterm
    xs -> Left . ArgsToPostlude pterm $ mapIndex <$> 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 & #local %~ Map.union localBinds
           in CTreeE . DRef . RuleRef n <$> traverse (resolveCTree newEnv) args
        else Left $ MismatchingArgs n params'
    Nothing -> case Map.lookup n (local env) of
      Just _ -> Right . CTreeE . DRef $ GenericRef n
      Nothing -> Left $ UnboundReference n
resolveRef env (OGenerator g x) = CTreeE . DGenerator g <$> resolveCTree env x
resolveRef env (OValidator v x) = CTreeE . DValidator v <$> resolveCTree env x

resolveCTree ::
  BindingEnv OrReferenced OrReferenced ->
  CTree OrReferenced ->
  Either NameResolutionFailure (CTree DistReferenced)
resolveCTree e = CTree.traverseCTree (resolveRef e) (resolveCTree e)

buildResolvedCTree ::
  PartialCTreeRoot OrReferenced ->
  Either NameResolutionFailure (PartialCTreeRoot DistReferenced)
buildResolvedCTree (PartialCTreeRoot ct) = PartialCTreeRoot <$> traverse go ct
  where
    go pn =
      let args = parameters pn
          localBinds = Map.fromList $ zip args (CTreeE . flip OrRef [] <$> args)
          env = BindingEnv @OrReferenced @OrReferenced ct localBinds
       in traverse (resolveCTree env) pn

--------------------------------------------------------------------------------
-- 4. Monomorphisation
--------------------------------------------------------------------------------

type data MonoReferenced

data instance XXCTree MonoReferenced
  = MRuleRef Name
  | MGenerator (CBORGen RuleTerm) (CTree MonoReferenced)
  | MValidator TermValidator (CTree MonoReferenced)

type MonoEnv = BindingEnv DistReferenced MonoReferenced

-- | We introduce additional bindings in the state
type MonoState = Map.Map Name (CTree MonoReferenced)

-- | A simplified phase reachable from any of the function-bearing post-mono
-- phases ('MonoReferenced', 'GenPhase', 'ValidatorPhase'). It strips the
-- generator/validator extensions so the tree can be `Show`n, `Eq`-compared,
-- and pretty-printed for debug output.
type data MonoSimplePhase

newtype instance XXCTree MonoSimplePhase = MSimpleRef Name
  deriving (Generic, Show, Eq)

instance Pretty (XXCTree MonoSimplePhase) where
  pretty (MSimpleRef n) = pretty n

instance IndexMappable CTree MonoReferenced MonoSimplePhase where
  mapIndex = foldCTree mapExt mapIndex
    where
      mapExt (MRuleRef n) = CTreeE $ MSimpleRef n
      mapExt (MGenerator _ x) = mapIndex x
      mapExt (MValidator _ x) = mapIndex x

instance IndexMappable CTreeRoot MonoReferenced MonoSimplePhase where
  mapIndex (CTreeRoot m) = CTreeRoot $ mapIndex <$> m

instance IndexMappable CTree GenPhase MonoSimplePhase where
  mapIndex = foldCTree mapExt mapIndex
    where
      mapExt (GenRef n) = CTreeE $ MSimpleRef n
      mapExt (GenGenerator _ x) = mapIndex x

instance IndexMappable CTreeRoot GenPhase MonoSimplePhase where
  mapIndex (CTreeRoot m) = CTreeRoot $ mapIndex <$> m

instance IndexMappable CTree ValidatorPhase MonoSimplePhase where
  mapIndex = foldCTree mapExt mapIndex
    where
      mapExt (VRuleRef n) = CTreeE $ MSimpleRef n
      mapExt (VValidator _ x) = mapIndex x

instance IndexMappable CTreeRoot ValidatorPhase MonoSimplePhase where
  mapIndex (CTreeRoot m) = CTreeRoot $ mapIndex <$> m

-- | Project any phase that maps to 'MonoSimplePhase' down to a `Show`able
-- representation. Used for debug output by `Gen`, `Validator`, etc.
showSimple ::
  forall a phase.
  ( IndexMappable a phase MonoSimplePhase
  , Show (a MonoSimplePhase)
  ) =>
  a phase -> String
showSimple = show . mapIndex @_ @_ @MonoSimplePhase

-- | 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 MonoReferenced))
    , HasReader
        "local"
        (Map.Map (Name) (CTree MonoReferenced))
    )
    via Field
          "local"
          ()
          ( Lift
              ( ExceptT
                  NameResolutionFailure
                  (Lift (StateT MonoState (MonadReader (Reader MonoEnv))))
              )
          )
  deriving
    ( HasSource
        "global"
        (Map.Map (Name) (ProvidedParameters (CTree DistReferenced)))
    , HasReader
        "global"
        (Map.Map (Name) (ProvidedParameters (CTree DistReferenced)))
    )
    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 DistReferenced] -> MonoM Name
synthMono origName args =
  let dropGenerator = fmap $ mapIndex @_ @DistReferenced @DistReferencedNoGen
      argsName = Name (T.intercalate "," $ renderStrict . layoutCompact . pretty <$> dropGenerator args)
      -- We use % to mark a monomorphised generic rule, '%' is not allowed in
      -- CDDL names, so there should be no conflicts
      fresh = "%" <> origName <> "<" <> argsName <> ">"
   in do
        -- Lookup the original name in the global bindings
        globalBinds <- ask @"global"
        case Map.lookup origName globalBinds of
          Just (ProvidedParameters [] _) -> throwNR $ MismatchingArgs origName []
          Just (ProvidedParameters params' r) ->
            if length params' == length args
              then do
                rargs <- traverse resolveGenericCTree args
                let localBinds = Map.fromList $ zip params' rargs
                Reader.local @"local" (Map.union localBinds) $ do
                  foo <- resolveGenericCTree r
                  modify @"synth" $ Map.insert fresh foo
              else throwNR $ MismatchingArgs origName params'
          Nothing -> throwNR $ UnboundReference origName
        pure fresh

resolveGenericRef ::
  CTree.Node DistReferenced ->
  MonoM (CTree MonoReferenced)
resolveGenericRef (DRef (RuleRef n [])) = pure . CTreeE $ MRuleRef n
resolveGenericRef (DRef (RuleRef n args)) = do
  fresh <- synthMono n args
  pure . CTreeE $ MRuleRef fresh
resolveGenericRef (DRef (GenericRef n)) = do
  localBinds <- ask @"local"
  case Map.lookup n localBinds of
    Just node -> pure node
    Nothing -> throwNR $ UnboundReference n
resolveGenericRef (DGenerator g x) = do
  binds <- ask @"local"
  body <- resolveGenericCTree x
  let bindsGen = mapIndex @CTree @MonoReferenced @GenPhase <$> binds
      g' = withLocalGenBindings bindsGen g
  pure . CTreeE $ MGenerator g' body
resolveGenericRef (DValidator v x) = do
  binds <- ask @"local"
  body <- resolveGenericCTree x
  let bindsVal = mapIndex @CTree @MonoReferenced @ValidatorPhase <$> binds
      v' = withLocalValidateBindings bindsVal . v
  pure . CTreeE $ MValidator v' body

resolveGenericCTree ::
  CTree DistReferenced ->
  MonoM (CTree MonoReferenced)
resolveGenericCTree = CTree.traverseCTree resolveGenericRef resolveGenericCTree

-- | 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.
buildMonoCTree ::
  PartialCTreeRoot DistReferenced ->
  Either NameResolutionFailure (CTreeRoot MonoReferenced)
buildMonoCTree (PartialCTreeRoot ct) = do
  let a1 = runExceptT $ runMonoM (traverse resolveGenericCTree monoC)
      a2 = runStateT a1 mempty
      (r, newBindings) = runReader a2 initBindingEnv
  CTreeRoot . (`Map.union` newBindings) <$> r
  where
    initBindingEnv = BindingEnv ct mempty
    monoC =
      Map.mapMaybe
        ( \case
            ProvidedParameters [] f -> Just f
            _ -> Nothing
        )
        ct

--------------------------------------------------------------------------------
-- Combined resolution
--------------------------------------------------------------------------------

fullResolveCDDL :: CDDL CTreePhase -> Either NameResolutionFailure (CTreeRoot MonoReferenced)
fullResolveCDDL cddl = do
  let refCTree = buildRefCTree (asMap cddl)
  rCTree <- buildResolvedCTree refCTree
  buildMonoCTree rCTree

instance IndexMappable CTree DistReferenced DistReferencedNoGen where
  mapIndex = foldCTree mapExt mapIndex
    where
      mapExt (DRef x) = CTreeE . DHRef $ mapIndex x
      mapExt (DGenerator _ x) = mapIndex x
      mapExt (DValidator _ x) = mapIndex x

instance IndexMappable DistRef DistReferenced DistReferencedNoGen where
  mapIndex (GenericRef n) = GenericRef n
  mapIndex (RuleRef n args) = RuleRef n $ mapIndex <$> args