packages feed

axel-0.0.11: src/Axel/Parse.hs

{-# LANGUAGE GADTs #-}

module Axel.Parse where

import Axel.Prelude

import Axel.Eff.Error (Error(ParseError))
import Axel.Haskell.Language (haskellOperatorSymbols, haskellSyntaxSymbols)
import Axel.Parse.AST
  ( Expression(LiteralChar, LiteralInt, LiteralString, SExpression,
           Symbol)
  , bottomUpFmapSplicing
  , getAnn
  )
import qualified Axel.Sourcemap as SM (Expression)
import Axel.Sourcemap
  ( Position(Position, _column, _line)
  , SourceMetadata
  , quoteSourceMetadata
  , wrapCompoundExpressions
  )

import Control.Applicative ((<|>))
import Control.Lens (op)
import Control.Monad (void)

import Data.List ((\\))
import Data.Maybe (fromMaybe)
import qualified Data.Text as T
import Data.Void (Void)

import qualified Polysemy as Sem
import qualified Polysemy.Error as Sem

import qualified Text.Megaparsec as P
import qualified Text.Megaparsec.Char as P
import qualified Text.Megaparsec.Char.Lexer as P (charLiteral)

type Parser = P.Parsec Void Text

-- Adapted from https://hackage.haskell.org/package/megaparsec-7.0.5/docs/Text-Megaparsec-Char-Lexer.html#v:charLiteral.
stringLiteral :: Parser Text
stringLiteral = T.pack <$> (P.char '"' *> P.manyTill P.charLiteral (P.char '"'))

ann :: (SourceMetadata -> a -> b) -> Parser a -> Parser b
ann f x = do
  parsecPosition <- P.getSourcePos
  let sourcePosition =
        ( P.sourceName parsecPosition
        , Position
            { _line = P.unPos $ P.sourceLine parsecPosition
            , _column = P.unPos $ P.sourceColumn parsecPosition
            })
  f (Just sourcePosition) <$> x

parseReadMacro :: Text -> Text -> Parser SM.Expression
parseReadMacro prefix wrapper = do
  expr <- P.string prefix *> expression
  ann SExpression (pure [Symbol Nothing (T.unpack wrapper), expr])

eol :: Parser ()
eol = P.try (void P.eol) <|> P.eof

ignored :: Parser ()
ignored = P.skipMany $ P.try comment <|> void P.spaceChar

literalChar :: Parser SM.Expression
literalChar = ann LiteralChar (P.string "#\\" *> P.anySingle)

literalInt :: Parser SM.Expression
literalInt = ann LiteralInt (read <$> P.some P.digitChar)

literalList :: Parser SM.Expression
literalList =
  ann
    SExpression
    ((Symbol Nothing "list" :) <$>
     (P.char '[' *> P.many sExpressionItem <* P.char ']'))

literalString :: Parser SM.Expression
literalString = ann LiteralString (T.unpack <$> stringLiteral)

quasiquotedExpression :: Parser SM.Expression
quasiquotedExpression = parseReadMacro "`" "quasiquote"

quotedExpression :: Parser SM.Expression
quotedExpression = parseReadMacro "'" "quote"

sExpressionItem :: Parser SM.Expression
sExpressionItem = ignored *> expression <* ignored

sExpression :: Parser SM.Expression
sExpression =
  ann SExpression (P.char '(' *> P.many sExpressionItem <* P.char ')')

infixSExpression :: Parser SM.Expression
infixSExpression =
  ann
    SExpression
    ((Symbol Nothing "applyInfix" :) <$>
     (P.char '{' *> P.many sExpressionItem <* P.char '}'))

spliceUnquotedExpression :: Parser SM.Expression
spliceUnquotedExpression = parseReadMacro "~@" "unquoteSplicing"

symbol :: Parser SM.Expression
symbol =
  ann
    Symbol
    (P.some
       (P.try P.alphaNumChar <|> P.try (P.oneOf ['\'', '_']) <|>
        P.try (P.oneOf (map fst haskellSyntaxSymbols \\ syntaxSymbols)) <|>
        P.oneOf (map fst haskellOperatorSymbols)))

unquotedExpression :: Parser SM.Expression
unquotedExpression = parseReadMacro "~" "unquote"

comment :: Parser ()
comment =
  P.try (P.string "--" *> eol) <|>
  void (P.string "-- " *> P.manyTill (void P.anySingle) eol)

expression :: Parser SM.Expression
expression =
  P.try literalChar <|> P.try literalInt <|> P.try literalList <|>
  P.try literalString <|>
  P.try quotedExpression <|>
  P.try quasiquotedExpression <|>
  P.try spliceUnquotedExpression <|>
  P.try unquotedExpression <|>
  P.try sExpression <|>
  P.try infixSExpression <|>
  symbol

-- Adapted from Appendix A of "Quasiquotation in Lisp" by Alan Bawden.
expandQuasiquote :: SM.Expression -> SM.Expression
expandQuasiquote (SExpression _ [Symbol _ "unquote", expr]) = expr
expandQuasiquote (SExpression _ [Symbol _ "unquoteSplicing", _]) =
  error
    "Illegal splicing unquote at the top level of a quasiquote! (`~@foo is not allowed, but `(~@foo) is.)"
expandQuasiquote (SExpression ann' xs) =
  SExpression
    ann'
    [ Symbol ann' "AST.SExpression"
    , quoteSourceMetadata ann'
    , SExpression
        ann'
        [ Symbol ann' "concat"
        , SExpression ann' (Symbol ann' "list" : map expandQuasiquoteInList xs)
        ]
    ]
expandQuasiquote expr =
  let ann' = getAnn expr
   in SExpression ann' [Symbol ann' "quote", expr]

expandQuasiquoteInList :: SM.Expression -> SM.Expression
expandQuasiquoteInList (SExpression _ [Symbol _ "unquoteSplicing", expr]) =
  let ann' = getAnn expr
   in SExpression ann' [Symbol ann' "AST.toExpressionList", expr]
expandQuasiquoteInList expr =
  let ann' = getAnn expr
   in SExpression ann' [Symbol ann' "list", expandQuasiquote expr]

parseMultiple ::
     (Sem.Member (Sem.Error Error) effs)
  => Maybe FilePath
  -> Text
  -> Sem.Sem effs [SM.Expression]
parseMultiple maybeFilePath input =
  either throwErr (pure . expandQuasiquotes) $
  P.parse program (T.unpack $ op FilePath filePath) input
  where
    filePath = fromMaybe (FilePath "") maybeFilePath
    program = P.some (ignored *> expression <* ignored) <* P.eof
    throwErr = Sem.throw . ParseError filePath . T.pack . P.errorBundlePretty
    expandQuasiquotes =
      map $
      bottomUpFmapSplicing
        (\case
           SExpression _ (Symbol _ "quasiquote":xs') -> map expandQuasiquote xs'
           x -> [x])

parseSource ::
     (Sem.Member (Sem.Error Error) effs)
  => Maybe FilePath
  -> Text
  -> Sem.Sem effs SM.Expression
parseSource filePath input =
  wrapCompoundExpressions <$> parseMultiple filePath input

syntaxSymbols :: String
syntaxSymbols = "()[]{}"