EtaMOO-0.1.0.0: src/MOO/Unparser.hs
{-# LANGUAGE OverloadedStrings #-}
-- | Recovering MOO code from an abstract syntax tree for the @verb_code()@
-- built-in function
module MOO.Unparser ( unparse ) where
import Control.Monad.Reader
import Control.Monad.Writer
import Data.Char
import Data.Set (Set)
import Data.Text (Text)
import qualified Data.Set as S
import qualified Data.Text as T
import MOO.AST
import MOO.Types
import MOO.Parser (keywords)
type Unparser = ReaderT UnparserEnv (Writer Text)
data UnparserEnv = UnparserEnv {
fullyParenthesizing :: Bool
, indenting :: Bool
, indentation :: Text
}
initUnparserEnv = UnparserEnv {
fullyParenthesizing = False
, indenting = False
, indentation = ""
}
-- | @unparse@ /fully-paren/ /indent/ /program/ synthesizes the MOO code
-- corresponding to the given abstract syntax tree. If /fully-paren/ is true,
-- all expressions are fully parenthesized, even when unnecessary given the
-- circumstances of precedence. If /indent/ is true, the resulting MOO code
-- will be indented with spaces as appropriate to show the nesting structure
-- of statements.
--
-- The MOO code is returned as a single 'Text' value containing embedded
-- newline characters.
unparse :: Bool -> Bool -> Program -> Text
unparse fullyParen indent (Program stmts) =
execWriter $ runReaderT (tellStatements stmts) $ initUnparserEnv {
fullyParenthesizing = fullyParen
, indenting = indent
}
indent :: Unparser ()
indent = do
indenting <- asks indenting
when indenting $ tell =<< asks indentation
moreIndented :: Unparser a -> Unparser a
moreIndented = local $ \env -> env { indentation = " " <> indentation env }
tellStatements :: [Statement] -> Unparser ()
tellStatements = mapM_ tellStatement
tellStatement :: Statement -> Unparser ()
tellStatement stmt = case stmt of
Expression _ expr -> indent >> tellExpr expr >> tell ";\n"
If _ cond (Then thens) elseIfs (Else elses) -> do
indent >> tell "if (" >> tellExpr cond >> tell ")\n"
moreIndented $ tellStatements thens
mapM_ tellElseIf elseIfs
unless (null elses) $ do
indent >> tell "else\n"
moreIndented $ tellStatements elses
indent >> tell "endif\n"
where tellElseIf (ElseIf _ cond thens) = do
indent >> tell "elseif (" >> tellExpr cond >> tell ")\n"
moreIndented $ tellStatements thens
ForList _ var expr body -> tellBlock "for" (Just var) detail body
where detail = tell " in" >> detailExpr expr
ForRange _ var (start, end) body -> tellBlock "for" (Just var) detail body
where detail = tell " in [" >> tellExpr start >> tell ".." >>
tellExpr end >> tell "]\n"
While _ var expr body -> tellBlock "while" var (detailExpr expr) body
Fork _ var expr body -> tellBlock "fork" var (detailExpr expr) body
Break var -> indent >> tell "break" >> maybeTellVar var >> tell ";\n"
Continue var -> indent >> tell "continue" >> maybeTellVar var >> tell ";\n"
Return _ Nothing -> indent >> tell "return;\n"
Return _ (Just expr) -> indent >> tell "return " >>
tellExpr expr >> tell ";\n"
TryExcept body excepts -> do
indent >> tell "try\n"
moreIndented $ tellStatements body
mapM_ tellExcept excepts
indent >> tell "endtry\n"
where tellExcept (Except _ var codes handler) = do
indent >> tell "except" >> maybeTellVar var >> tell " ("
case codes of
ANY -> tell "ANY"
Codes args -> tell =<< unparseArgs args
tell ")\n"
moreIndented $ tellStatements handler
TryFinally body (Finally finally) -> do
indent >> tell "try\n"
moreIndented $ tellStatements body
indent >> tell "finally\n"
moreIndented $ tellStatements finally
indent >> tell "endtry\n"
tellBlock :: StrT -> Maybe Id -> Unparser () -> [Statement] -> Unparser ()
tellBlock name maybeVar detail body = do
indent >> tell name >> maybeTellVar maybeVar >> detail
moreIndented $ tellStatements body
indent >> tell "end" >> tell name >> tell "\n"
maybeTellVar :: Maybe Id -> Unparser ()
maybeTellVar Nothing = return ()
maybeTellVar (Just var) = tell " " >> tell var
detailExpr :: Expr -> Unparser ()
detailExpr expr = tell " (" >> tellExpr expr >> tell ")\n"
tellExpr :: Expr -> Unparser ()
tellExpr = tell <=< unparseExpr
unparseExpr :: Expr -> Unparser Text
unparseExpr expr = case expr of
Literal value -> return $ toLiteral value
List args -> do
args' <- unparseArgs args
return $ "{" <> args' <> "}"
Variable var -> return var
PropRef (Literal (Obj 0)) (Literal (Str name))
| isIdentifier name -> return $ "$" <> name
PropRef obj name -> do
obj' <- case obj of
Literal Int{} -> paren obj -- avoid digits followed by dot (-> float)
_ -> parenL expr obj
name' <- unparseNameExpr name
return $ obj' <> "." <> name'
Assign lhs rhs -> do
lhs' <- unparseExpr lhs
rhs' <- unparseExpr rhs
return $ lhs' <> " = " <> rhs'
ScatterAssign scats rhs -> do
scats' <- unparseScatter scats
rhs' <- unparseExpr rhs
return $ "{" <> scats' <> "} = " <> rhs'
VerbCall (Literal (Obj 0)) (Literal (Str name)) args
| isIdentifier name -> do args' <- unparseArgs args
return $ "$" <> name <> "(" <> args' <> ")"
VerbCall obj name args -> do
obj' <- parenL expr obj
name' <- unparseNameExpr name
args' <- unparseArgs args
return $ obj' <> ":" <> name' <> "(" <> args' <> ")"
BuiltinFunc func args -> do
args' <- unparseArgs args
return $ func <> "(" <> args' <> ")"
Index lhs rhs -> do
lhs' <- parenL expr lhs
rhs' <- unparseExpr rhs
return $ lhs' <> "[" <> rhs' <> "]"
Range lhs (from, to) -> do
lhs' <- parenL expr lhs
from' <- unparseExpr from
to' <- unparseExpr to
return $ lhs' <> "[" <> from' <> ".." <> to' <> "]"
Length -> return "$"
-- Left-associative operators
In lhs rhs -> binaryL lhs " in " rhs
Plus lhs rhs -> binaryL lhs " + " rhs
Minus lhs rhs -> binaryL lhs " - " rhs
Times lhs rhs -> binaryL lhs " * " rhs
Divide lhs rhs -> binaryL lhs " / " rhs
Remain lhs rhs -> binaryL lhs " % " rhs
And lhs rhs -> binaryL lhs " && " rhs
Or lhs rhs -> binaryL lhs " || " rhs
Equal lhs rhs -> binaryL lhs " == " rhs
NotEqual lhs rhs -> binaryL lhs " != " rhs
LessThan lhs rhs -> binaryL lhs " < " rhs
LessEqual lhs rhs -> binaryL lhs " <= " rhs
GreaterThan lhs rhs -> binaryL lhs " > " rhs
GreaterEqual lhs rhs -> binaryL lhs " >= " rhs
-- Right-associative operators
Power lhs rhs -> binaryR lhs " ^ " rhs
Negate lhs@(Literal x) | numeric x -> negateParen lhs
Negate lhs@(Literal x `Index` _) | numeric x -> negateParen lhs
Negate lhs@(Literal x `Range` _) | numeric x -> negateParen lhs
Negate lhs@(Literal Flt{} `PropRef` _) -> negateParen lhs
Negate lhs@(VerbCall (Literal x) _ _) | numeric x -> negateParen lhs
Negate lhs -> ("-" <>) `liftM` parenL expr lhs
Not lhs -> ("!" <>) `liftM` parenL expr lhs
Conditional cond lhs rhs -> do
cond' <- parenR expr cond
lhs' <- unparseExpr lhs
rhs' <- parenR expr rhs
return $ cond' <> " ? " <> lhs' <> " | " <> rhs'
Catch lhs codes (Default dv) -> do
lhs' <- unparseExpr lhs
codes' <- case codes of
ANY -> return "ANY"
Codes args -> unparseArgs args
case dv of
Nothing -> return $ "`" <> lhs' <> " ! " <> codes' <> "'"
Just expr -> do
expr' <- unparseExpr expr
return $ "`" <> lhs' <> " ! " <> codes' <> " => " <> expr' <> "'"
where binaryL lhs op rhs = do
lhs' <- parenL expr lhs
rhs' <- parenR expr rhs
return $ lhs' <> op <> rhs'
binaryR lhs op rhs = do
lhs' <- parenR expr lhs
rhs' <- parenL expr rhs
return $ lhs' <> op <> rhs'
numeric :: Value -> Bool
numeric Int{} = True
numeric Flt{} = True
numeric _ = False
negateParen = liftM ("-" <>) . paren
unparseArgs :: [Arg] -> Unparser Text
unparseArgs = liftM (T.intercalate ", ") . mapM unparseArg
where unparseArg (ArgNormal expr) = unparseExpr expr
unparseArg (ArgSplice expr) = ("@" <>) `liftM` unparseExpr expr
unparseScatter :: [ScatItem] -> Unparser Text
unparseScatter = liftM (T.intercalate ", ") . mapM unparseScat
where unparseScat (ScatRequired var) = return var
unparseScat (ScatRest var) = return $ "@" <> var
unparseScat (ScatOptional var Nothing) = return $ "?" <> var
unparseScat (ScatOptional var (Just expr)) = do
expr' <- unparseExpr expr
return $ "?" <> var <> " = " <> expr'
unparseNameExpr :: Expr -> Unparser Text
unparseNameExpr (Literal (Str name))
| isIdentifier name = return name
unparseNameExpr expr = do
expr' <- unparseExpr expr
return $ "(" <> expr' <> ")"
paren :: Expr -> Unparser Text
paren expr = do
expr' <- unparseExpr expr
return $ "(" <> expr' <> ")"
mightParen :: (Int -> Int -> Bool) -> Expr -> Expr -> Unparser Text
mightParen cmp parent child = do
fullyParenthesizing <- asks fullyParenthesizing
if (fullyParenthesizing && precedence child < precedence PropRef{}) ||
(precedence parent `cmp` precedence child)
then paren child
else unparseExpr child
parenL :: Expr -> Expr -> Unparser Text
parenL = mightParen (>)
parenR :: Expr -> Expr -> Unparser Text
parenR = mightParen (>=)
isIdentifier :: StrT -> Bool
isIdentifier name = isIdentifier' (T.unpack name) && not (isKeyword name)
where isIdentifier' (c:cs) = isIdentStart c && all isIdentChar cs
isIdentStart c = isAlpha c || c == '_'
isIdentChar c = isAlphaNum c || c == '_'
isKeyword :: StrT -> Bool
isKeyword = (`S.member` keywordsSet) . T.toCaseFold
keywordsSet :: Set StrT
keywordsSet = S.fromList $ map (T.toCaseFold . T.pack) keywords