packages feed

hatt-1.5.0.0: src/hatt.hs

{-# LANGUAGE DeriveDataTypeable #-}

module Main (main) where

import Data.Logic.Propositional
import Data.Logic.Propositional.Tables
import Data.Logic.Propositional.NormalForms

import Control.Monad (when, unless)
import Data.Char (toLower)
import System.Console.CmdArgs
import System.Console.Haskeline
    ( InputT
    , runInputT
    , defaultSettings
    , getInputLine
    , outputStr
    , outputStrLn
    )

data Command = Exit
             | Help
             | Pretty
             | Coloured
             | Eval Expr
             | Convert NormalForm Expr
             | Error String

data ProgramMode = ProgramMode
  { evaluate    :: String
  , interactive :: Bool
  , pretty      :: Bool
  , coloured    :: Bool
  } deriving (Show, Data, Typeable)

data NormalForm = NNF | CNF | DNF

programMode :: ProgramMode
programMode = ProgramMode
  { evaluate    = "" &= typ  "EXPRESSION"
                     &= help "Print the truth table for the given expression"
  , interactive = False &= help "Enter interactive mode"
  , pretty      = False &= help "Use Unicode logic symbols"
  , coloured    = False &= help "Use colour-coded symbols"
  } &= summary "Hatt 1.4.0.2, (c) Benedict Eastaugh 2012"
    &= program "hatt"

main :: IO ()
main = do opts <- cmdArgs programMode
          let expStr    = evaluate opts
              interMode = interactive opts
              evalMode  = (not . null) expStr
              printer   = selectPrinter opts
          
          -- If the --evaluate flag is passed with an expression, print the
          -- truth table for that expression.
          when evalMode $ putStr (eval printer expStr)
          
          -- Unless the --evaluate flag is passed with an expression and
          -- interactive mode is NOT explicitly requested, terminate the
          -- program; otherwise, enter interactive mode.
          unless (evalMode && not interMode) $
              putStrLn replIntroText
              >> runInputT defaultSettings (repl opts)

repl :: ProgramMode -> InputT IO ()
repl mode = do
    minput <- getInputLine "> "
    case minput of
      Nothing  -> return ()
      Just cmd -> case parseCommand cmd of
        Exit              -> return ()
        Help              -> outputStr (replHelpText printer)
                             >> repl mode
        Pretty            -> outputStrLn ppMessage
                             >> repl (mode {pretty = not isPretty})
        Coloured          -> outputStrLn cpMessage
                             >> repl (mode {coloured = not isColoured})
        (Eval expr)       -> outputStr (truthTableP printer expr)
                             >> repl mode
        (Convert nf expr) -> outputStrLn (toNFStr nf (fst printer) expr)
                             >> repl mode
        (Error err)       -> outputStrLn ("Error: " ++ err)
                             >> repl mode
  where
    printer    = selectPrinter mode
    isPretty   = pretty mode
    isColoured = coloured mode
    ppMessage  = (if isPretty then "Dis" else "En") ++ "abling pretty-printing."
    cpMessage  = (if isColoured then "Dis" else "En") ++ "abling colour-coding."

eval :: Printer -> String -> String
eval p str = case parseExpr "" str of
               Left  err  -> "Parse error at " ++ show err ++ "\n"
               Right expr -> truthTableP p expr

parseCommand :: String -> Command
parseCommand input = case cmd . words $ input of
                       ""       -> Error "you must enter an expression or a command."
                       "exit"   -> Exit
                       "help"   -> Help
                       "pretty" -> Pretty
                       "colour" -> Coloured
                       "nnf"    -> eval_ (Convert NNF) (getExpr input)
                       "cnf"    -> eval_ (Convert CNF) (getExpr input)
                       "dnf"    -> eval_ (Convert DNF) (getExpr input)
                       _        -> eval_ Eval input
  where
    cmd []       = ""
    cmd ws       = map toLower . head $ ws
    eval_ dt str = case parseExpr "hatt" str of
                     Left  err  -> Error $ "parse error at " ++ show err
                     Right expr -> dt expr
    getExpr      = unwords . tail . words

toNFStr :: NormalForm -> (Expr -> String) -> Expr -> String
toNFStr NNF p = p . toNNF
toNFStr CNF p = p . toCNF
toNFStr DNF p = p . toDNF

replIntroText :: String
replIntroText = unwords
  [ "Entering interactive mode."
  , "Type `help` if you don't know what to do!"
  ]

replHelpText :: Printer -> String
replHelpText printer = unlines
  [ "Hatt's interactive mode has several commands."
  , ""
  , "help"
  , "  Print this help text."
  , ""
  , "pretty"
  , "  Pretty-print expressions using Unicode logic symbols. Only employ this"
  , "  option if your console is Unicode-aware. If pretty-printing is already"
  , "  enabled, using this command will disable it."
  , ""
  , "colour"
  , "  Colour truth values: green for true, red for false. This feature needs"
  , "  your console to support ANSI colour codes. If coloured mode is already"
  , "  enabled, this command will disable it."
  , ""
  , "exit"
  , "  Quit the program."
  , ""
  , "If you don't type in a command, the program will assume you're writing a"
  , "logical expression to be evaluated and attempt to parse it."
  , ""
  , "For example, if you enter \"(A -> B)\" at the prompt, Hatt will print the"
  , "truth table for that expression. Here's an example console session."
  , ""
  , "    > " ++ showAscii exp1
  , indentBy 4 $ truthTableP printer exp1
 ++ "> " ++ showAscii exp2 ++ "\n"
 ++ truthTableP printer exp2
  , "You can also convert expressions to different normal forms: negation"
  , "normal form, conjunctive normal form and disjunctive normal form. To do"
  , "this just prepend the expression you want to convert with \"nnf\", \"cnf\""
  , "or \"dnf\". For example,"
  , ""
  , "    > nnf " ++ showAscii exp3
  , "    " ++ (fst printer . toNNF) exp3
  , ""
  , "If none of this makes any sense, try reading the README file."
  ]
  where
      exp1 = Disjunction (Variable $ Var 'A') (Variable $ Var 'B')
      exp2 = Conditional (Variable $ Var 'P') exp4
      exp3 = Negation exp2
      exp4 = Conjunction (Variable $ Var 'Q') (Variable $ Var 'R')

selectPrinter :: ProgramMode -> Printer
selectPrinter m = let expPrinter   = if pretty m then show else showAscii
                      tablePrinter = if coloured m then colourBool else showBool
                  in (expPrinter, tablePrinter)

indentBy :: Int -> String -> String
indentBy n = unlines . map (replicate n ' ' ++) . lines