掃二維碼與項目經(jīng)理溝通
我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流
我們將會在本篇文章中看到從零開始實現(xiàn)的編譯器,將簡單的類 LISP 計算語言編譯成 JavaScript。完整的源代碼在 這里。

我們將會:
開始吧!
Lisp 族語言最迷人的地方在于,它們的語法就是樹狀表示的,這就是這門語言很容易解析的原因。我們很快就能接觸到它。但首先讓我們把自己的語言定義好。關于我們語言的語法的范式(BNF)描述如下:
program ::= exprexpr ::=| | ([ ])
基本上,我們可以在該語言的最頂層定義表達式并對其進行運算。表達式由一個整數(shù)(比如 5)、一個變量(比如 x)或者一個表達式列表(比如 (add x 1))組成。
整數(shù)對應它本身的值,變量對應它在當前環(huán)境中綁定的值,表達式列表對應一個函數(shù)調(diào)用,該列表的第一個參數(shù)是相應的函數(shù),剩下的表達式是傳遞給這個函數(shù)的參數(shù)。
該語言中,我們保留一些內(nèi)建的特殊形式,這樣我們就能做一些更有意思的事情:
let 表達式使我們可以在它的 body 環(huán)境中引入新的變量。語法如下:
let ::= (let ([]) ) letargs ::= () body ::=
lambda 表達式:也就是匿名函數(shù)定義。語法如下:
lambda ::= (lambda ([]) )
還有一些內(nèi)建函數(shù): add、mul、sub、div 和 print。
讓我們看看用我們這門語言編寫的入門示例程序:
(let((compose(lambda (f g)(lambda (x) (f (g x)))))(square(lambda (x) (mul x x)))(add1(lambda (x) (add x 1))))(print ((compose square add1) 5)))
這個程序定義了 3 個函數(shù):compose、square 和 add1。然后將計算結果的值 ((compose square add1) 5) 輸出出來。
我相信了解這門語言,這些信息就足夠了。開始實現(xiàn)它吧。
在 Haskell 中,我們可以這樣定義語言:
type Name = Stringdata Expr= ATOM Atom| LIST [Expr]deriving (Eq, Read, Show)data Atom= Int Int| Symbol Namederiving (Eq, Read, Show)
我們可以解析用該語言用 Expr 定義的程序。而且,這里我們添加了新數(shù)據(jù)類型 Eq、Read 和 Show 等實例用于測試和調(diào)試。你能夠在 REPL 中使用這些數(shù)據(jù)類型,驗證它們確實有用。
我們不在語法中定義 lambda、let 或其它的內(nèi)建函數(shù),原因在于,當前情況下我們沒必要用到這些東西。這些函數(shù)僅僅是 LIST (表達式列表)的更加特殊的用例。所以我決定將它放到后面的部分。
一般來說你想要在抽象語法中定義這些特殊用例 —— 用于改進錯誤信息、禁用靜態(tài)分析和優(yōu)化等等,但在這里我們不會這樣做,對我們來說這些已經(jīng)足夠了。
另一件你想做的事情可能是在語法中添加一些注釋信息。比如定位:Expr 是來自哪個文件的,具體到這個文件的哪一行哪一列。你可以在后面的階段中使用這一特性,打印出錯誤定位,即使它們不是處于解析階段。
Program 數(shù)據(jù)類型,可以按順序包含多個 Expr我們要做的第一件事情是定義一個嵌入式領域專用語言Embedded Domain Specific Language(EDSL),我們會用它來定義我們的語言解析器。這常常被稱為解析器組合庫。我們做這件事完全是出于學習的目的,Haskell 里有很好的解析庫,在實際構建軟件或者進行實驗時,你應該使用它們。megaparsec 就是這樣的一個庫。
首先我們來談談解析庫的實現(xiàn)的思路。本質(zhì)上,我們的解析器就是一個函數(shù),接受一些輸入,可能會讀取輸入的一些或全部內(nèi)容,然后返回解析出來的值和無法解析的輸入部分,或者在解析失敗時拋出異常。我們把它寫出來。
newtype Parser a= Parser (ParseString -> Either ParseError (a, ParseString))data ParseString= ParseString Name (Int, Int) Stringdata ParseError= ParseError ParseString Errortype Error = String
這里我們定義了三個主要的新類型。
第一個,Parser a 是之前討論的解析函數(shù)。
第二個,ParseString 是我們的輸入或攜帶的狀態(tài)。它有三個重要的部分:
Name: 這是源的名字(Int, Int): 這是源的當前位置String: 這是等待解析的字符串第三個,ParseError 包含了解析器的當前狀態(tài)和一個錯誤信息。
現(xiàn)在我們想讓這個解析器更靈活,我們將會定義一些常用類型的實例。這些實例讓我們能夠將小巧的解析器和復雜的解析器結合在一起(因此它的名字叫做 “解析器組合器”)。
第一個是 Functor 實例。我們需要 Functor 實例,因為我們要能夠對解析值應用函數(shù)從而使用不同的解析器。當我們定義自己語言的解析器時,我們將會看到關于它的示例。
instance Functor Parser wherefmap f (Parser parser) =Parser (\str -> first f <$> parser str)
第二個是 Applicative 實例。該實例的常見用例是在多個解析器中實現(xiàn)一個純函數(shù)。
instance Applicative Parser wherepure x = Parser (\str -> Right (x, str))(Parser p1) <*> (Parser p2) =Parser $\str -> do(f, rest) <- p1 str(x, rest') <- p2 restpure (f x, rest')
(注意:我們還會實現(xiàn)一個 Monad 實例,這樣我們才能使用符號)
第三個是 Alternative 實例。萬一前面的解析器解析失敗了,我們要能夠提供一個備用的解析器。
instance Alternative Parser whereempty = Parser (`throwErr` "Failed consuming input")(Parser p1) <|> (Parser p2) =Parser $\pstr -> case p1 pstr ofRight result -> Right resultLeft _ -> p2 pstr
第四個是 Monad 實例。這樣我們就能鏈接解析器。
instance Monad Parser where(Parser p1) >>= f =Parser $\str -> case p1 str ofLeft err -> Left errRight (rs, rest) ->case f rs ofParser parser -> parser rest
接下來,讓我們定義一種的方式,用于運行解析器和防止失敗的助手函數(shù):
runParser :: String -> String -> Parser a -> Either ParseError (a, ParseString)runParser name str (Parser parser) = parser $ ParseString name (0,0) strthrowErr :: ParseString -> String -> Either ParseError athrowErr ps@(ParseString name (row,col) _) errMsg =Left $ ParseError ps $ unlines[ "*** " ++ name ++ ": " ++ errMsg, "* On row " ++ show row ++ ", column " ++ show col ++ "."]
現(xiàn)在我們將會開始實現(xiàn)組合器,這是 EDSL 的 API,也是它的核心。
首先,我們會定義 oneOf。如果輸入列表中的字符后面還有字符的話,oneOf 將會成功,否則就會失敗。
oneOf :: [Char] -> Parser CharoneOf chars =Parser $ \caseps@(ParseString name (row, col) str) ->case str of[] -> throwErr ps "Cannot read character of empty string"(c:cs) ->if c `elem` charsthen Right (c, ParseString name (row, col+1) cs)else throwErr ps $ unlines ["Unexpected character " ++ [c], "Expecting one of: " ++ show chars]
optional 將會拋出異常,停止解析器。失敗時它僅僅會返回 Nothing。
optional :: Parser a -> Parser (Maybe a)optional (Parser parser) =Parser $\pstr -> case parser pstr ofLeft _ -> Right (Nothing, pstr)Right (x, rest) -> Right (Just x, rest)
many 將會試著重復運行解析器,直到失敗。當它完成的時候,會返回成功運行的解析器列表。many1 做的事情是一樣的,但解析失敗時它至少會拋出一次異常。
many :: Parser a -> Parser [a]many parser = go []where go cs = (parser >>= \c -> go (c:cs)) <|> pure (reverse cs)many1 :: Parser a -> Parser [a]many1 parser =(:) <$> parser <*> many parser
下面的這些解析器通過我們定義的組合器來實現(xiàn)一些特殊的解析器:
char :: Char -> Parser Charchar c = oneOf [c]string :: String -> Parser Stringstring = traverse charspace :: Parser Charspace = oneOf " \n"spaces :: Parser Stringspaces = many spacespaces1 :: Parser Stringspaces1 = many1 spacewithSpaces :: Parser a -> Parser awithSpaces parser =spaces *> parser <* spacesparens :: Parser a -> Parser aparens parser =(withSpaces $ char '(')*> withSpaces parser<* (spaces *> char ')')sepBy :: Parser a -> Parser b -> Parser [b]sepBy sep parser = dofrst <- optional parserrest <- many (sep *> parser)pure $ maybe rest (:rest) frst
現(xiàn)在為該門語言定義解析器所需要的所有東西都有了。
我們會用自頂而下的方法定義解析器。
parseExpr :: Parser ExprparseExpr = fmap ATOM parseAtom <|> fmap LIST parseListparseList :: Parser [Expr]parseList = parens $ sepBy spaces1 parseExprparseAtom :: Parser AtomparseAtom = parseSymbol <|> parseIntparseSymbol :: Parser AtomparseSymbol = fmap Symbol parseName
注意到這四個函數(shù)是在我們這門語言中屬于高階描述。這解釋了為什么 Haskell 執(zhí)行解析工作這么棒。在定義完高級部分后,我們還需要定義低級別的 parseName 和 parseInt。
我們能在這門語言中用什么字符作為名字呢?用小寫的字母、數(shù)字和下劃線吧,而且名字的第一個字符必須是字母。
parseName :: Parser NameparseName = doc <- oneOf ['a'..'z']cs <- many $ oneOf $ ['a'..'z'] ++ "0123456789" ++ "_"pure (c:cs)
整數(shù)是一系列數(shù)字,數(shù)字前面可能有負號 -:
parseInt :: Parser AtomparseInt = dosign <- optional $ char '-'num <- many1 $ oneOf "0123456789"let result = read $ maybe num (:num) sign ofpure $ Int result
最后,我們會定義用來運行解析器的函數(shù),返回值可能是一個 Expr 或者是一條錯誤信息。
runExprParser :: Name -> String -> Either String ExprrunExprParser name str =case runParser name str (withSpaces parseExpr) ofLeft (ParseError _ errMsg) -> Left errMsgRight (result, _) -> Right result
Program 類型編寫一個解析器parseNameparseInt 可能出現(xiàn)溢出情況,找到處理它的方法,不要用 read。我們還想做一件事,將我們的程序以源代碼的形式打印出來。這對完善錯誤信息很有用。
printExpr :: Expr -> StringprintExpr = printExpr' False 0printAtom :: Atom -> StringprintAtom = \caseSymbol s -> sInt i -> show iprintExpr' :: Bool -> Int -> Expr -> StringprintExpr' doindent level = \caseATOM a -> indent (bool 0 level doindent) (printAtom a)LIST (e:es) ->indent (bool 0 level doindent) $concat[ "(", printExpr' False (level + 1) e, bool "\n" "" (null es), intercalate "\n" $ map (printExpr' True (level + 1)) es, ")"]indent :: Int -> String -> Stringindent tabs e = concat (replicate tabs " ") ++ e
Program 類型編寫一個美觀的輸出器好,目前為止我們寫了近 200 行代碼,這些代碼一般叫做編譯器的前端。我們還要寫大概 150 行代碼,用來執(zhí)行三個額外的任務:我們需要根據(jù)需求定義一個 JS 的子集,定義一個將我們的語言轉譯成這個子集的轉譯器,最后把所有東西整合在一起。開始吧。
首先,我們要定義將要使用的 JavaScript 的子集:
data JSExpr= JSInt Int| JSSymbol Name| JSBinOp JSBinOp JSExpr JSExpr| JSLambda [Name] JSExpr| JSFunCall JSExpr [JSExpr]| JSReturn JSExprderiving (Eq, Show, Read)type JSBinOp = String
這個數(shù)據(jù)類型表示 JavaScript 表達式。我們有兩個原子類型 JSInt 和 JSSymbol,它們是由我們這個語言中的 Atom 轉譯來的,我們用 JSBinOp 來表示二元操作,比如 + 或 *,用 JSLambda 來表示匿名函數(shù),和我們語言中的 lambda expression(lambda 表達式) 一樣,我們將會用 JSFunCall 來調(diào)用函數(shù),用 let 來引入新名字,用 JSReturn 從函數(shù)中返回值,在 JavaScript 中是需要返回值的。
JSExpr 類型是對 JavaScript 表達式的 抽象表示。我們會把自己語言中表達式的抽象表示 Expr 轉譯成 JavaScript 表達式的抽象表示 JSExpr。但為了實現(xiàn)這個功能,我們需要實現(xiàn) JSExpr ,并從這個抽象表示中生成 JavaScript 代碼。我們將通過遞歸匹配 JSExpr 實現(xiàn),將 JS 代碼當作 String 來輸出。這和我們在 printExpr 中做的基本上是一樣的。我們還會追蹤元素的作用域,這樣我們才可以用合適的方式縮進生成的代碼。
printJSOp :: JSBinOp -> StringprintJSOp op = opprintJSExpr :: Bool -> Int -> JSExpr -> StringprintJSExpr doindent tabs = \caseJSInt i -> show iJSSymbol name -> nameJSLambda vars expr -> (if doindent then indent tabs else id) $ unlines["function(" ++ intercalate ", " vars ++ ") {",indent (tabs+1) $ printJSExpr False (tabs+1) expr] ++ indent tabs "}"JSBinOp op e1 e2 -> "(" ++ printJSExpr False tabs e1 ++ " " ++ printJSOp op ++ " " ++ printJSExpr False tabs e2 ++ ")"JSFunCall f exprs -> "(" ++ printJSExpr False tabs f ++ ")(" ++ intercalate ", " (fmap (printJSExpr False tabs) exprs) ++ ")"JSReturn expr -> (if doindent then indent tabs else id) $ "return " ++ printJSExpr False tabs expr ++ ";"
JSProgram 類型,它可以包含多個 JSExpr ,然后創(chuàng)建一個叫做 printJSExprProgram 的函數(shù)來生成代碼。JSExpr 的新類型:JSIf,并為其生成代碼。我們快做完了。這一節(jié)將會創(chuàng)建函數(shù),將 Expr 轉譯成 JSExpr。
基本思想很簡單,我們會將 ATOM 轉譯成 JSSymbol 或者 JSInt,然后會將 LIST 轉譯成一個函數(shù)調(diào)用或者轉譯的特例。
type TransError = StringtranslateToJS :: Expr -> Either TransError JSExprtranslateToJS = \caseATOM (Symbol s) -> pure $ JSSymbol sATOM (Int i) -> pure $ JSInt iLIST xs -> translateList xstranslateList :: [Expr] -> Either TransError JSExprtranslateList = \case[] -> Left "translating empty list"ATOM (Symbol s):xs| Just f <- lookup s builtins ->f xsf:xs ->JSFunCall <$> translateToJS f <*> traverse translateToJS xs
builtins 是一系列要轉譯的特例,就像 lambada 和 let。每一種情況都可以獲得一系列參數(shù),驗證它是否合乎語法規(guī)范,然后將其轉譯成等效的 JSExpr。
type Builtin = [Expr] -> Either TransError JSExprtype Builtins = [(Name, Builtin)]builtins :: Builtinsbuiltins =[("lambda", transLambda),("let", transLet),("add", transBinOp "add" "+"),("mul", transBinOp "mul" "*"),("sub", transBinOp "sub" "-"),("div", transBinOp "div" "/"),("print", transPrint)]
我們這種情況,會將內(nèi)建的特殊形式當作特殊的、非第一類的進行對待,因此不可能將它們當作第一類函數(shù)。
我們會把 Lambda 表達式轉譯成一個匿名函數(shù):
transLambda :: [Expr] -> Either TransError JSExprtransLambda = \case[LIST vars, body] -> dovars' <- traverse fromSymbol varsJSLambda vars' <$> (JSReturn <$> translateToJS body)vars ->Left $ unlines["Syntax error: unexpected arguments for lambda.","expecting 2 arguments, the first is the list of vars and the second is the body of the lambda.","In expression: " ++ show (LIST $ ATOM (Symbol "lambda") : vars)]fromSymbol :: Expr -> Either String NamefromSymbol (ATOM (Symbol s)) = Right sfromSymbol e = Left $ "cannot bind value to non symbol type: " ++ show e
我們會將 let 轉譯成帶有相關名字參數(shù)的函數(shù)定義,然后帶上參數(shù)調(diào)用函數(shù),因此會在這一作用域中引入變量:
transLet :: [Expr] -> Either TransError JSExprtransLet = \case[LIST binds, body] -> do(vars, vals) <- letParams bindsvars' <- traverse fromSymbol varsJSFunCall . JSLambda vars' <$> (JSReturn <$> translateToJS body) <*> traverse translateToJS valswhereletParams :: [Expr] -> Either Error ([Expr],[Expr])letParams = \case[] -> pure ([],[])LIST [x,y] : rest -> ((x:) *** (y:)) <$> letParams restx : _ -> Left ("Unexpected argument in let list in expression:\n" ++ printExpr x)vars ->Left $ unlines["Syntax error: unexpected arguments for let.","expecting 2 arguments, the first is the list of var/val pairs and the second is the let body.","In expression:\n" ++ printExpr (LIST $ ATOM (Symbol "let") : vars)]
我們會將可以在多個參數(shù)之間執(zhí)行的操作符轉譯成一系列二元操作符。比如:(add 1 2 3) 將會變成 1 + (2 + 3)。
transBinOp :: Name -> Name -> [Expr] -> Either TransError JSExprtransBinOp f _ [] = Left $ "Syntax error: '" ++ f ++ "' expected at least 1 argument, got: 0"transBinOp _ _ [x] = translateToJS xtransBinOp _ f list = foldl1 (JSBinOp f) <$> traverse translateToJS list
然后我們會將 print 轉換成對 console.log 的調(diào)用。
transPrint :: [Expr] -> Either TransError JSExprtransPrint [expr] = JSFunCall (JSSymbol "console.log") . (:[]) <$> translateToJS exprtransPrint xs = Left $ "Syntax error. print expected 1 arguments, got: " ++ show (length xs)
注意,如果我們將這些代碼當作 Expr 的特例進行解析,那我們就可能會跳過語法驗證。
Program 轉譯成 JSProgramif Expr Expr Expr 添加一個特例,并將它轉譯成你在上一次練習中實現(xiàn)的 JSIf 條件語句。最終,我們將會把所有東西整合到一起。我們會:
ExprJSExpr我們還會啟用一些用于測試的標志位:
--e 將進行解析并打印出表達式的抽象表示(Expr)--pp 將進行解析,美化輸出--jse 將進行解析、轉譯、并打印出生成的 JS 表達式(JSExpr)的抽象表示--ppc 將進行解析,美化輸出并進行編譯
main :: IO ()main = getArgs >>= \case[file] ->printCompile =<< readFile file["--e",file] ->either putStrLn print . runExprParser "--e" =<< readFile file["--pp",file] ->either putStrLn (putStrLn . printExpr) . runExprParser "--pp" =<< readFile file["--jse",file] ->either print (either putStrLn print . translateToJS) . runExprParser "--jse" =<< readFile file["--ppc",file] ->either putStrLn (either putStrLn putStrLn) . fmap (compile . printExpr) . runExprParser "--ppc" =<< readFile file_ ->putStrLn $ unlines["Usage: runghc Main.hs [ --e, --pp, --jse, --ppc ]" ,"--e print the Expr","--pp pretty print Expr","--jse print the JSExpr","--ppc pretty print Expr and then compile"]printCompile :: String -> IO ()printCompile = either putStrLn putStrLn . compilecompile :: String -> Either Error Stringcompile str = printJSExpr False 0 <$> (translateToJS =<< runExprParser "compile" str)
大功告成。將自己的語言編譯到 JS 子集的編譯器已經(jīng)完成了。再說一次,你可以在 這里 看到完整的源文件。
用我們的編譯器運行第一節(jié)的示例,產(chǎn)生的 JavaScript 代碼如下:
$ runhaskell Lisp.hs example.lsp(function(compose, square, add1) {return (console.log)(((compose)(square, add1))(5));})(function(f, g) {return function(x) {return (f)((g)(x));};}, function(x) {return (x * x);}, function(x) {return (x + 1);})
如果你在自己電腦上安裝了 node.js,你可以用以下命令運行這段代碼:
$ runhaskell Lisp.hs example.lsp | node -p36undefined

我們在微信上24小時期待你的聲音
解答本文疑問/技術咨詢/運營咨詢/技術建議/互聯(lián)網(wǎng)交流