packages feed

imp-ppl-0.1.0.0: src/Imp/BDD/Compile.hs

{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
-- | Compilation from 'Imp' programs to BDDs.
module Imp.BDD.Compile
  ( Compiled
  , compile
  ) where

import Control.Monad.Reader
import Control.Monad.State.Strict
import Data.IntMap.Strict (IntMap)
import qualified Data.IntMap.Strict as IntMap
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Proxy (Proxy(..))
import GHC.TypeLits (KnownSymbol, symbolVal)

import Imp.BDD
import Imp.BDD.Builder
import Imp.BDD.WMC (Weight(..))
import Imp.DSL (Imp(..))

-- | Compiled result: BDD manager, variable weights, Knightian variables and index,
--   and the worlds: each return value with its BDD guard.
type Compiled a = (BDDManager, IntMap Weight, Map String Int, Map a BDD)

-- | State accumulated during compilation.
data CompileState = CompileState
  { csWeights :: !(IntMap Weight)
  , csKnights :: !(Map String (BDD, Int))
  }

-- | Read-only environment for compilation.
newtype CompileEnv = CompileEnv
  { ceTag :: String
  }

initState :: CompileState
initState = CompileState IntMap.empty Map.empty

type CompileM = ReaderT CompileEnv (StateT CompileState BDDM)

-- | Compile an Imp program to its worlds, one guard per return value.
compile :: Ord a => Imp g a -> Compiled a
compile prog =
  let comp = runStateT (runReaderT (compileM prog) (CompileEnv "")) initState
      ((worlds, st), mgr) = runState comp emptyManager
  in (mgr, csWeights st, snd <$> csKnights st, worlds)

-- | Lift a BDD-manager action into the compile monad.
liftBDDM :: BDDM a -> CompileM a
liftBDDM = lift . lift

-- | Resolve a (possibly tag-prefixed) Knightian name.
resolveName :: String -> CompileM String
resolveName baseName = do
  currentTag <- asks ceTag
  return $ if null currentTag then baseName else currentTag ++ "." ++ baseName

-- | Allocate a fresh probabilistic BDD variable with the given Bernoulli weight.
flipVar :: Double -> CompileM BDD
flipVar p = do
  (var, varLabel) <- liftBDDM newVar
  modify' $ \s -> s
    { csWeights = IntMap.insert (unVarLabel varLabel) (Prob p) (csWeights s) }
  return var

-- | Return the BDD variable for a Knightian name, allocating one if it
--   hasn't been seen before.
knightVar :: forall n. KnownSymbol n => CompileM BDD
knightVar = do
  name <- resolveName (symbolVal (Proxy :: Proxy n))
  knights <- gets csKnights
  case Map.lookup name knights of
    Just (var, _) -> return var
    Nothing -> do
      (var, varLabel) <- liftBDDM newVar
      let i = Map.size knights
      modify' $ \s -> s
        { csWeights = IntMap.insert (unVarLabel varLabel) (Knight i) (csWeights s)
        , csKnights = Map.insert name (var, i) (csKnights s)
        }
      return var

-- | Compile a program to worlds.
compileM :: Ord a => Imp g a -> CompileM (Map a BDD)
compileM = \case
  ImpReturn a -> return (Map.singleton a BDDTrue)
  ImpFlip p -> do
    v <- flipVar p
    return (Map.fromList [(True, v), (False, bddNot v)])
  ImpKnight (_ :: Proxy n) -> do
    v <- knightVar @n
    return (Map.fromList [(True, v), (False, bddNot v)])
  ImpObserve b ->
    return (Map.singleton () (if b then BDDTrue else BDDFalse))
  ImpBind m f -> do
    worlds <- compileM m
    conjoined <- mapM (\(a, g) -> traverse (liftBDDM . bddAnd g) =<< compileM (f a))
                      (Map.toList worlds)
    liftBDDM (traverse bddAny (Map.unionsWith (++) [ (: []) <$> c | c <- conjoined ]))
  ImpTag (_ :: Proxy t) inner -> do
    let baseTag = symbolVal (Proxy :: Proxy t)
    resolvedTag <- if null baseTag then asks ceTag else resolveName baseTag
    local (\env -> env { ceTag = resolvedTag }) (compileM inner)
  ImpBranch True  t _ -> compileM t
  ImpBranch False _ f -> compileM f