packages feed

swarm-0.7.0.0: src/swarm-lang/Swarm/Language/Context.hs

{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE OverloadedStrings #-}

-- |
-- SPDX-License-Identifier: BSD-3-Clause
--
-- Generic contexts (mappings from variables to other things, such as
-- types, values, or capability sets) used throughout the codebase.
-- For example, while typechecking we use a context to store a mapping
-- from variables in scope to their types. As another example, at
-- runtime, robots store an 'Env' which contains several contexts,
-- mapping variables to things like their current value, any
-- requirements associated with using the variable, and so on.
--
-- The implementation here goes to some effort to make it possible to
-- serialize and deserialize contexts so that sharing is preserved and
-- the encoding of serialized contexts does not blow up due to
-- repeated values.
module Swarm.Language.Context where

import Control.Algebra (Has, run)
import Control.Carrier.State.Strict (execState)
import Control.Effect.Reader (Reader, ask, local)
import Control.Effect.State (State, get, modify)
import Control.Lens.Empty (AsEmpty (..))
import Control.Lens.Prism (prism)
import Control.Monad (unless)
import Data.Aeson (FromJSON (..), FromJSONKey, ToJSON (..), ToJSONKey, genericParseJSON, genericToJSON, withText)
import Data.Data (Data)
import Data.Function (on)
import Data.Hashable
import Data.List.NonEmpty qualified as NE
import Data.Map (Map)
import Data.Map qualified as M
import Data.Semigroup (Sum (..))
import Data.Text qualified as T
import GHC.Generics (Generic)
import Swarm.Pretty (PrettyPrec (..))
import Swarm.Util (failT, showT)
import Swarm.Util.JSON (optionsMinimize)
import Swarm.Util.Yaml (FromJSONE, getE, liftE, parseJSONE)
import Text.Printf (printf)
import Text.Read (readMaybe)
import Prelude hiding (lookup)

------------------------------------------------------------
-- Context hash

-- | A context hash is a hash value used to identify contexts without
--   having to compare them for equality.  Hash values are computed
--   homomorphically, so that two equal contexts will be guaranteed to
--   have the same hash value, even if they were constructed with a
--   different sequence of operations.
--
--   The downside of this approach is that, /in theory/, there could
--   be hash collisions, that is, two different contexts which
--   nonetheless have the same hash value.  However, this is extremely
--   unlikely.  The benefit is that everything can be purely
--   functional, without the need to thread around some kind of
--   globally unique ID generation effect.
newtype CtxHash = CtxHash {getCtxHash :: Int}
  deriving (Eq, Ord, Data, Generic, ToJSONKey, FromJSONKey)
  deriving (Semigroup, Monoid) via Sum Int
  deriving (Num) via Int

instance Show CtxHash where
  show (CtxHash h) = printf "%016x" h

instance ToJSON CtxHash where
  toJSON h = toJSON (show h)

instance FromJSON CtxHash where
  parseJSON = withText "hash" $ \t -> case readMaybe ("0x" ++ T.unpack t) of
    Nothing -> fail "Could not parse CtxHash"
    Just h -> pure (CtxHash h)

-- | The hash for a single variable -> value binding.
singletonHash :: (Hashable v, Hashable t) => v -> t -> CtxHash
singletonHash x t = CtxHash $ hashWithSalt (hash x) t

-- | The hash for an entire Map's worth of bindings.
mapHash :: (Hashable v, Hashable t) => Map v t -> CtxHash
mapHash = M.foldMapWithKey singletonHash

------------------------------------------------------------
-- Context structure

-- | 'CtxF' represents one level of structure of a context: a context
--   is either empty, a singleton, or built via deletion or union.
data CtxF f v t
  = CtxEmpty
  | CtxSingle v t
  | CtxDelete v t (f v t)
  | CtxUnion (f v t) (f v t)
  deriving (Eq, Show, Functor, Foldable, Traversable, Data, Generic)

instance (ToJSON v, ToJSON t, ToJSON (f v t)) => ToJSON (CtxF f v t) where
  toJSON = genericToJSON optionsMinimize

instance (FromJSON v, FromJSON t, FromJSON (f v t)) => FromJSON (CtxF f v t) where
  parseJSON = genericParseJSON optionsMinimize

-- | Map over the recursive structure stored in a 'CtxF'.
restructureCtx :: (f v t -> g v t) -> CtxF f v t -> CtxF g v t
restructureCtx _ CtxEmpty = CtxEmpty
restructureCtx _ (CtxSingle x t) = CtxSingle x t
restructureCtx h (CtxDelete x t f1) = CtxDelete x t (h f1)
restructureCtx h (CtxUnion f1 f2) = CtxUnion (h f1) (h f2)

-- | A 'CtxTree' is one possible representation of a context,
--   consisting of a structured record of the process by which a
--   context was constructed.  This representation would be terrible
--   for doing efficient variable lookups, but it can be used to
--   efficiently serialize/deserialize the context while recovering
--   sharing.
--
--   It stores a top-level hash of the context, along with a recursive
--   tree built via 'CtxF'.
data CtxTree v t = CtxTree CtxHash (CtxF CtxTree v t)
  deriving (Eq, Functor, Foldable, Traversable, Data, Generic, ToJSON, FromJSON, Show)

------------------------------------------------------------
-- Contexts

-- | A context is a mapping from variable names to things.  We store
--   both a 'Map' (for efficient lookup) as well as a 'CtxTree' (for
--   sharing-aware serializing/deserializing).
data Ctx v t = Ctx {unCtx :: Map v t, ctxStruct :: CtxTree v t}
  deriving (Functor, Traversable, Data, Generic)

-- | Get the top-level hash of a context.
ctxHash :: Ctx v t -> CtxHash
ctxHash (Ctx _ (CtxTree h _)) = h

instance Show (Ctx v t) where
  -- An auto-derived, naive Show instance blows up as it loses all
  -- sharing, so have `show` simply output a placeholder.
  show _ = "<Ctx>"

-- | Compare contexts for equality just by comparing their hashes.
instance Eq (Ctx v t) where
  (==) = (==) `on` ctxHash

instance (Hashable v, Hashable t) => Hashable (Ctx v t) where
  hash = getCtxHash . ctxHash
  hashWithSalt s = hashWithSalt s . getCtxHash . ctxHash

instance Foldable (Ctx v) where
  foldMap f = foldMap f . unCtx

-- | Rebuild a complete 'Ctx' from a 'CtxTree'.
ctxFromTree :: Ord v => CtxTree v t -> Ctx v t
ctxFromTree tree = Ctx (varMap tree) tree
 where
  varMap (CtxTree _ s) = case s of
    CtxEmpty -> M.empty
    CtxSingle x t -> M.singleton x t
    CtxDelete x _ s1 -> M.delete x (varMap s1)
    CtxUnion s1 s2 -> varMap s2 `M.union` varMap s1

-- | "Roll up" one level of context structure while building a new
--   top-level Map and computing an appropriate top-level hash.
--
--   In other words, the input of type @CtxF Ctx t@ represents a
--   context where the topmost level of structure is split out by
--   itself as 'CtxF', with the rest of the recursive structure stored
--   in the embedded 'Ctx' values.  'rollCtx' takes the single level
--   of structure with recursive subtrees and "rolls them up" into one
--   recursive tree.
rollCtx :: (Ord v, Hashable v, Hashable t) => CtxF Ctx v t -> Ctx v t
rollCtx s = Ctx m (CtxTree h (restructureCtx ctxStruct s))
 where
  (m, h) = case s of
    CtxEmpty -> (M.empty, 0)
    CtxSingle x t -> (M.singleton x t, singletonHash x t)
    CtxDelete x _ (Ctx m1 (CtxTree h1 _)) -> case M.lookup x m1 of
      Nothing -> (m1, h1)
      Just t' -> (M.delete x m1, h1 - singletonHash x t')
    CtxUnion (Ctx m1 (CtxTree h1 _)) (Ctx m2 (CtxTree h2 _)) -> (m2 `M.union` m1, h')
     where
      -- `Data.Map.intersection l r` returns a map with common keys,
      -- but values from `l`.  The values in m1 are the ones we want
      -- to subtract from the hash, since they are the ones that will
      -- be overwritten.
      overwritten = M.intersection m1 m2
      h' = h1 + h2 - mapHash overwritten

------------------------------------------------------------
-- Context instances

-- | Serialize a context simply as its hash; we assume that a
--   top-level CtxMap has been seralized somewhere, from which we can
--   recover this context by looking it up.
instance ToJSON (Ctx v t) where
  toJSON = toJSON . ctxHash

-- | Deserialize a context.  We expect to see a hash, and look it up
--   in the provided CtxMap.
instance Ord v => FromJSONE (CtxMap CtxTree v t) (Ctx v t) where
  parseJSONE v = do
    h <- liftE $ parseJSON @CtxHash v
    m <- getE
    case getCtx h m of
      Nothing -> failT ["Encountered unknown context hash", showT h]
      Just ctx -> pure ctx

instance (PrettyPrec t) => PrettyPrec (Ctx v t) where
  prettyPrec _ _ = "<Ctx>"

-- | The semigroup operation for contexts is /right/-biased union.
instance (Ord v, Hashable v, Hashable t) => Semigroup (Ctx v t) where
  (<>) = union

instance (Ord v, Hashable v, Hashable t) => Monoid (Ctx v t) where
  mempty = empty
  mappend = (<>)

instance AsEmpty (Ctx v t) where
  _Empty = prism (const empty) isEmpty
   where
    isEmpty c
      | M.null (unCtx c) = Right ()
      | otherwise = Left c

------------------------------------------------------------
-- Context operations

-- | The empty context.
empty :: Ctx v t
-- We could also define empty = rollCtx CtxEmpty but that would introduce an
-- unnecessary Hashable t constraint.
empty = Ctx M.empty (CtxTree mempty CtxEmpty)

-- | A singleton context.
singleton :: (Ord v, Hashable v, Hashable t) => v -> t -> Ctx v t
singleton x t = rollCtx $ CtxSingle x t

-- | Create a 'Ctx' from a 'Map'.
fromMap :: (Ord v, Hashable v, Hashable t) => Map v t -> Ctx v t
fromMap m = case NE.nonEmpty (M.assocs m) of
  Nothing -> empty
  Just ne -> foldr1 union (NE.map (uncurry singleton) ne)

-- | Look up a variable in a context.
lookup :: Ord v => v -> Ctx v t -> Maybe t
lookup x = M.lookup x . unCtx

-- | Look up a variable in a context in an ambient Reader effect.
lookupR :: (Ord v, Has (Reader (Ctx v t)) sig m) => v -> m (Maybe t)
lookupR x = lookup x <$> ask

-- | Delete a variable from a context.
delete :: (Ord v, Hashable v, Hashable t) => v -> Ctx v t -> Ctx v t
delete x ctx@(Ctx m _) = case M.lookup x m of
  Nothing -> ctx
  Just t -> rollCtx $ CtxDelete x t ctx

-- | Get the list of key-value associations from a context.
assocs :: Ctx v t -> [(v, t)]
assocs = M.assocs . unCtx

-- | Get the list of bound variables from a context.
vars :: Ctx v t -> [v]
vars = M.keys . unCtx

-- | Add a key-value binding to a context (overwriting the old one if
--   the key is already present).
addBinding :: (Ord v, Hashable v, Hashable t) => v -> t -> Ctx v t -> Ctx v t
addBinding x t ctx = ctx `union` singleton x t

-- | /Right/-biased union of contexts.
union :: (Ord v, Hashable v, Hashable t) => Ctx v t -> Ctx v t -> Ctx v t
union ctx1 ctx2 = rollCtx $ CtxUnion ctx1 ctx2

-- | Locally extend the context with an additional binding.
withBinding :: (Has (Reader (Ctx v t)) sig m, Ord v, Hashable v, Hashable t) => v -> t -> m a -> m a
withBinding x ty = local (addBinding x ty)

-- | Locally extend the context with an additional context of
--   bindings.
withBindings :: (Has (Reader (Ctx v t)) sig m, Ord v, Hashable v, Hashable t) => Ctx v t -> m a -> m a
withBindings ctx = local (`union` ctx)

------------------------------------------------------------
-- Context serializing/deserializing

-- | A 'CtxMap' maps context hashes to context structures.  Those
--   structures could either be complete context trees, or just a
--   single level of structure containing more hashes.
type CtxMap f v t = Map CtxHash (CtxF f v t)

-- | Reconstitute the context corresponding to a particular hash, by
--   looking it up in a context map.
getCtx :: Ord v => CtxHash -> CtxMap CtxTree v t -> Maybe (Ctx v t)
getCtx h m = ctxFromTree . CtxTree h <$> M.lookup h m

-- | Turn a context into a context map containing every subtree of its
--   structure.
toCtxMap :: Ord v => Ctx v t -> CtxMap CtxTree v t
toCtxMap (Ctx m s) = run $ execState M.empty (buildCtxMap m s)

-- | Build a context map by keeping track of the incrementally built
--   map in a state effect, and traverse the given context structure
--   to add all subtrees to the map---but, of course, stopping without
--   recursing further whenever we see a hash that is already in the
--   map.
buildCtxMap ::
  forall v t m sig.
  (Ord v, Has (State (CtxMap CtxTree v t)) sig m) =>
  Map v t ->
  CtxTree v t ->
  m ()
buildCtxMap m (CtxTree h s) = do
  cm <- get @(CtxMap CtxTree v t)
  unless (h `M.member` cm) $ do
    modify (M.insert h s)
    case s of
      CtxEmpty -> pure ()
      CtxSingle {} -> pure ()
      CtxDelete x t s1 -> buildCtxMap (M.insert x t m) s1
      CtxUnion s1 s2 -> buildCtxMap m s1 *> buildCtxMap m s2

newtype ConstHash v t = ConstHash CtxHash
  deriving (Eq, Show, Generic, ToJSON, FromJSON)

-- | "Dehydrate" a context map by replacing the actual context trees
--   with single structure layers containing only hashes.  A
--   dehydrated context map is very suitable for serializing, since it
--   makes sharing completely explicit---even if a given context is
--   referenced multiple times, the references are simply hash values,
--   and the context is stored only once, under its hash.
dehydrate :: CtxMap CtxTree v t -> CtxMap ConstHash v t
dehydrate = M.map (restructureCtx (\(CtxTree h1 _) -> ConstHash h1))

-- | "Rehydrate" a dehydrated context map by replacing every hash with
--   an actual context structure.  We do this by building the result
--   as a lazy, recursive map, replacing each hash by the result we
--   get when looking it up in the map being built.  A context which
--   is referenced multiple times will thus be shared in memory.
rehydrate :: CtxMap ConstHash v t -> CtxMap CtxTree v t
rehydrate m = m'
 where
  m' = M.map (restructureCtx (\(ConstHash h) -> CtxTree h (m' M.! h))) m