datalog-0.2.0.1: src/Database/Datalog/MagicSets.hs
{-# LANGUAGE BangPatterns #-}
module Database.Datalog.MagicSets ( magicSetsRules, seedDatabase ) where
import Control.Monad ( MonadPlus(..), foldM )
import qualified Control.Monad.Catch as E
import Data.Hashable
import Data.HashMap.Strict ( HashMap )
import qualified Data.HashMap.Strict as HM
import Data.HashSet ( HashSet )
import qualified Data.HashSet as HS
import Data.List ( foldl' )
import Data.Maybe ( fromMaybe )
import Data.Monoid
import Data.Sequence ( Seq, (><), ViewL(..) )
import qualified Data.Sequence as S
import Data.Text ( Text )
import Database.Datalog.Adornment
import Database.Datalog.Database
import Database.Datalog.Errors
import Database.Datalog.Relation
import Database.Datalog.Rules
import Debug.Trace
debug = flip trace
-- FIXME: All references to negated relations must refer to the
-- Rel[FFF] relation version because we don't transform those into
-- versions with bound variables
seedDatabase :: (E.MonadThrow m, Eq a, Hashable a, Show a)
=> Database a
-> [Rule a]
-> Query a
-> [(Text, a)]
-> m (Database a)
seedDatabase db0 rs (Query (Clause (Relation rname) ts)) bindings = do
(tup, bs) <- foldM toTuple ([], []) ts
let magicRel = MagicRelation (BindingPattern (reverse bs)) rname
r0 = ensureDatabaseRelation db0 magicRel (length tup)
-- If there is a rule that defines the magic relation, we need
-- to force the evaluator to evaluate that rule by toggling the
-- dirty bit (delta table). We do this by using
-- addTupleToRelation. If there is no rule defining the magic
-- table, we can't do that because the delta bit will never be
-- toggled off and the evaluator will loop forever. In that
-- case, we have to use addTupleToRelation'
r1 = case any (definesRelation magicRel) rs of
True -> addTupleToRelation r0 (Tuple (reverse tup))
False -> addTupleToRelation' r0 (Tuple (reverse tup))
return $! replaceRelation db0 r1
where
toTuple acc@(tacc, bacc) t =
case t of
Atom a -> return (a : tacc, B : bacc)
BindVar name ->
case lookup name bindings of
Nothing -> E.throwM (NoVariableBinding name)
Just v -> return (v : tacc, B : bacc)
LogicVar _ -> return (tacc, F : bacc)
FreshVar _ -> return (tacc, F : bacc)
Anything -> error "Anything should be removed before seedDatabase"
definesRelation :: Relation -> Rule a -> Bool
definesRelation r (Rule ac _ _) = adornedClauseRelation ac == r
-- | Returns the rules generated by the magic sets transformation
--
-- If there are no BoundVars or Atoms in the query, don't perform the
-- transformation since it won't help much.
--
-- Note that performing the simple magic sets transformation on a
-- negated literal can break stratification. For now, this
-- implementation will not compute magic sets for negated literals.
-- That is, if a relation appears as a negated literal, do not perform
-- the magic transformation on it. It isn't quite clear to me if it
-- is just literals appearing negated or all literals used to define
-- literals appearing negated.
--
-- There is an algorithm in
--
-- > I. Balbin, G.S. Port, K. Ramamohanarao, K. Meenakshi, Efficient bottom-up computation of queries on stratified databases, The Journal of Logic Programming, Volume 11, Issues 3–4, October–November 1991, Pages 295-344, ISSN 0743-1066, 10.1016/0743-1066(91)90030-S.
-- > (http://www.sciencedirect.com/science/article/pii/074310669190030S)
--
-- that handles magic for negated literals.
magicSetsRules :: (E.MonadThrow m, Hashable a, Eq a, Show a)
=> Query a -- ^ The goal query
-> [(Clause a, [Literal Clause a])] -- ^ The user-provided rules
-> m [Rule a]
magicSetsRules q rs =
-- mapM adornRule rs
transformRules (S.singleton (queryPattern q)) mempty
where
-- These cannot be transformed
negatedRelations = foldr collectNegatedRelations mempty rs
-- Any relations in this list are inferred by rules and are
-- therefore eligible for the magic transformation (relations
-- in the fact database are not).
rawRules = foldr groupRules mempty rs
groupRules r = HM.insertWith (++) (clauseRelation (fst r)) [r]
inferredRelations = HS.fromList $ HM.keys rawRules
isInferred :: QueryPattern -> Bool
isInferred p = HS.member (queryPatternRelation p) inferredRelations
transformRules !worklist !generated =
case S.viewl worklist of
EmptyL -> do
let filteredRules = concat (HM.elems generated)
recPreds = HS.fromList $ map queryPatternRelation (HM.keys generated)
magicFilterTables = concatMap (toMagicFilterTable recPreds) filteredRules
mapM adornRule (map fst filteredRules ++ magicFilterTables)
elt :< rest ->
case HM.lookup elt generated of
-- Already processed this binding pattern
Just _ -> transformRules rest generated
Nothing -> do
let matchingRules = fromMaybe (error "No rules for pattern") $ HM.lookup (queryPatternRelation elt) rawRules
(magic, newWork) <- foldM (magicTransform elt) (mempty, mempty) matchingRules
transformRules (rest >< newWork) (HM.insert elt magic generated)
-- The QueryPattern doesn't affect the adornments added for the
-- sideways information passing strategy (for that, the terms in the
-- head area *always* bound). The QueryPattern is separate and is
-- only used to compute other QueryPatterns for the worklist and to
-- determine whether or not magic needs to be applied.
magicTransform :: (E.MonadThrow m, Hashable a, Eq a, Show a)
=> QueryPattern
-> ([((Clause a, [Literal Clause a]), [QueryPattern])], Seq QueryPattern)
-> (Clause a, [Literal Clause a])
-> m ([((Clause a, [Literal Clause a]), [QueryPattern])], Seq QueryPattern)
magicTransform bp (newRules, work) rawRule@(c, lits) = do
let hasB = hasBinding bp
isNeg = HS.member (clauseRelation c) negatedRelations
bodyBindingPattern = reverse $ snd $ foldl' bindVars (patternToInitialMap bp c, []) lits
adornedLits = zip lits bodyBindingPattern
newDeps = filter isInferred bodyBindingPattern
newWork = work >< S.fromList newDeps
case not hasB || isNeg of
True -> do
-- If a rule has no bindings in the head (or has a
-- negation), we don't do the magic transformation. We
-- still need to make sure all of its reachable literals are
-- processed, though.
return ((rawRule, []) : newRules, newWork)
False -> do
let (mf, mp) = buildMagicFilter bp c
return (((c, mf : lits), mp : bodyBindingPattern) : newRules, newWork)
-- | For each literal referencing a recursive relation (even if it is
-- recursive in a different rule), generate a magic filter table
-- definition rule for it.
toMagicFilterTable :: (Eq a)
=> HashSet Relation
-> ((Clause a, [Literal Clause a]), [QueryPattern])
-> [(Clause a, [Literal Clause a])]
toMagicFilterTable ps ((c, lits), qps) =
map (buildMagicFilterRule lits) (filter (isRecPred . fst) body)
where
body = zip lits qps
isRecPred l =
case l of
Literal (Clause r _) -> r `HS.member` ps
_ -> False
-- | Take a binding pattern and a rule head and create its magic
-- filter literal. The magic filter literal is the head clause
-- changed to reference a magic version of the same relation and with
-- the free columns deleted.
buildMagicFilter :: QueryPattern -> Clause a -> (Literal Clause a, QueryPattern)
buildMagicFilter qp (Clause (Relation t) ts) =
(Literal (Clause mrel retainedTs), QueryPattern mrel (BindingPattern (map (const F) retainedTs)))
where
mrel = MagicRelation bp t
bp = queryPatternBindings qp
retainedTs = takeBoundTerms qp ts
buildMagicFilter _ _ = error "Cannot have a magic relation yet"
takeBoundTerms :: QueryPattern -> [Term a] -> [Term a]
takeBoundTerms (QueryPattern _ qp) ts =
map snd retainedTuples
where
allTuples = zip (bindingPattern qp) ts
retainedTuples = filter ((==B) . fst) allTuples
-- | For each occurrence of the head clause in a literal, generate a
-- rule defining the magic filter.
--
-- To do that for occurrence O,
--
-- 1) Delete everything to the right of O in the body
--
-- 2) Turn O into a magic clause and delete its free columns
--
-- 3) Replace the head with O
buildMagicFilterRule :: (Eq a)
=> [Literal Clause a]
-> (Literal Clause a, QueryPattern)
-> (Clause a, [Literal Clause a])
buildMagicFilterRule lits (lc@(Literal c), qp) =
let retainedLits = takeWhile (/= lc) lits
retainedTerms = takeBoundTerms qp (clauseTerms c)
Relation relName = clauseRelation c
h = Clause (MagicRelation (queryPatternBindings qp) relName) retainedTerms
in (h, retainedLits)
bindVars :: (Eq a, Hashable a)
=> (HashSet (Term a), [QueryPattern])
-> Literal Clause a
-> (HashSet (Term a), [QueryPattern])
bindVars acc@(alreadyBound, patts) l =
case l of
ConditionalClause _ _ _ _ -> acc
Literal (Clause r ts) ->
let (binds, qp) = foldl' bindVar (alreadyBound, []) ts
in (binds, QueryPattern r (BindingPattern (reverse qp)) : patts)
-- For now, we treat all variables in a negated literal as Free
-- because we don't want to generate any magic clauses for them
-- (that can break stratification). Treating them all as free
-- here gets them properly skipped later.
NegatedLiteral (Clause r ts) ->
let qp = map (const F) ts
in (alreadyBound, QueryPattern r (BindingPattern qp) : patts)
where
bindVar (bindings, bs) t =
case t `HS.member` bindings of
True -> (bindings, B : bs)
False ->
case t of
LogicVar _ -> (HS.insert t bindings, F : bs)
Anything -> error "Wildcard variables should have been rewritten already"
FreshVar _ -> (HS.insert t bindings, F : bs)
BindVar _ -> (bindings, B : bs)
Atom _ -> (bindings, B : bs)
patternToInitialMap :: (Eq a, Hashable a) => QueryPattern -> Clause a -> HashSet (Term a)
patternToInitialMap qp (Clause _ ts) =
HS.fromList $ takeBoundTerms qp ts
data QueryPattern = QueryPattern { queryPatternRelation :: Relation
, queryPatternBindings :: BindingPattern
}
deriving (Eq, Show)
instance Hashable QueryPattern where
hashWithSalt s (QueryPattern r bs) =
s `hashWithSalt` r `hashWithSalt` bs
hasBinding :: QueryPattern -> Bool
hasBinding (QueryPattern _ bs) = any (==B) (bindingPattern bs)
queryPattern :: Query a -> QueryPattern
queryPattern (Query c) =
QueryPattern (clauseRelation c) $ BindingPattern (map toBinding (clauseTerms c))
where
toBinding t =
case t of
Atom _ -> B
BindVar _ -> B
LogicVar _ -> F
Anything -> F
FreshVar _ -> F
-- If the input query binding doesn't have any bound elements, the
-- rule gets no magic.
-- FIXME: This would be better if dead rules couldn't affect it...
-- a dead rule with a negation will be a problem.
collectNegatedRelations :: (Clause a, [Literal Clause a])
-> HashSet Relation
-> HashSet Relation
collectNegatedRelations (_, cs) acc =
foldr addIfNegated acc cs
where
addIfNegated (NegatedLiteral (Clause h _)) s = HS.insert h s
addIfNegated _ s = s
-- If the rule ends up with multiple binding patterns for the
-- recursive rule, the rule needs to be split. This means that, for
-- each binding pattern, the full set of rules defining that relation
-- must be duplicated
-- If the query has a bound literal, that influences the rules it
-- corresponds to. Other rules are not affected. Those positions
-- bound in the query are bound in the associated rules.
--
-- Note: all variables in the head must appear in the body
adornRule :: (E.MonadThrow m, Eq a, Hashable a)
=> (Clause a, [Literal Clause a]) -> m (Rule a)
adornRule (hd, lits) = do
(vmap, lits') <- mapAccumM adornLiteral mempty lits
(allVars, Literal hd') <- adornLiteral vmap (Literal hd)
let headVars = HS.fromList (clauseTerms hd)
-- FIXME: This test isn't actually strict enough. All head vars
-- must appear in a non-negative literal
case headVars `HS.difference` (HS.fromList (HM.keys allVars)) == mempty of
True -> return $! Rule hd' lits' allVars
False -> E.throwM RangeRestrictionViolation
adornLiteral :: (E.MonadThrow m, Eq a, Hashable a)
=> HashMap (Term a) Int
-> Literal Clause a
-> m (HashMap (Term a) Int, Literal AdornedClause a)
adornLiteral boundVars l =
case l of
Literal c -> adornClause Literal c
NegatedLiteral c -> adornClause NegatedLiteral c
ConditionalClause cid f ts _ ->
return (boundVars, ConditionalClause cid f ts boundVars)
where
adornClause constructor (Clause p ts) = do
(bound', ts') <- mapAccumM adornTerm boundVars ts
let c' = constructor $ AdornedClause p ts'
return (bound', c')
adornTerm bvs t =
case t of
BindVar _ -> error "Bind variables are only allowed in queries"
Anything -> error "Anything should have been removed already"
-- Atoms are always bound
Atom _ -> return (bvs, (t, BoundAtom))
LogicVar _ ->
-- The first occurrence is Free, while the rest are Bound
case HM.lookup t bvs of
Just ix -> return (bvs, (t, Bound ix))
Nothing ->
let ix = HM.size bvs
in return (HM.insert t ix bvs, (t, Free ix))
FreshVar _ ->
let ix = HM.size bvs
in return (HM.insert t ix bvs, (t, Free ix))
-- Helpers missing from the standard libraries
{-# INLINE mapAccumM #-}
-- | Monadic mapAccumL
mapAccumM :: (Monad m, MonadPlus p) => (acc -> x -> m (acc, y)) -> acc -> [x] -> m (acc, p y)
mapAccumM _ z [] = return (z, mzero)
mapAccumM f z (x:xs) = do
(z', y) <- f z x
(z'', ys) <- mapAccumM f z' xs
return (z'', return y `mplus` ys)