module CompileToJS (showErr, jsModule) where import Ast import Control.Arrow (first) import Control.Monad (liftM,(<=<),join,ap) import Data.Char (isAlpha,isDigit) import Data.List (intercalate,sortBy,inits,foldl') import Data.Map (toList) import Data.Either (partitionEithers) import qualified Text.Pandoc as Pan import Initialize import Rename (derename) import Cases import Guid showErr :: String -> String showErr err = mainEquals $ "Elm.Graphics.text(Elm.Text.monospace(" ++ msg ++ "))" where msg = show . concatMap (++"
") . lines $ err indent = concatMap f where f '\n' = "\n " f c = [c] parens s = "(" ++ s ++ ")" braces s = "{" ++ s ++ "}" jsList ss = "["++ intercalate "," ss ++"]" jsFunc args body = "function(" ++ args ++ "){" ++ indent body ++ "}" assign x e = "\nvar " ++ x ++ "=" ++ e ++ ";" ret e = "\nreturn "++ e ++";" iff a b c = a ++ "?" ++ b ++ ":" ++ c mainEquals s = globalAssign "Elm.main" (jsFunc "" (ret s)) globalAssign m s = "\n" ++ m ++ "=" ++ s ++ ";" tryBlock escapees names e = concat [ "\n(function() {\ntry{\n" ++ e ++ "\n} catch (e) {" , "\nElm.main=function() {" , "\nvar msg = ('

Your browser may not be supported. " ++ "Are you using a modern browser?

' +" ++ " '
Runtime Error in " ++ intercalate "." names ++ " module:
' + e + '" ++ msg ++ "
');" , "\ndocument.body.innerHTML = Elm.Text.monospace(msg);" , "throw e;" , "};}}());" ] where msg | escapees /= [] = concat [ "

The problem may stem from an improper usage of:
" , intercalate ", " $ map (concatMap escape) escapees ] | otherwise = "" escape '\'' = "\\'" escape '"' = "\\\"" escape c = [c] jsModule (escapees, Module names exports imports stmts) = tryBlock escapees (tail modNames) $ concat [ assign "$op" "{}" , "\nfor(this['i'] in Elm){eval('var '+this['i']+'=Elm[this.i];');}" , concatMap (\n -> globalAssign n $ n ++ " || {}") . map (intercalate ".") . drop 2 . inits $ take (length modNames - 1) modNames , "\nif (" ++ modName ++ ") throw \"Module name collision, '" ++ intercalate "." (tail modNames) ++ "' is already defined.\"; " , globalAssign modName $ jsFunc "" (includes ++body++ export) ++ "()" , mainEquals $ modName ++ ".main" ] where modNames = if null names then ["Elm", "Main"] else "Elm" : names modName = intercalate "." modNames includes = concatMap jsImport $ map (first ("Elm."++)) imports body = stmtsToJS stmts export = getExports exports stmts exps = if null exports then ["main"] else exports getExports names stmts = ret . braces $ intercalate ",\n" (op : map fnPair fns) where exNames n = either id id n `elem` names exports | null names = concatMap get stmts | otherwise = filter exNames (concatMap get stmts) (fns,ops) = partitionEithers exports opPair op = "'" ++ op ++ "' : $op['" ++ op ++ "']" fnPair fn = let fn' = derename fn in fn' ++ ":" ++ fn op = ("$op : "++) . braces . intercalate ", " $ map opPair ops get' (FnDef x _ _) = Left x get' (OpDef op _ _ _) = Right op get s = case s of Definition d -> [ get' d ] Datatype _ _ tcs -> map (Left . fst) tcs ImportEvent _ _ x _ -> [ Left x ] ExportEvent _ _ _ -> [] jsImport (modul, how) = concat [ "\ntry{\n if (!(" ++ modul ++ " instanceof Object)) throw 'module not found';\n} catch(e) {\n throw (\"Module '" , drop 1 (dropWhile (/='.') modul) , "' is missing. Compile with --make flag or load missing " , "module in a separate JavaScript file.\");\n}" ] ++ jsImport' (modul, how) jsImport' (modul, As name) = assign name modul jsImport' (modul, Importing vs) = concatMap (\v -> assign v $ modul ++ "." ++ v) vs jsImport' (modul, Hiding vs) = concat [ assign "hiddenVars" . ("{"++) . (++"}") . intercalate "," $ map (\v -> v ++ ":true") vs , "\nfor (this['i'] in " ++ modul ++ ") " , braces . indent . concat $ [ "\nif (hiddenVars[this['i']]) continue;" , "\neval('var ' + this['i'] + ' = " , modul, "[this.i];');" ] ] stmtsToJS :: [Statement] -> String stmtsToJS stmts = run (concat `liftM` mapM toJS (sortBy cmpStmt stmts)) where cmpStmt s1 s2 = compare (valueOf s1) (valueOf s2) valueOf s = case s of Datatype _ _ _ -> 1 ImportEvent _ _ _ _ -> 3 Definition (FnDef _ [] _) -> 3 Definition (OpDef _ _ _ _) -> 3 Definition _ -> 4 ExportEvent _ _ _ -> 5 class ToJS a where toJS :: a -> GuidCounter String instance ToJS Def where toJS (FnDef x [] e) = assign x `liftM` toJS e toJS (FnDef f (a:as) e) = do body <- toJS (foldr Lambda e as) return $ concat ["\nfunction ",f,parens a, braces . indent $ ret body] toJS (OpDef op a1 a2 e) = do body <- toJS (foldr Lambda e [a1,a2]) return $ concat [ "\n$op['", op, "'] = ", body, ";" ] instance ToJS Statement where toJS (Definition d) = toJS d toJS (Datatype _ _ tcs) = concat `liftM` mapM (toJS . toDef) tcs where toDef (name,args) = Definition . FnDef name vars $ Data (derename name) (map Var vars) where vars = map (('a':) . show) [1..length args] toJS (ImportEvent js base elm _) = do v <- toJS base return $ concat [ "\nvar " ++ elm ++ "=Elm.Signal.constant(" ++ v ++ ");" , "\nValue.addListener(document, '" ++ js , "', function(e) { Dispatcher.notify(" ++ elm , ".id, e.value); });" ] toJS (ExportEvent js elm _) = return $ concat [ "\nlift(function(v) { " , "var e = document.createEvent('Event');" , "e.initEvent('", js, "', true, true);" , "e.value = v;" , "document.dispatchEvent(e); return v; })(", elm, ");" ] instance ToJS Expr where toJS expr = case expr of IntNum n -> return $ show n FloatNum n -> return $ show n Var x -> return $ x Chr c -> return $ show c Str s -> return $ "Value.str" ++ parens (show s) Boolean b -> return $ if b then "true" else "false" Range lo hi -> jsRange `liftM` toJS lo `ap` toJS hi Access e lbl -> (\s -> s ++ "." ++ lbl) `liftM` toJS e Binop op e1 e2 -> binop op `liftM` toJS e1 `ap` toJS e2 If eb et ef -> parens `liftM` (iff `liftM` toJS eb `ap` toJS et `ap` toJS ef) Lambda v e -> liftM (jsFunc v . ret) (toJS e) App (Var "toText") (Str s) -> return $ "toText" ++ parens (show s) App (Var "link") (Str s) -> return $ "link(" ++ show s ++ ")" App (Var "plainText") (Str s) -> return $ "plainText(" ++ show s ++ ")" App e1 e2 -> (++) `liftM` (toJS e1) `ap` (parens `liftM` toJS e2) Let defs e -> jsLet defs e Case e cases -> caseToJS e cases Data name es -> (\ss -> jsList $ show name : ss) `liftM` mapM toJS es Markdown doc -> return $ "text('" ++ pad ++ md ++ pad ++ "')" where md = formatMarkdown $ Pan.writeHtmlString Pan.defaultWriterOptions doc pad = "
 
" formatMarkdown = concatMap f where f '\'' = "\\'" f '\n' = "\\n" f '"' = "\"" f c = [c] jsLet defs e' = do body <- (++) `liftM` jsDefs defs `ap` (ret `liftM` toJS e') return $ jsFunc "" body ++ "()" jsDefs defs = concat `liftM` mapM toJS (sortBy f defs) where f a b = compare (valueOf a) (valueOf b) valueOf (FnDef _ args _) = min 1 (length args) valueOf (OpDef _ _ _ _) = 1 caseToJS e ps = do match <- caseToMatch ps e' <- toJS e (match',stmt) <- case (match,e) of (Match name _ _, Var x) -> return (matchSubst [(name,x)] match, "") (Match name _ _, _ ) -> return (match, assign name e') _ -> liftM (\n -> (match, e')) guid matches <- matchToJS match' return $ concat [ "function(){", stmt, matches, "}()" ] matchToJS (Match name clauses def) = do cases <- concat `liftM` mapM (clauseToJS name) clauses finally <- matchToJS def return $ concat [ "\nswitch(", name, "[0]){", indent cases, "\n}", finally ] matchToJS Fail = return "\nthrow \"Non-exhaustive pattern match in case\";" matchToJS Break = return "break;" matchToJS (Other e) = ret `liftM` toJS e matchToJS (Seq ms) = concat `liftM` mapM matchToJS ms clauseToJS var (Clause name vars e) = do let vars' = map (\n -> var ++ "[" ++ show n ++ "]") [ 1 .. length vars ] s <- matchToJS $ matchSubst (zip vars vars') e return $ concat [ "\ncase ", show name, ":", s ] jsNil = "[\"Nil\"]" jsCons e1 e2 = jsList [ show "Cons", e1, e2 ] jsRange e1 e2 = (++"()") . jsFunc "" $ assign "lo" e1 ++ assign "hi" e2 ++ assign "lst" jsNil ++ "if(lo<=hi){do{lst=" ++ (jsCons "hi" "lst") ++ "}while(hi-->lo)}" ++ ret "lst" binop (o:p) e1 e2 | isAlpha o || '_' == o = (o:p) ++ parens e1 ++ parens e2 | otherwise = let ops = ["+","-","*","/","&&","||"] in case o:p of ":" -> jsCons e1 e2 "++" -> append e1 e2 "$" -> e1 ++ parens e2 "." -> jsFunc "x" . ret $ e1 ++ parens (e2 ++ parens "x") "^" -> "Math.pow(" ++ e1 ++ "," ++ e2 ++ ")" "==" -> "eq(" ++ e1 ++ "," ++ e2 ++ ")" "/=" -> "not(eq(" ++ e1 ++ "," ++ e2 ++ "))" "<" -> "(compare(" ++ e1 ++ ")(" ++ e2 ++ ")[0] === 'LT')" ">" -> "(compare(" ++ e1 ++ ")(" ++ e2 ++ ")[0] === 'GT')" "<=" -> "function() { var ord = compare(" ++ e1 ++ ")(" ++ e2 ++ ")[0]; return ord==='LT' || ord==='EQ'; }()" ">=" -> "function() { var ord = compare(" ++ e1 ++ ")(" ++ e2 ++ ")[0]; return ord==='GT' || ord==='EQ'; }()" _ | elem (o:p) ops -> parens (e1 ++ (o:p) ++ e2) | otherwise -> concat [ "$op['", o:p, "']" , parens e1, parens e2 ] append e1 e2 = "Value.append" ++ parens (e1 ++ "," ++ e2)