packages feed

swearjure-1.0.0: src/Swearjure/Parser.hs

{-# OPTIONS_GHC -Wall -Werror #-}
{-# LANGUAGE DeriveFunctor, DeriveFoldable, DeriveTraversable #-}
{-# LANGUAGE FlexibleContexts #-}

module Swearjure.Parser
       ( PValF(..)
       , PVal
       , readAsts
       , readStatic
       , ParseResult(..)
       , feedCont
       ) where

import           Control.Applicative hiding (many)
import           Control.Monad.Reader
import           Data.Attoparsec.Text
import qualified Data.Attoparsec.Text.Lazy as L
import           Data.Char (isAlphaNum)
import           Data.Foldable (Foldable)
import           Data.Generics.Fixplate (Mu(..), ShowF(..), EqF(..), OrdF(..))
import           Data.Sequence
import qualified Data.Sequence as S
import qualified Data.Text as T
import qualified Data.Text.Lazy as B
import           Data.Traversable (Traversable)
import           Swearjure.Errors

type ParseState = Bool

data PValF p = PSym String
             | PString String
             | PKw String
             | PQualKw String
             | PChar Char
             | PFnLit [p]
             | PList [p]
             | PVec [p]
             | PSet [p]
             | PHM [(p, p)]
             | PSyntaxQuote p
             deriving (Eq, Ord, Show, Functor, Foldable, Traversable)

type PVal = Mu PValF

instance EqF PValF where equalF = (==)
instance OrdF PValF where compareF = compare
instance ShowF PValF where showsPrecF = showsPrec

cljns :: String
cljns = "clojure.core/"

-- | Discards the result of the parser
omit :: Parser p -> Parser ()
omit p = p >> return ()

-- | Semi matches a semicolon and returns it
semi :: Parser Char
semi = char ';'

commentLine :: Parser String
commentLine = skipMany1 semi >> manyTill anyChar (endOfLine <|> endOfInput)

-- | Filters out any kind of whitespace.
whiteSpace :: ParseState -> Parser ()
whiteSpace b = skipMany (omit (char ',') <|> omit space <|> omit commentLine
                         <|> try (sharpWhites b))

sharpWhites :: ParseState -> Parser ()
sharpWhites b = char '#' >> (sharp_ b <|> sharpBang)

sharp_ :: ParseState -> Parser ()
sharp_ b = char '_' >> omit (expr b)

sharpBang :: Parser ()
sharpBang = char '!' >> manyTill anyChar (endOfLine <|> endOfInput) >> return ()

lexeme :: Parser p -> ParseState -> Parser p
lexeme p b = whiteSpace b >> p

anyOf :: String -> Parser Char
anyOf = satisfy . inClass

-- fixme: These are.. nasty to do properly.
startSymChar :: Parser Char
startSymChar = anyOf "-+*/!=<>?_&$%"

symChar :: Parser Char
symChar = startSymChar <|> anyOf "#'"

-- TODO: If sym only contains single /, then / can't be the last value UNLESS
-- the symbol is "/".
symString :: Parser String
symString = do x <- startSymChar
               xs <- many' symChar
               return $ x : xs

symbol :: Parser PVal
symbol = (Fix . PSym) <$> symString

keyword :: Parser PVal
keyword = char ':' >> (nonQual <|> qual <|> alphaNums)
  where nonQual = Fix . PKw <$> symString
        qual = Fix . PQualKw <$> (char ':' >> symString)

between :: Parser open -> Parser close -> Parser a -> Parser a
between open close p = do { omit open; v <- p; omit close; return v }

swjString :: Parser PVal
swjString = Fix . PString <$> (between (char '"') (char '"')
                               (many' stringChar))
  where stringChar = stringLetter <|> stringEscape
                     <?> "string character (non-alphanumeric)"
        stringLetter = satisfy (\c -> (c /= '"') && (c /= '\\') && not (isAlphaNum c))
        -- Todo: Fix this properly?
        stringEscape = char '\\' >> anyOf "\\\""

delimited :: Char -> Char -> Parser c -> ParseState -> Parser c
delimited start stop p b = between (char start) (lexeme (char stop) b) p

list :: ParseState -> Parser PVal
list b = Fix . PList <$> delimited '(' ')' (many' $ expr b) b

vec :: ParseState -> Parser PVal
vec b = Fix . PVec <$> delimited '[' ']' (many' $ expr b) b

hashMap :: ParseState -> Parser PVal
hashMap b = Fix . PHM <$> delimited '{' '}' (many' pair) b
  where pair = (,) <$> expr b <*> expr b

call :: String -> [PVal] -> PVal
call s es = Fix . PList $ (Fix . PSym) s : es

sugared :: Char -> String -> ParseState -> Parser PVal
sugared c s b = do omit (char c)
                   e <- expr b
                   return $ call (cljns ++ s) [e]

quote :: ParseState -> Parser PVal
quote b = do omit (char '\'')
             e <- expr b
             return $ call "quote" [e]

syntaxQuote :: ParseState -> Parser PVal
syntaxQuote b = char '`' >> (Fix . PSyntaxQuote <$> expr b)

deref :: ParseState -> Parser PVal
deref = sugared '@' "deref"

optionMaybe :: Parser p -> Parser (Maybe p)
optionMaybe p = option Nothing (liftM Just p)

-- this needs a bit of help to avoid turning ~@foo into (unquote (deref foo))
unquote :: ParseState -> Parser PVal
unquote b = do omit $ char '~'
               splice <- optionMaybe $ char '@'
               e <- expr b
               return $ call (unquoteType splice) [e]
  where unquoteType (Just _) = cljns ++ "unquote-splicing"
        unquoteType Nothing = cljns ++ "unquote"

-- TODO: Should use attr to tag on vals.
--meta :: Parser PVal
--meta = do omit $ char '^'
--          m <- expr
--          e <- expr
--          return $ call "with-meta" [e, m]

-- sharpies be here

sharp :: ParseState -> Parser PVal
sharp b = char '#' >> (sharpQuote b <|> set b <|> fnLit b <|> unreadable)

sharpQuote :: ParseState -> Parser PVal
sharpQuote b = do omit (char '\'')
                  e <- expr b
                  return $ call "var" [e]

-- must propagate the Bool over the affected results
fnLit :: ParseState -> Parser PVal
fnLit True = fail "Nested #() are not allowed"
fnLit False = do omit $ char '('
                 exps <- many' (expr True)
                 omit $ char ')'
                 return $ Fix $ PFnLit exps

set :: ParseState -> Parser PVal
set b = Fix . PSet <$> delimited '{' '}' (many' $ expr b) b

unreadable :: Parser a
unreadable = char '<' >> fail "Unreadable form"

-- TODO: #=
-- TODO: #@? and #?

character :: Parser PVal
character = char '\\' >> (Fix . PChar <$> satisfy (not . isAlphaNum))

alphaNums :: Parser a
alphaNums = satisfy isAlphaNum >> fail "Alphanumeric characters are not allowed"

expr :: Bool -> Parser PVal
expr b
  = do whiteSpace b
       list b <|> vec b <|> symbol <|> keyword <|> swjString <|> hashMap b
         <|> quote b <|> syntaxQuote b <|> deref b <|> unquote b <|> sharp b <|>
         character <|> alphaNums

justWS :: String -> Bool
justWS s
  = case parse (whiteSpace False) (T.pack s) of
     (Fail _ _ _) -> False
     (Partial _) -> True
     (Done txt ()) -> T.null txt

type ResultSeq = (Seq (Either SwjError PVal))
type ParseFn = T.Text -> Result PVal

-- parsing != "reading". A PVal is expanded because of reader conditionals
data ParseResult = Results ResultSeq String
                 | Continuation ResultSeq String ParseFn

-- feeds Continuations more data. The continuation must be unwrapped
feedCont :: String -> ResultSeq -> String -> ParseFn -> ParseResult
feedCont s xs accum cont
  = merge $ readRec (cont $ T.pack $ s ++ "\n")
    where merge (vals, Nothing) = Results (xs >< vals) (accum ++ ('\n' : s))
          merge (vals, Just newCont) = Continuation (xs >< vals)
                                       (accum ++ ('\n' : s)) newCont

readRec :: Result PVal -> (ResultSeq, Maybe ParseFn)
readRec (Fail _ _ err) = (S.singleton $ Left $ SyntaxError err, Nothing)
readRec (Partial cont) = (S.empty, Just cont)
readRec (Done txt r) | justWS (T.unpack txt) = (S.singleton $ Right r, Nothing)
readRec (Done txt r) = recur deeper
  where deeper = readRec $ parse (whiteSpace False >> expr False) txt
        recur (x, y) = (Right r <| x, y)

readAsts :: String -> ParseResult
readAsts s | justWS s = Results S.empty s
readAsts s = foo $ readRec $ parse (whiteSpace False >> expr False) (T.pack $ s ++ "\n")
  where foo (vals, Nothing) = Results vals s
        foo (vals, Just cont) = Continuation vals s cont

-- difference between readStatic and readAsts is that readStatic is incremental,
-- and won't give back continuations. Consequently it just gobbles until it's
-- done. Passes back a list to avoid the strictness we get with Sequences.
readStatic :: B.Text -> [Either SwjError PVal]
readStatic s | lazyWS s = []
readStatic s = go $ L.parse (whiteSpace False >> expr False) s
  where go (L.Fail _ _ err) = [Left $ SyntaxError err]
        go (L.Done txt r) = Right r : readStatic txt

lazyWS :: B.Text -> Bool
lazyWS s = case L.parse (whiteSpace False) s of
            (L.Fail _ _ _) -> False
            (L.Done txt ()) -> B.null txt