axel-0.0.13: src/Axel/Parse.hs
module Axel.Parse where
import Axel.Prelude
import Axel.Eff.Error (Error(ParseError))
import Axel.Haskell.Language
( haskellKeywords
, haskellOperatorSymbols
, haskellSyntaxSymbols
, isOperator
)
import Axel.Parse.AST
( Expression(LiteralChar, LiteralFloat, LiteralInt, LiteralString,
SExpression, Symbol)
, bottomUpFmapSplicing
, getAnn
)
import qualified Axel.Sourcemap as SM (Expression)
import Axel.Sourcemap
( Position(Position, _column, _line)
, SourceMetadata
, quoteSourceMetadata
, wrapCompoundExpressions
)
import Axel.Utils.List (remove, unsafeHead)
import qualified Axel.Utils.Text as T (remove)
import Control.Applicative ((<|>))
import Control.Lens (op)
import Control.Monad (void)
import Data.List ((\\), foldl')
import Data.Maybe (fromMaybe)
import Data.MonoTraversable (onotElem)
import qualified Data.Text as T
import Data.Void (Void)
import Effectful ((:>))
import qualified Effectful as Eff
import qualified Effectful.Error.Static as Eff
import qualified Text.Megaparsec as P
import qualified Text.Megaparsec.Char as P
import qualified Text.Megaparsec.Char.Lexer as P
( charLiteral
, decimal
, float
, signed
)
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 = void P.eol <|> P.eof
ignored :: Parser ()
ignored =
P.skipMany $ P.try singleLineComment <|> multiLineComment <|> void P.spaceChar
literalChar :: Parser SM.Expression
literalChar = ann LiteralChar (P.string "#\\" *> P.charLiteral)
literalInt :: Parser SM.Expression
literalInt = ann LiteralInt (P.signed mempty P.decimal)
literalFloat :: Parser SM.Expression
literalFloat = ann LiteralFloat (P.signed mempty P.float)
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 $
hygenisizeIdentifier <$>
P.some
(P.alphaNumChar <|> P.oneOf ['\'', '_'] <|>
P.oneOf (map fst haskellSyntaxSymbols \\ syntaxSymbols) <|>
P.oneOf (map fst haskellOperatorSymbols))
unquotedExpression :: Parser SM.Expression
unquotedExpression = parseReadMacro "~" "unquote"
singleLineComment :: Parser ()
singleLineComment =
P.try (P.string "--" *> eol) <|>
void (P.string "-- " *> P.manyTill (void P.anySingle) eol)
multiLineComment :: Parser ()
multiLineComment =
void $ P.try (P.string "{-") *> P.manyTill (void P.anySingle) (P.string "-}")
expression :: Parser SM.Expression
expression =
P.try literalFloat <|> P.try literalInt <|> sExpression <|> infixSExpression <|>
literalList <|>
literalString <|>
quotedExpression <|>
quasiquotedExpression <|>
spliceUnquotedExpression <|>
unquotedExpression <|>
literalChar <|>
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' "AxelRuntime_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' "AxelRuntime_AST.toExpressionList", expr]
expandQuasiquoteInList expr =
let ann' = getAnn expr
in SExpression ann' [Symbol ann' "list", expandQuasiquote expr]
parseMultiple' ::
(Eff.Error Error :> effs)
=> ([SM.Expression] -> [SM.Expression])
-> Maybe FilePath
-> Text
-> Eff.Eff effs [SM.Expression]
parseMultiple' postProcess maybeFilePath input =
either throwErr (pure . postProcess) $
P.parse program (T.unpack $ op FilePath filePath) input
where
filePath = fromMaybe (FilePath "") maybeFilePath
program = P.some (ignored *> expression <* ignored) <* P.eof
throwErr =
Eff.throwError . ParseError filePath . T.pack . P.errorBundlePretty
parseMultiple ::
(Eff.Error Error :> effs)
=> Maybe FilePath
-> Text
-> Eff.Eff effs [SM.Expression]
parseMultiple = parseMultiple' expandQuasiquotes
where
expandQuasiquotes =
map $
bottomUpFmapSplicing
(\case
SExpression _ (Symbol _ "quasiquote":xs') -> map expandQuasiquote xs'
x -> [x])
parseSource ::
(Eff.Error Error :> effs)
=> Maybe FilePath
-> Text
-> Eff.Eff effs SM.Expression
parseSource filePath input =
wrapCompoundExpressions <$> parseMultiple filePath input
syntaxSymbols :: String
syntaxSymbols = "()[]{}\""
charReplacements ::
[(Char, String)] -> [(Char, String)] -> String -> [(Char, String)]
charReplacements syntaxCharReplacements operatorCharReplacements x =
let targetCharReplacements =
syntaxCharReplacements <>
if isOperator x
then []
else operatorCharReplacements
in allowQualifiedNames $
filter (\(sym, _) -> sym `onotElem` syntaxSymbols) targetCharReplacements
where
allowQualifiedNames = remove $ (== '.') . fst
valueHygienePrefix :: Text
valueHygienePrefix = "aXEL_VALUE_"
hygenisizeIdentifier :: String -> String
hygenisizeIdentifier x
| [(_, new)] <- filter ((x ==) . fst) haskellKeywords = new
| otherwise =
let prefix =
if unsafeHead x `elem` map fst replacements
then valueHygienePrefix
else ""
in T.unpack $
prefix <>
foldl'
(\acc (old, new) -> T.replace (T.singleton old) (T.pack new) acc)
(T.pack x)
replacements
where
replacements =
charReplacements haskellSyntaxSymbols haskellOperatorSymbols x
unhygenisizeIdentifier :: String -> String
unhygenisizeIdentifier x
| [(old, _)] <- filter ((x ==) . snd) haskellKeywords = old
| otherwise -- There _could_ be multiple matches, but that would be VERY, VERY BAD™.
=
T.unpack $
T.remove valueHygienePrefix $ -- The prefix isn't always there, in which case this just no-ops.
foldl'
(\acc (old, new) -> T.replace (T.pack new) (T.singleton old) acc)
(T.pack x) $
charReplacements haskellSyntaxSymbols haskellOperatorSymbols x