packages feed

datalog-0.2.0.1: src/Database/Datalog/Evaluate.hs

{-# LANGUAGE BangPatterns, ScopedTypeVariables #-}
-- | This module defines the evaluation strategy of the library.
--
-- It currently uses a bottom-up semi-naive evaluator.
module Database.Datalog.Evaluate (
  applyRuleSet,
  select
  ) where

import Control.Applicative
import qualified Control.Monad.Catch as E
import Control.Monad ( foldM, liftM )
import Control.Monad.ST.Strict
import Data.Graph
import Data.Hashable
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashMap.Strict as HM
import Data.Maybe ( fromMaybe )
import Data.Monoid
import Data.Vector.Mutable ( STVector )
import qualified Data.Vector.Mutable as V

import Database.Datalog.Database
import Database.Datalog.Rules

-- | Bindings are vectors of values.  Each variable in a rule is
-- assigned an index in the Bindings during the adornment process.
-- When evaluating a rule, if a free variable is encountered, all of
-- its possible values are entered at the index for that variable in a
-- Bindings vector.  When a bound variable is encountered, its current
-- value is looked up from the Bindings.  If that value does not match
-- the concrete tuple being examined, that tuple is rejected.
--
-- The mapping of variable to index into the bindings vector is stored
-- in the Rule data structure.
newtype Bindings s a = Bindings (STVector s a)


-- | Apply a set of rules.  All of the rules must have the same head
-- relation.  This is what implements the semi-naive evaluation
-- strategy.  For each rule of the form
--
-- > T(x,y) |- G(x,z), T(z,y).
--
-- simulate the rule
--
-- > ΔT(x,y) |- G(x,z), ΔT(z,y).
--
-- That is, at each step only look at the *new* tuples for each
-- recursive relation.  The intuition is that, if a new tuple is to be
-- generated on the next step, it must reference a new tuple from this
-- step (otherwise it would have already been generated) If a relation
-- appears twice in a body:
--
-- > T(x,y) |- T(x,z), T(z,y).
--
-- we have to substitute ΔT once for *each* occurrence of T in the
-- body, with the other occurrences referencing the non-Δ table:
--
-- > ΔT(x,y) |- ΔT(x,z), T(z,y).
-- > ΔT(x,y) |- T(x,z), ΔT(z,y).
--
-- While collecting all of the new tuples (see projectLiteral), a new
-- Δ table is generated.
applyRuleSet :: (E.MonadThrow m, Eq a, Hashable a, Show a)
                => Database a -> [Rule a] -> m (Database a)
applyRuleSet _ [] = error "applyRuleSet: Empty rule set not possible"
applyRuleSet db rss@(r:_) = return $ runST $ do
  bss <- concat <$> mapM (applyRules db) (orderRules rss)
  db' <- projectLiterals db h bss
  return db' -- `debug` show db'
  where
    h = ruleHead r

-- | Each of the lists of generated bindings has its own
-- ruleVariableMap, so zip them together so that project has them
-- paired up and ready to use.
--
-- Apply a set of rules
applyRules :: (Eq a, Hashable a, Show a)
              => Database a
              -> [Rule a]
              -> ST s [(Rule a, [Bindings s a])]
applyRules db rs = do
  bs <- mapM (applyRule db) rs
  return $ zip rs bs

-- | Toplogically sort rules (with SCCs treated as a unit).  This
-- means that dependency rules will be fired before the rules that
-- depend on them, which is the best evaluation order we can hope for.
orderRules :: forall a . (Eq a, Hashable a) => [Rule a] -> [[Rule a]]
orderRules rs = map toList (stronglyConnComp deps)
  where
    toList (AcyclicSCC r) = [r]
    toList (CyclicSCC rss) = rss
    toKeyM = HM.fromList (zip rs [0..])
    toKey :: Rule a -> Int
    toKey r = fromMaybe (error "Missing toKeyM entry") $ HM.lookup r toKeyM

    deps = foldr toContext [] rs
    toContext r@(Rule _ b _) acc =
      -- All of the rules for a given relation are in the same SCC
      -- stratum, so we will see them all in @rs@
      let brules = concatMap relationToRules b
      in (r, toKey r, map toKey brules) : acc
    relationToRules rel = filter (hasRelHead rel) rs
    hasRelHead c (Rule h _ _) =
      case c of
        Literal ac -> adornedClauseRelation h == adornedClauseRelation ac
        -- This should probably be impossible since negated terms
        -- would be in a different stratum.
        NegatedLiteral ac -> adornedClauseRelation h == adornedClauseRelation ac
        _ -> False

-- | A worker to apply a single rule to the database (producing a new
-- database).  This handles deciding if we need to do any Δ-table
-- substitutions.  If not, it just does a simple fold with
-- joinLiteral.
applyRule :: (Eq a, Hashable a, Show a)
             => Database a -> Rule a -> ST s [Bindings s a]
applyRule db r = do
  -- We need to substitute the ΔT table in for *one* occurrence of the
  -- T relation in the rule body at a time.  It must be substituted in
  -- at *each* position where T appears.
  case any (referencesRelation hr) b of
    -- If the relation does not appear in the body at all, we don't
    -- need to do the delta substitution.
    False -> do
      v0 <- V.new (HM.size m)
      foldM (joinLiteral db) [Bindings v0] b
    -- Otherwise, swap the delta table in for each each occurrence of
    -- the relation in the body.
    True -> concat <$> foldM (joinWithDeltaAt db hr b m) [] b
  where
    h = ruleHead r
    hr = adornedClauseRelation h
    b = ruleBody r
    m = ruleVariableMap r

-- | Return True if the given literal references the given Relation
referencesRelation:: Relation -> Literal AdornedClause a -> Bool
referencesRelation hrel rel =
  case rel of
    Literal l -> adornedClauseRelation l == hrel
    NegatedLiteral l -> adornedClauseRelation l == hrel
    _ -> False

-- | The worker that substitutes a Δ-table for each clause referencing
-- the relation @hr@.
joinWithDeltaAt :: (Eq a, Hashable a)
                   => Database a
                   -> Relation
                   -> [Literal AdornedClause a]
                   -> HashMap k v
                   -> [[Bindings s a]]
                   -> Literal AdornedClause a
                   -> ST s [[Bindings s a]]
joinWithDeltaAt db hr b m acc c =
  case referencesRelation hr c of
    -- This clause doesn't reference the relation so don't do anything
    False -> return acc
    -- This clause does reference it, so we need to evaluate the
    -- entire rule here.  swapJoin handles substituting the Δ table
    -- for the relation in this clause (see withDeltaRelation - it
    -- makes a new database with the Δ swapped for the data of this
    -- relation).
    True -> do
      v0 <- V.new (HM.size m)
      bs <- foldM swapJoin [Bindings v0] b
      return (bs : acc)
  where
    swapJoin bs thisClause =
      case thisClause == c of
        False -> joinLiteral db bs thisClause
        True -> withDeltaRelation db hr $ \db' -> joinLiteral db' bs thisClause

-- | Ensure that the relation named by the clause argument is in the
-- database.  Get the DBRelation.  Then fold over the Bindings,
-- constructing a tuple for each one (that is inserted into the
-- relation).  Then build a new database with that relation replaced
-- with the new one.
projectLiterals :: (Eq a, Hashable a, Show a)
                   => Database a
                   -> AdornedClause a
                   -> [(Rule a, [Bindings s a])]
                   -> ST s (Database a)
projectLiterals db c bssMaps = do
  let r = adornedClauseRelation c
      rel = ensureDatabaseRelation db r (length (adornedClauseTerms c))
      rel' = resetRelationDelta rel
  -- We reset the delta since we are computing the new delta for the
  -- next iteration.  The act of adding tuples to the relation
  -- automatically computes the delta.
  rel'' <- foldM (\irel (rule, bs) -> foldM (project rule) irel bs) rel' bssMaps
  return $ replaceRelation db rel''
  where
    project rule !r b = do
      t <- bindingsToTuple (ruleHead rule) (ruleVariableMap rule) b
      return $ addTupleToRelation r t

-- | Determine if a PartialTuple and a concrete Tuple from the
-- database match.  Walks the partial tuple (which is sorted by index)
-- and the current tuple in parallel and tries to avoid allocations as
-- much as possible.
tupleMatches :: (Eq a) => PartialTuple a -> Tuple a -> Bool
tupleMatches (PartialTuple pvs) (Tuple vs) =
  parallelTupleWalk pvs vs

parallelTupleWalk :: (Eq a) => [Maybe a] -> [a] -> Bool
parallelTupleWalk [] [] = True
parallelTupleWalk (p:ps) (v:vs) =
  case p of
    Nothing -> parallelTupleWalk ps vs
    Just pv -> pv == v && parallelTupleWalk ps vs
parallelTupleWalk _ _ = error "Partial tuple length mismatch"

{-# INLINE scanSpace #-}
-- | The common worker for 'select' and 'matchAny'
scanSpace :: (Eq a)
             => ((Tuple a -> Bool) -> [Tuple a] -> b)
             -> Database a
             -> Relation
             -> PartialTuple a
             -> b
scanSpace f db p pt = f (tupleMatches pt) space
  where
    -- FIXME: This is where we use the index, if available.  If not,
    -- we have to fall back to a table scan.  Instead of computing
    -- indices up front, it may be best to only compute them on the
    -- fly (and then only if they will be referenced again later).
    -- They can be thrown away as soon as they can't be referenced
    -- again.  This will save storage and up-front costs.

    -- Note that the relation might not exist in the database here
    -- because this is the first time data is being inferred for the
    -- EDB.  In that case, just start with empty data and the project
    -- step will insert the table into the database for the next step.
    space = fromMaybe mempty (dataForRelation db p)

-- | Return all of the tuples in the given relation that match the
-- given PartialTuple
select :: (Eq a) => Database a -> Relation -> PartialTuple a -> [Tuple a]
select = scanSpace filter

-- | Return true if any tuples in the given relation match the given
-- 'PartialTuple'
anyMatch :: (Eq a) => Database a -> Relation -> PartialTuple a -> Bool
anyMatch = scanSpace any

{-# INLINE joinLiteralWith #-}
-- | The common worker for the non-conditional clause join functions.
joinLiteralWith :: AdornedClause a
                   -> [Bindings s a]
                   -> (Bindings s a -> PartialTuple a -> ST s [Bindings s a])
                   -> ST s [Bindings s a]
joinLiteralWith c bs f = concatMapM (apply c f) bs
  where
    apply cl fn b = do
      pt <- buildPartialTuple cl b
      fn b pt

-- | Join a literal with the current set of bindings.  This can
-- increase the number of bindings (for a non-negated clause) or
-- decrease the number of bindings (for a negated or conditional
-- clause).
joinLiteral :: (Eq a, Hashable a)
               => Database a
               -> [Bindings s a]
               -> Literal AdornedClause a
               -> ST s [Bindings s a]
joinLiteral db bs (Literal c) = joinLiteralWith c bs (normalJoin db c)
joinLiteral db bs (NegatedLiteral c) = joinLiteralWith c bs (negatedJoin db c)
joinLiteral _ bs (ConditionalClause _ p vs m) =
  foldM (applyJoinCondition p vs m) [] bs

-- | Extract the values that the predicate requires from the current
-- bindings.  Apply the predicate and if it returns True, retain the
-- set of bindings; otherwise, discard it.
applyJoinCondition :: (Eq a, Hashable a)
                      => ([a] -> Bool)
                      -> [Term a]
                      -> HashMap (Term a) Int
                      -> [Bindings s a]
                      -> Bindings s a
                      -> ST s [Bindings s a]
applyJoinCondition p vs m acc b@(Bindings binds) = do
  vals <- mapM extractBinding vs
  case p vals of
    True -> return $! b : acc
    False -> return acc
  where
    extractBinding t =
      let Just ix = HM.lookup t m
      in V.read binds ix

-- | Non-negated join; it works by selecting all of the tuples
-- matching the input PartialTuple and then recording all of the newly
-- bound variable values (i.e., the free variables in the rule).  This
-- produces one set of bindings for each possible value of the free
-- variables in the rule (or could be empty if there are no matching
-- tuples).
normalJoin :: (Eq a, Hashable a) => Database a -> AdornedClause a -> Bindings s a
              -> PartialTuple a -> ST s [Bindings s a]
normalJoin db c binds pt = mapM (projectTupleOntoLiteral c binds) ts
  where
    ts = select db (adornedClauseRelation c) pt

-- | Retain the input binding if there are no matches in the database
-- for the input PartialTuple.  Otherwise reject it.
negatedJoin :: (Eq a, Hashable a) => Database a -> AdornedClause a -> Bindings s a
               -> PartialTuple a -> ST s [Bindings s a]
negatedJoin db c binds pt =
  case anyMatch db (adornedClauseRelation c) pt of
    True -> return []
    False -> return [binds]

-- | For each term in the clause, take it as a literal if it is bound
-- or is an atom.  Otherwise, leave it as free (not mentioned in the
-- partial tuple).
buildPartialTuple :: AdornedClause a -> Bindings s a -> ST s (PartialTuple a)
buildPartialTuple c (Bindings bs) =
  PartialTuple <$> mapM toPartial (adornedClauseTerms c)
  where
    toPartial ta =
      case ta of
        (Atom a, BoundAtom) -> return $! Just a
        (_, Bound slot) -> do
          b <- V.read bs slot
          return $! Just b
        _ -> return Nothing


-- | For each free variable in the tuple (according to the adorned
-- clause), enter its value into the input bindings
projectTupleOntoLiteral :: AdornedClause a -> Bindings s a -> Tuple a -> ST s (Bindings s a)
projectTupleOntoLiteral c (Bindings binds) (Tuple t) = do
  -- We need a copy here because the input bindings are shared among
  -- many calls to this function
  b <- V.clone binds
  let atoms = zip (adornedClauseTerms c) t
  mapM_ (bindFreeVariable b) atoms
  return $! Bindings b
  where
    bindFreeVariable b ((_, adornment), val) =
      case adornment of
        Free ix -> V.write b ix val
        _ -> return ()

-- | Convert a set of variable bindings to a tuple that matches the
-- input clause (which should have all variables).  This is basically
-- unifying variables with the head of the rule.
bindingsToTuple :: (Eq a, Hashable a, Show a)
                   => AdornedClause a
                   -> HashMap (Term a) Int
                   -> Bindings s a
                   -> ST s (Tuple a)
bindingsToTuple c vmap (Bindings bs) = do
  vals <- mapM variableTermToValue (adornedClauseTerms c)
  return $ Tuple vals
  where
    variableTermToValue (t, _) =
      case HM.lookup t vmap of
        Nothing -> error ("NonVariableInRuleHead " ++ show c ++ " " ++ show t ++ " " ++ show vmap)
        Just ix -> V.read bs ix


-- Helpers

{-# INLINE mapM' #-}
-- | This is an alternative definition of mapM that accumulates its
-- results on the heap instead of the stack.  This should avoid some
-- stack overflows when processing some million+ element lists..
mapM' :: (Monad m) => (a -> m b) -> [a] -> m [b]
mapM' f = go []
  where
    go acc [] = return (reverse acc)
    go acc (a:as) = do
      x <- f a
      go (x:acc) as

{-# INLINE concatMapM #-}
concatMapM :: (Monad m) => (a -> m [b]) -> [a] -> m [b]
concatMapM f xs = liftM concat (mapM' f xs)