sunroof-compiler-0.2: Language/Sunroof/JavaScript.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE FlexibleContexts #-}
-- TODO: Remove as soon as the pretty printing stuff is actually used.
{-# OPTIONS_GHC -fno-warn-unused-binds #-}
-- | Basic low-level types and their combinators.
-- These are used as output of the compiler.
-- Everything here is untypes and not supposed for public use!
module Language.Sunroof.JavaScript
( Expr, ExprE(..), E(..)
, Id, Stmt(..), Type(..)
, Rhs(..)
, showExpr, showStmt
, operator, binOp, uniOp
, literal
, scopeForEffect
) where
import Data.List ( intercalate )
import Data.Reify ( MuRef(..) )
import Control.Applicative ( Applicative, pure, (<$>), (<*>))
import Data.Traversable ( Traversable(..) )
import Data.Foldable ( Foldable(..) )
import Data.Monoid ( Monoid(..) )
import Data.Char ( isAlpha, isAlphaNum )
-- -------------------------------------------------------------
-- Javascript Expressions
-- -------------------------------------------------------------
-- | Javascript identifier.
type Id = String
-- | Short name for instantiated expressions.
type Expr = E ExprE
-- | Instantiated expressions.
data ExprE = ExprE Expr deriving Show
-- | Plain expressions in Javascript.
data E expr = Lit String -- ^ A precompiled (atomic) Javascript literal.
| Var Id -- ^ A variable.
| Dot expr expr Type -- ^ Field/attribute access (with type information): @expr . expr :: Type@
| Apply expr [expr] -- ^ Function application: @expr ( expr, ..., expr )@
| Function [Id] [Stmt] -- ^ Anonymous function with parameter names and body.
deriving Show
instance MuRef ExprE where
type DeRef ExprE = E
mapDeRef f (ExprE e) = traverse f e
instance Traversable E where
traverse _ (Lit s) = pure (Lit s)
traverse _ (Var s) = pure (Var s)
traverse f (Dot o n a) = Dot <$> f o <*> f n <*> pure a
traverse f (Apply s xs) = Apply <$> f s <*> traverse f xs
traverse _ (Function nms stmts) = pure (Function nms stmts)
instance Foldable E where
foldMap _ (Lit _) = mempty
foldMap _ (Var _) = mempty
foldMap f (Dot o n _) = f o `mappend` f n
foldMap f (Apply o xs) = f o `mappend` foldMap f xs
foldMap _ (Function _nms _stmts) = mempty
instance Functor E where
fmap _ (Lit s) = Lit s
fmap _ (Var s) = Var s
fmap f (Dot o n a) = Dot (f o) (f n) a
fmap f (Apply s xs) = Apply (f s) (map f xs)
fmap _ (Function nms stmts) = Function nms stmts
-- | Show an expression as compiled Javascript.
-- The boolean argument says non-trivial arguments need parenthesis.
showExpr :: Bool -> Expr -> String
-- Original comment: These being up here, cause a GHC warning for missing patterns.
-- So they are moved down.
-- Response: They *need* to be here, it makes a different. I've fixed the warning.
showExpr _ (Lit a) = a -- always stand alone, or pre-parenthesised
showExpr _ (Var v) = v -- always stand alone
showExpr b e = p $ case e of
-- (Apply (ExprE (Var "[]")) [ExprE a,ExprE x]) -> showExpr True a ++ "[" ++ showExpr False x ++ "]"
(Apply (ExprE (Var "?:")) [ExprE a,ExprE x,ExprE y]) -> showExpr True a ++ "?" ++ showExpr True x ++ ":" ++ showExpr True y
(Apply (ExprE (Var op)) [ExprE x,ExprE y]) | not (any isAlpha op) -> showExpr True x ++ op ++ showExpr True y
(Apply (ExprE (Var "!")) [ExprE ex]) -> "!" ++ showExpr True ex
-- We have a constructor call:
(Apply (ExprE (Lit op)) args) | isNewConstructor op -> op ++ showArgs args
(Apply (ExprE fn) args) -> showFun fn args
(Dot (ExprE a) (ExprE x) Base) -> showIdx a x
-- This is a shortcomming in Javascript, where grabbing a indirected function
-- throws away the context (self/this). So we force storage of the context, using a closure.
(Dot (ExprE a) (ExprE x) (Fun xs _)) ->
"function(" ++ intercalate "," args ++ ") { return (" ++
showIdx a x ++ ")(" ++ intercalate "," args ++ "); }"
where args = [ "a" ++ show i | i <- take (length xs) ([0..] :: [Int])]
-- This pattern was missing too.
(Dot (ExprE _a) (ExprE _x) Unit) ->
error "Dot pattern on unit type. Don't know what to do."
(Function args body) ->
"function" ++
"(" ++ intercalate "," args ++ ") {\n" ++
indent 2 (unlines (map showStmt body)) ++
"}"
_ -> error "should never happen"
where
p txt = if b then "(" ++ txt ++ ")" else txt
-- | @showIdx o a@ accesses the field/attribute @a@ of the object @o@.
showIdx :: Expr -> Expr -> String
showIdx a (Lit x) | Just n <- isGoodSelectName x
= showExpr True a ++ "." ++ n
showIdx a ix = showExpr True a ++ "[" ++ showExpr False ix ++ "]"
-- | @showArgs a@ creates a string representing the given expressions
-- in an argument list that can be used for functions or constructors.
showArgs :: [ExprE] -> String
showArgs args = "(" ++ intercalate "," (map (\ (ExprE e') -> showExpr False e') args) ++ ")"
-- | Show a function application,
showFun :: Expr -> [ExprE] -> String
showFun e args = case e of
(Dot (ExprE a) (ExprE (Lit x)) _)
| Just n <- isGoodSelectName x -> showExpr True a ++ "." ++ n ++ showArgs args
(Dot (ExprE a) (ExprE x) _) -> "(" ++ showIdx a x ++ ")" ++ showArgs args
_ -> showExpr True e ++ showArgs args
-- | Check if the given 'Id' is a valid Javascript identifier.
isIdentifier :: Id -> Bool
isIdentifier x | not (null x) = isAlpha (head x) && all isAlphaNum (drop 1 x)
isIdentifier _ = False
-- | Check if the given 'Id' represents a constructor call without
-- arguments. That means a string beginning with @"new "@ followed
-- by a valid identifier ('isIdentifier').
isNewConstructor :: Id -> Bool
isNewConstructor x = take 4 x == "new " && isIdentifier (drop 4 x)
-- | Check if the given name is a field/attribute seletor that
-- can be printed without quotes using the dot-notation.
isGoodSelectName :: Id -> Maybe Id
isGoodSelectName xs
| length xs < 2 = Nothing
| head xs == '"' &&
last xs == '"' &&
all isAlpha xs' = return xs'
| otherwise = Nothing
where
xs' = tail (init xs)
-- -------------------------------------------------------------
-- Helper Combinators
-- -------------------------------------------------------------
-- | Combinator to create a operator/function applied to the given arguments.
operator :: Id -> [Expr] -> Expr
operator n ps = Apply (ExprE $ Var n) (fmap ExprE ps)
-- | Short-hand to create the applied binary operator/function.
-- See 'operator'.
binOp :: String -> Expr -> Expr -> E ExprE
binOp o e1 e2 = operator o [e1, e2]
-- | Short-hand to create the applied unary operator/function.
-- See 'operator'.
uniOp :: String -> Expr -> E ExprE
uniOp o e = operator o [e]
-- | Combinator to create a expression containing a
-- literal in form of a string.
literal :: String -> Expr
literal = Lit
-- | Indent all lines of the given string by the given number
-- of spaces.
indent :: Int -> String -> String
indent n = unlines . map (take n (cycle " ") ++) . lines
-- | Create a anonymous function to scope all effects
-- in the given block of statement.
scopeForEffect :: [Stmt] -> Expr
scopeForEffect stmts = Apply (ExprE $ Function [] stmts) []
-- -------------------------------------------------------------
-- Javascript References
-- -------------------------------------------------------------
-- | A Right hand side of an assignment.
data Rhs = VarRhs Id -- ^ A variable
| DotRhs Expr Expr -- ^ a named field
showRhs :: Rhs -> String
showRhs (VarRhs var) = "var " ++ var
showRhs (DotRhs e1 e2) = showIdx e1 e2
-- -------------------------------------------------------------
-- Javascript Statements
-- -------------------------------------------------------------
-- TODO: remove VarStmt, replace with AssignStmt and ExprStmt.
-- TODO: add type to return stmt; should not return "null"
-- | Plain Javascript statements.
data Stmt = AssignStmt Rhs Expr -- ^ Restricted assignment: @Rhs = Expr;@
| DeleteStmt Expr -- ^ Delete reference @delete Rhs;@
| ExprStmt Expr -- ^ Expression statement, for the sake of its side effects: @Expr;@
| ReturnStmt Expr -- ^ Return statement: @return Expr;@
| IfStmt Expr [Stmt] [Stmt] -- ^ If-Then-Else statement: @if (Expr) { Stmts } else { Stmts }@
| WhileStmt Expr [Stmt] -- ^ While loop: @while (Expr) { Stmts }@
| CommentStmt String -- ^ A comment in the code: @// String@
instance Show Stmt where
show = showStmt
-- | Translate a statement into actual Javascript.
showStmt :: Stmt -> String
showStmt (AssignStmt e1 e2) = showRhs e1 ++ " = " ++ showExpr False e2 ++ ";"
showStmt (DeleteStmt e) = "delete " ++ showExpr False e ++ ";"
showStmt (ExprStmt e) = showExpr False e ++ ";"
showStmt (ReturnStmt e) = "return " ++ showExpr False e ++ ";"
showStmt (IfStmt i t e) = "if(" ++ showExpr False i ++ "){\n"
++ indent 2 (unlines (map showStmt t))
++ "} else {\n"
++ indent 2 (unlines (map showStmt e))
++ "}"
showStmt (WhileStmt b stmts) = "while(" ++ showExpr False b ++ "){\n"
++ indent 2 (unlines (map showStmt stmts))
++ "}"
showStmt (CommentStmt msg) = "/* " ++ msg ++ " */"
-- -------------------------------------------------------------
-- Javascript Types
-- -------------------------------------------------------------
-- | Abstract types for Javascript expressions in Sunroof.
data Type = Base -- ^ Base type like object or other primtive types.
| Unit -- ^ Unit or void type. There is a effect but no value.
| Fun [Type] Type -- ^ Function type: @(t_1,..,t_n) -> t@
deriving (Eq,Ord)
instance Show Type where
show Base = "*"
show Unit = "()"
show (Fun xs t) = show xs ++ " -> " ++ show t
-- -------------------------------------------------------------
-- Pretty Printer
-- -------------------------------------------------------------
data Doc = Text String -- plain text (assume no newlines)
| Indent Int Doc -- indent document by n
| Sep [Doc] -- on seperate lines
text :: String -> Doc
text = Text
--indent :: Int -> Doc -> Doc
--indent = Indent
sep :: [Doc] -> Doc
sep = Sep
pretty :: Doc -> String
pretty (Text txt) = txt
pretty (Sep docs) = unlines $ map pretty docs
pretty (Indent n doc) = unlines $ map (take n (cycle " ") ++) $ lines $ pretty doc