packages feed

datalog-0.2.0.1: tools/repl/Main.hs

{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE FlexibleContexts #-}
module Main ( main ) where

import qualified Control.Monad.Catch as E
import Control.Monad.Trans.Class ( lift )
import Control.Monad.Trans.State.Strict ( evalStateT, StateT, modify, gets )
import qualified Data.Foldable as F
import qualified Data.List as L
import qualified Data.Map as M
import Data.Maybe ( catMaybes )
import Data.Sequence ( Seq, (|>) )
import qualified Data.Sequence as Seq
import qualified Data.Text as T
import Data.Typeable ( Typeable )
import qualified System.Console.Haskeline as HL
import Text.Printf ( printf )

import Database.Datalog

import qualified Parser as P
import qualified Commands as C

main :: IO ()
main = evalStateT (HL.runInputT settings loop) s0
  where
    settings = HL.defaultSettings
    s0 = ReplState { commands = Seq.empty
                   , definedRelations = M.empty
                   }

type ReplM = StateT ReplState IO

data ReplState = ReplState { commands :: !(Seq C.Command)
                           , definedRelations :: !(M.Map String Int)
                           }

-- Each time a (non-query) command is entered, just record it in the
-- list.  If the command is a query, then "interpret" the whole list
-- of commands as a DatabaseBuilder action and run it to produce a
-- Database.  Then execute the query.  Displaying facts is just a
-- filter over the list of commands.

loop :: HL.InputT ReplM ()
loop = do
  minput <- HL.getInputLine "% "
  case minput of
    Nothing -> return ()
    Just input -> do
      let cmd = P.parseInput input
      case cmd of
        Left err -> HL.outputStrLn (show err) >> loop
        Right C.DumpFacts -> do
          cs <- lift $ gets commands
          F.forM_ cs $ \c ->
            case c of
              C.AssertFact cl -> HL.outputStrLn (clauseString cl)
              _ -> return ()
          loop
        Right C.DumpRules -> do
          cs <- lift $ gets commands
          F.forM_ cs $ \c -> do
            case c of
              C.AddRule ruleHead ruleBody ->
                HL.outputStrLn (ruleString ruleHead ruleBody)
              _ -> return ()
          loop
        Right (C.Query qc@(C.Clause name _)) -> do
          erows <- lift $ E.try (evaluateQuery qc)
          case erows of
            Left err -> do
              let errAs :: EvaluationError
                  errAs = err
              HL.outputStrLn (show errAs)
              loop
            Right rows -> do
              F.forM_ rows $ \row -> do
                let s = L.intercalate ", " row
                HL.outputStrLn $ printf "%s(%s)" name s
              loop
        Right C.Quit -> return ()
        Right C.Help -> printHelp >> loop
        Right c@(C.AssertFact f) -> do
          ok <- guardArity f
          case ok of
            Nothing ->
              lift $ modify $ \s -> s { commands = commands s |> c }
            Just err -> HL.outputStrLn err
          loop
        Right c@(C.AddRule ruleHead ruleBody) -> do
          hres <- guardArity ruleHead
          bress <- mapM guardArity ruleBody
          case catMaybes (hres : bress) of
            [] -> lift $ modify $ \s -> s { commands = commands s |> c }
            errs -> HL.outputStrLn (unlines errs)
          loop

guardArity :: (Show a) => C.Clause a -> HL.InputT ReplM (Maybe String)
guardArity f@(C.Clause name args) = do
  rels <- lift $ gets definedRelations
  case M.lookup name rels of
    Just arity | length args == arity -> return Nothing
               | otherwise ->
                 return $ Just ("Arity mismatch: " ++ show f ++ " should have arity " ++ show arity)
    Nothing -> do
      lift $ modify $ \s -> s { definedRelations = M.insert name (length args) (definedRelations s) }
      return Nothing

printHelp :: HL.InputT ReplM ()
printHelp =
  HL.outputStrLn $ unlines [ "Datalog REPL"
                           , ""
                           , "Commands"
                           , "  :help - this text"
                           , "  :quit - exit the repl"
                           , "  :facts - print all defined facts"
                           , "  :rules - print all defined rules"
                           , ""
                           , "Syntax"
                           , "  To declare a fact:"
                           , "    relation1(arg1, arg2)."
                           , "  To define a rule:"
                           , "    relation2(X, Y) :- relation1(X, Z), relation1(Z, Y)."
                           , "  To issue a query:"
                           , "    relation2(X, Y)?"
                           , ""
                           , "  Variables are in all caps.  Literals (atoms) begin with a lowercase letter.  Relation names also begin with a lowercase letter."
                           ]

ruleString :: C.Clause C.AnyValue -> [C.Clause C.AnyValue] -> String
ruleString ruleHead ruleBody =
  concat [ cstring ruleHead
         , " :- "
         , L.intercalate ", " (map cstring ruleBody)
         ]
  where
    cstring (C.Clause name args) =
      let strs = L.intercalate ", " $ map valToString args
      in printf "%s(%s)" name strs
    valToString (C.AVVariable s) = s
    valToString (C.AVLiteral (C.LVString s)) = s

clauseString :: C.Clause C.LiteralValue -> String
clauseString (C.Clause name lits) = printf "%s(%s)" name strs
  where
    strs = L.intercalate ", " $ map litToString lits

litToString :: C.LiteralValue -> String
litToString (C.LVString s) = s

pleatM :: (Monad m, F.Foldable f) => a -> f b -> (a -> b -> m a) -> m a
pleatM seed elts f = F.foldlM f seed elts

evaluateQuery :: C.Clause C.AnyValue -> StateT ReplState IO [[String]]
evaluateQuery (C.Clause name vals) = do
  cs <- gets commands
  db <- makeDatabase $ do
    _ <- pleatM M.empty cs $ \ !a c -> do
      case c of
        C.AssertFact fact@(C.Clause rel factVals) ->
          case M.lookup rel a of
            Nothing -> do
              let arity = length factVals
              r <- addRelation (T.pack rel) arity
              assertFact r (map litToString factVals)
              lift $ modify $ \s -> s { definedRelations = M.insert rel arity (definedRelations s) }
              return $ M.insert rel (r, arity) a
            Just (r, arity) | arity == length factVals -> do
              assertFact r (map litToString factVals)
              return a
            Just (_, arity) -> E.throwM $ ArityMismatch arity fact
        _ -> return a
    return ()
  queryDatabase db $ do
    _ <- pleatM M.empty cs $ \ !a c -> do
      case c of
        C.AddRule h@(C.Clause headRel headVals) body -> do
          a1 <- checkArityDefs a h
          a2 <- F.foldlM checkArityDefs a1 body
          hr <- inferencePredicate (T.pack headRel)
          let headTerms = map toTerm headVals
              bodies = map toBodyClause body
          assertRule (hr, headTerms) bodies
          return a2
        _ -> return a
    qrel <- inferencePredicate (T.pack name)
    issueQuery qrel (map toTerm vals)

toBodyClause :: C.Clause C.AnyValue -> QueryBuilder ReplM String (Literal Clause String)
toBodyClause c@(C.Clause rel vals) = do
  checkArity c
  r <- inferencePredicate (T.pack rel)
  lit r (map toTerm vals)

toTerm :: C.AnyValue -> Term String
toTerm (C.AVVariable v) = LogicVar (T.pack v)
toTerm (C.AVLiteral (C.LVString l)) = Atom l

checkArityDefs :: M.Map String (Relation, Int)
                  -> C.Clause C.AnyValue
                  -> QueryBuilder ReplM String (M.Map String (Relation, Int))
checkArityDefs defs c@(C.Clause rel vals) = do
  checkArity c
  case M.lookup rel defs of
    Nothing -> do
      r <- inferencePredicate (T.pack rel)
      return $ M.insert rel (r, length vals) defs
    Just (_, arity) | arity == length vals -> return defs
                    | otherwise -> E.throwM $ ArityMismatch2 arity c

checkArity :: C.Clause C.AnyValue -> QueryBuilder ReplM String ()
checkArity c@(C.Clause rel vals) = do
  rs <- lift $ gets definedRelations
  case M.lookup rel rs of
    Just arity | carity == arity -> return ()
               | otherwise -> E.throwM $ ArityMismatch2 arity c
    Nothing -> lift $ modify $ \s -> s { definedRelations = M.insert rel carity (definedRelations s) }
  where
    carity = length vals

data EvaluationError = ArityMismatch Int (C.Clause C.LiteralValue)
                     | ArityMismatch2 Int (C.Clause C.AnyValue)
                     deriving (Eq, Ord, Show, Typeable)

instance E.Exception EvaluationError