dataframe-operations-2.6.0.0: src/DataFrame/Expression/Parse.hs
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
-- | Reload expressions from their 'Show' and 'prettyPrint' printed forms.
module DataFrame.Expression.Parse (
ColType (..),
Schema,
schemaOf,
parseShown,
parsePretty,
parseShownAs,
parsePrettyAs,
) where
import Data.Char (isAlphaNum, isDigit, isSpace)
import Data.List (isPrefixOf, sortBy)
import qualified Data.Map.Strict as M
import Data.Ord (Down (..), comparing)
import qualified Data.Text as T
import Data.Type.Equality (testEquality, (:~:) (Refl))
import DataFrame.Internal.Column (Columnable)
import DataFrame.Internal.DataFrame (DataFrame, columnNames)
import Type.Reflection (typeRep)
import qualified DataFrame.Functions as F
import DataFrame.Internal.Expression (Expr (..), SomeExpr (..), fromSomeExpr)
import DataFrame.Internal.Expression.Operators (
(.&&.),
(./=.),
(.<.),
(.<=.),
(.==.),
(.>.),
(.>=.),
(.||.),
)
import DataFrame.Internal.Expression.Reflect (
Dict (..),
fracDict,
integralDict,
numDict,
ordDict,
realDict,
)
import DataFrame.Operations.Core (columnAsVector)
-- | Column types the parser can build expressions over.
data ColType = TInt | TDouble | TText | TBool
deriving (Eq, Show)
type Schema = M.Map T.Text ColType
-- | Column types of a frame, for the columns whose type the parser supports.
schemaOf :: DataFrame -> Schema
schemaOf df = M.fromList [(c, t) | c <- columnNames df, Just t <- [typeOf c]]
where
typeOf c
| ok (F.col @Int c) = Just TInt
| ok (F.col @Double c) = Just TDouble
| ok (F.col @T.Text c) = Just TText
| ok (F.col @Bool c) = Just TBool
| otherwise = Nothing
ok :: forall a. (Columnable a) => Expr a -> Bool
ok e = either (const False) (const True) (columnAsVector e df)
-- | The untyped syntax both printers share.
data Ast
= ACol T.Text (Maybe ColType)
| -- | The value, and whether it was a Double (point or exponent).
ANum Double Bool
| AStr T.Text
| ABool Bool
| AIf Ast Ast Ast
| ACall T.Text [Ast]
deriving (Show)
-- | Two expressions of one type (the result of making operands agree).
data Same where
Same :: (Columnable t) => Expr t -> Expr t -> Same
typeName :: SomeExpr -> String
typeName (SomeExpr (_ :: Expr t)) = show (typeRep @t)
-- | Use evidence for a class on an expression's type, or name the operator.
need :: String -> Maybe (Dict c) -> ((c) => Either String r) -> Either String r
need what d k = maybe (Left (what ++ ": operand type not supported")) (\Dict -> k) d
elab :: Schema -> Ast -> Either String SomeExpr
elab schema ast = case ast of
ACol c (Just t) -> pure (colOf c t)
ACol c Nothing ->
maybe (Left ("unknown column " ++ show c)) (pure . colOf c) (M.lookup c schema)
ANum v True -> pure (SomeExpr (F.lit v))
ANum v False -> pure (SomeExpr (F.lit (round v :: Int)))
AStr s -> pure (SomeExpr (F.lit s))
ABool b -> pure (SomeExpr (F.lit b))
AIf c t e -> do
c' <- elab schema c
cb <-
maybe
(Left ("if: condition has type " ++ typeName c'))
pure
(fromSomeExpr @Bool c')
t' <- elab schema t
e' <- elab schema e
Same a b <- unify t' e'
pure (SomeExpr (F.ifThenElse cb a b))
ACall name args -> do
args' <- mapM (elab schema) args
call name args'
where
colOf c t = case t of
TInt -> SomeExpr (F.col @Int c)
TDouble -> SomeExpr (F.col @Double c)
TText -> SomeExpr (F.col @T.Text c)
TBool -> SomeExpr (F.col @Bool c)
-- | Make two operands the same type; a numeric literal adopts the other's.
unify :: SomeExpr -> SomeExpr -> Either String Same
unify a@(SomeExpr (x :: Expr p)) b@(SomeExpr (y :: Expr q))
| Just Refl <- testEquality (typeRep @p) (typeRep @q) = pure (Same x y)
| Just (Lit i) <- fromSomeExpr @Int a
, Just yd <- fromSomeExpr @Double b =
pure (Same (F.lit (fromIntegral i)) yd)
| Just xd <- fromSomeExpr @Double a
, Just (Lit i) <- fromSomeExpr @Int b =
pure (Same xd (F.lit (fromIntegral i)))
| Just (Lit d) <- fromSomeExpr @Double a
, Just yi <- fromSomeExpr @Int b
, whole d =
pure (Same (F.lit (round d)) yi)
| Just xi <- fromSomeExpr @Int a
, Just (Lit d) <- fromSomeExpr @Double b
, whole d =
pure (Same xi (F.lit (round d)))
| otherwise =
Left ("operand types differ: " ++ typeName a ++ " vs " ++ typeName b)
where
whole d = fromIntegral (round d :: Int) == (d :: Double)
call :: T.Text -> [SomeExpr] -> Either String SomeExpr
call name args = case (name, args) of
("toDouble", [SomeExpr (e :: Expr t)]) -> need "toDouble" (realDict @t) (pure (SomeExpr (F.toDouble e)))
("not", [a]) ->
maybe
(Left "not needs a Bool operand")
(pure . SomeExpr . F.not)
(fromSomeExpr @Bool a)
("negate", [SomeExpr (e :: Expr t)]) -> need "negate" (numDict @t) (pure (SomeExpr (negate e)))
("abs", [SomeExpr (e :: Expr t)]) -> need "abs" (numDict @t) (pure (SomeExpr (abs e)))
(_, [a, b]) -> do
Same x y <- unify a b
binary name x y
_ ->
Left
( "unsupported call "
++ T.unpack name
++ " with "
++ show (length args)
++ " arguments"
)
binary ::
forall t. (Columnable t) => T.Text -> Expr t -> Expr t -> Either String SomeExpr
binary name x y = case name of
"add" -> need "add" (numDict @t) (pure (SomeExpr (x + y)))
"sub" -> need "sub" (numDict @t) (pure (SomeExpr (x - y)))
"mult" -> need "mult" (numDict @t) (pure (SomeExpr (x * y)))
"divide" -> need "divide" (fracDict @t) (pure (SomeExpr (x / y)))
"div" -> need "div" (integralDict @t) (pure (SomeExpr (F.div x y)))
"mod" -> need "mod" (integralDict @t) (pure (SomeExpr (F.mod x y)))
"min" -> need "min" (ordDict @t) (pure (SomeExpr (F.min x y)))
"max" -> need "max" (ordDict @t) (pure (SomeExpr (F.max x y)))
"eq" -> pure (SomeExpr (x .==. y))
"neq" -> pure (SomeExpr (x ./=. y))
"lt" -> need "lt" (ordDict @t) (pure (SomeExpr (x .<. y)))
"gt" -> need "gt" (ordDict @t) (pure (SomeExpr (x .>. y)))
"leq" -> need "leq" (ordDict @t) (pure (SomeExpr (x .<=. y)))
"geq" -> need "geq" (ordDict @t) (pure (SomeExpr (x .>=. y)))
"and" -> bool (.&&.)
"or" -> bool (.||.)
_ -> Left ("unsupported operator " ++ T.unpack name)
where
bool f = case testEquality (typeRep @t) (typeRep @Bool) of
Just Refl -> pure (SomeExpr (f x y))
Nothing -> Left (T.unpack name ++ " needs Bool operands")
data SExp = SAtom String | SList [SExp]
deriving (Show)
sexp :: String -> Either String (SExp, String)
sexp s = case dropWhile isSpace s of
'(' : rest -> list rest []
'"' : _ -> case reads (dropWhile isSpace s) :: [(String, String)] of
[(str, rest)] -> pure (SAtom (show str), rest)
_ -> Left "bad string literal"
rest ->
let (tok, more) = span (\c -> not (isSpace c) && c /= '(' && c /= ')') rest
in if null tok
then Left ("unexpected input near " ++ take 20 rest)
else pure (SAtom tok, more)
where
list str acc = case dropWhile isSpace str of
')' : rest -> pure (SList (reverse acc), rest)
"" -> Left "unbalanced parentheses"
more -> do
(e, rest) <- sexp more
list rest (e : acc)
fromSExp :: SExp -> Either String Ast
fromSExp e = case e of
SList [SAtom "col", SAtom ('@' : ty), SAtom name] -> ACol <$> strLit name <*> (Just <$> colType ty)
SList [SAtom "col", SAtom "@Maybe", SAtom _, SAtom _] -> Left "nullable columns are not supported"
SList [SAtom "lit", inner] -> literal inner
SList [SAtom "ifThenElse", c, t, f] -> AIf <$> fromSExp c <*> fromSExp t <*> fromSExp f
SList (SAtom fn : args) -> ACall (T.pack fn) <$> mapM fromSExp args
SAtom a -> Left ("bare atom " ++ a)
SList [] -> Left "empty list"
SList (SList _ : _) -> Left "list head must be an atom"
where
strLit s = case reads s :: [(String, String)] of
[(v, "")] -> pure (T.pack v)
_ -> Left ("bad name " ++ s)
colType ty = case ty of
"Int" -> pure TInt
"Double" -> pure TDouble
"Text" -> pure TText
"Bool" -> pure TBool
_ -> Left ("unsupported column type " ++ ty)
-- (lit (3.5)), (lit ("x")); the inner parentheses come from show.
literal inner = case inner of
SList [SAtom v] -> atomLit v
SAtom v -> atomLit v
SList [SAtom "-", SAtom v] -> atomLit ('-' : v)
_ -> Left ("bad literal " ++ show inner)
atomLit v
| v == "True" = pure (ABool True)
| v == "False" = pure (ABool False)
| take 1 v == "\"" = AStr <$> strLit v
| otherwise = case reads v :: [(Double, String)] of
[(d, "")] -> pure (ANum d (any (`elem` (".eE" :: String)) v))
_ -> Left ("bad literal " ++ v)
-- | Parse the output of 'show' on an 'Expr'; columns carry their own types.
parseShown :: String -> Either String SomeExpr
parseShown s = do
(e, rest) <- sexp s
if all isSpace rest then pure () else Left ("trailing input: " ++ take 30 rest)
ast <- fromSExp e
elab M.empty ast
data Tok
= TNum Double Bool
| TStr T.Text
| TName T.Text
| TFun T.Text
| TOp String
| TLParen
| TRParen
| TComma
| TIf
| TElse
deriving (Eq, Show)
operators :: [String]
-- Dotted forms come from Operators, bare forms from Functions; same names.
operators =
sortBy
(comparing (Down . length))
[ ".<=."
, ".>=."
, ".==."
, "./=."
, ".&&."
, ".||."
, ".<."
, ".>."
, "<="
, ">="
, "=="
, "/="
, "<"
, ">"
, "//"
, "+"
, "-"
, "*"
, "/"
, "~"
]
functions :: [T.Text]
functions = ["toDouble", "min", "max", "mod", "abs", "negate", "not", "isJust"]
lexPretty :: Schema -> String -> Either String [Tok]
lexPretty schema = go
where
names = sortBy (comparing (Down . T.length)) (M.keys schema)
go [] = pure []
go s@(c : rest)
| isSpace c = go rest
| c == '(' = (TLParen :) <$> go rest
| c == ')' = (TRParen :) <$> go rest
| c == ',' = (TComma :) <$> go rest
| c == '"' = case reads s :: [(String, String)] of
[(str, more)] -> (TStr (T.pack str) :) <$> go more
_ -> Left "bad string literal"
| Just (n, more) <- keyword s = (n :) <$> go more
| Just (n, more) <- longest names s = (TName n :) <$> go more
| Just (f, more) <- longest functions s
, take 1 (dropWhile isSpace more) == "(" =
(TFun f :) <$> go more
| isDigit c = number s
-- A '-' touching a digit is a negative literal; binary '-' is spaced.
| c == '-', (d : _) <- rest, isDigit d = number s
| Just op <- firstPrefix operators s = (TOp op :) <$> go (drop (length op) s)
| otherwise = Left ("cannot lex near: " ++ take 30 s)
keyword s
| "else" `isPrefixOf` s && boundary (drop 4 s) = Just (TElse, drop 4 s)
| "if" `isPrefixOf` s && boundary (drop 2 s) = Just (TIf, drop 2 s)
| otherwise = Nothing
boundary more = case more of
[] -> True
(c : _) -> not (isAlphaNum c)
longest cands s = case [n | n <- cands, T.unpack n `isPrefixOf` s] of
(n : _) -> Just (n, drop (T.length n) s)
[] -> Nothing
firstPrefix cands s = case [op | op <- cands, op `isPrefixOf` s] of
(op : _) -> Just op
[] -> Nothing
number s0 =
let (sign, s) = case s0 of
'-' : more0 -> ("-", more0)
_ -> ("", s0)
(body0, more) =
span (\ch -> isDigit ch || ch == '.' || ch == 'e' || ch == 'E' || ch == '-') s
body = sign ++ body0
-- A trailing '-' belongs to the next token, not an exponent.
(num, more') =
if not (null body) && last body == '-'
then (init body, '-' : more)
else (body, more)
in case reads num :: [(Double, String)] of
[(d, "")] -> (TNum d (any (`elem` (".eE" :: String)) num) :) <$> go more'
_ -> Left ("bad number " ++ num)
-- Precedence climbing over the printer's precedences.
type P a = [Tok] -> Either String (a, [Tok])
parseExpr :: P Ast
parseExpr = parseOr
parseOr
, parseAnd
, parseCmp
, parseAdd
, parseMul
, parseUnary
, parsePrimary ::
P Ast
parseOr ts = do
(l, rest) <- parseAnd ts
case rest of
TOp ".||." : more -> do
(r, rest') <- parseOr more
pure (ACall "or" [l, r], rest')
_ -> pure (l, rest)
parseAnd ts = do
(l, rest) <- parseCmp ts
case rest of
TOp ".&&." : more -> do
(r, rest') <- parseAnd more
pure (ACall "and" [l, r], rest')
_ -> pure (l, rest)
parseCmp ts = do
(l, rest) <- parseAdd ts
case rest of
TOp op : more | Just name <- lookup op cmpNames -> do
(r, rest') <- parseAdd more
pure (ACall name [l, r], rest')
_ -> pure (l, rest)
where
cmpNames =
[ (".<=.", "leq")
, (".>=.", "geq")
, (".==.", "eq")
, ("./=.", "neq")
, (".<.", "lt")
, (".>.", "gt")
, ("<=", "leq")
, (">=", "geq")
, ("==", "eq")
, ("/=", "neq")
, ("<", "lt")
, (">", "gt")
]
parseAdd ts = parseMul ts >>= uncurry loop
where
loop l (TOp "+" : more) = parseMul more >>= \(r, rest) -> loop (ACall "add" [l, r]) rest
loop l (TOp "-" : more) = parseMul more >>= \(r, rest) -> loop (ACall "sub" [l, r]) rest
loop l rest = pure (l, rest)
parseMul ts = parseUnary ts >>= uncurry loop
where
loop l (TOp "*" : more) = parseUnary more >>= \(r, rest) -> loop (ACall "mult" [l, r]) rest
loop l (TOp "/" : more) = parseUnary more >>= \(r, rest) -> loop (ACall "divide" [l, r]) rest
loop l (TOp "//" : more) = parseUnary more >>= \(r, rest) -> loop (ACall "div" [l, r]) rest
loop l rest = pure (l, rest)
parseUnary (TOp "~" : more) = do
(e, rest) <- parseUnary more
pure (ACall "not" [e], rest)
parseUnary (TOp "-" : more) = do
(e, rest) <- parseUnary more
pure (ACall "negate" [e], rest)
parseUnary ts = parsePrimary ts
parsePrimary ts = case ts of
TNum v isD : rest -> pure (ANum v isD, rest)
TStr s : rest -> pure (AStr s, rest)
TName n : rest -> pure (ACol n Nothing, rest)
TFun f : TLParen : rest -> do
(args, rest') <- parseArgs rest []
pure (ACall f args, rest')
TLParen : rest -> do
(e, rest') <- parseExpr rest
case rest' of
TRParen : more -> pure (e, more)
_ -> Left "expected )"
TIf : rest -> parseIf rest
_ -> Left ("unexpected token " ++ show (take 3 ts))
where
parseArgs toks acc = do
(e, rest) <- parseExpr toks
case rest of
TComma : more -> parseArgs more (e : acc)
TRParen : more -> pure (reverse (e : acc), more)
_ -> Left "expected , or ) in call"
-- if C T else if C2 T2 else E
parseIf :: P Ast
parseIf ts = do
(c, rest) <- parseExpr ts
(t, rest') <- parseExpr rest
case rest' of
TElse : TIf : more -> do
(e, rest'') <- parseIf more
pure (AIf c t e, rest'')
TElse : more -> do
(e, rest'') <- parseExpr more
pure (AIf c t e, rest'')
_ -> Left "expected else"
-- | Parse the output of 'prettyPrint', given the frame's column types.
parsePretty :: Schema -> String -> Either String SomeExpr
parsePretty schema s = do
toks <- lexPretty schema s
(ast, rest) <- parseExpr toks
if null rest then pure () else Left ("trailing tokens: " ++ show (take 5 rest))
elab schema ast
parseShownAs :: forall a. (Columnable a) => String -> Either String (Expr a)
parseShownAs s = parseShown s >>= asType
parsePrettyAs ::
forall a. (Columnable a) => Schema -> String -> Either String (Expr a)
parsePrettyAs schema s = parsePretty schema s >>= asType
asType :: forall a. (Columnable a) => SomeExpr -> Either String (Expr a)
asType u =
maybe
(Left "expression has a different type from the one requested")
pure
(fromSomeExpr u)