atitit.本身動手開發編譯器and解釋器(2) ------語法分析,語義分析,代碼生成--attilax總結javascript
1. 創建AST 抽象語法樹 Abstract Syntax Tree,AST) 1前端
2. 創建AST 語法樹----遞歸降低(recursive descent)法 2java
3. 語法分析概念 2程序員
3.1. 上下文無關語言,非終結符(nonterminal symbol),終結符(terminal symbol)。注 2正則表達式
3.3. 分支預測的方法是超前查看 4express
3.5. ast錯誤報告 CPS(Continuation Pass-in Style)風格 。 5後端
4. ---code 6設計模式
5.2. 語義分析的第二個主要任務是找到全部標識符的定義。 9
5.2.1. 。因此咱們沒法只用一次抽象語法樹的遍從來完成語義分析。我採用的作法是分紅三次遍歷, 9
6. 下一個階段——代碼生成(設計模式---解釋器模式來實現。) 9
1.那麼什麼是抽象語法樹呢?其實就是通過簡化和抽象的語法分析樹。在完整的語法分析樹中每一個推導過程的終結符都包含在語法樹內,並且每一個非終結符都是不一樣的 節點類型。實際上,若是僅僅是要作編譯器的話,不少終結符(如關鍵字、各類標點符號)是無需出如今語法樹裏的;而前面表達式文法中的Factor、 Term也實際上沒有必要區分爲兩種不一樣的類型,能夠將其抽象爲BinaryExpression類型。這樣簡化、抽象以後的語法樹,更加利於後續語義分 析和代碼生成。使用.NET裏的面嚮對象語言來實現語法樹,最多見的作法就是用組合模式,將語法樹作成一顆對象樹,每種抽象語法對應一個節點類。下圖就是 miniSharp的抽象語法樹的全部類。
Attilax的總結是從上而下,先寫大框架組成法。。在在裏面的表達式裏面使用建設函數或者set函數注入類k...或者更好的辦法但基本思想是使用一個Stack,在進入一個新的做用域(大括號包圍的語句塊)時壓入一個新的HashSet,儲存這一做用域內聲明的變量。看成用域結束時彈出一個HashSet,這個做用域內的變量就從表裏刪除了
Attilax初次大概用了一天時間就解決了AST構建問題
做者:: 老哇的爪子 Attilax 艾龍, EMAIL:1466519819@qq.com
轉載請註明來源: http://blog.csdn.net/attilax
今天咱們就來討論實際編寫語法分析器的方法。今天介紹的這種方法叫作遞歸降低(recursive descent)法,這是一種適合手寫語法編譯器的方法,且很是簡單。遞歸降低法對語言所用的文法有一些限制,但遞歸降低是現階段主流的語法分析方法,因 爲它能夠由開發人員高度控制,在提供錯誤信息方面也頗有優點。就連微軟C#官方的編譯器也是手寫而成的遞歸降低語法分析器。
手寫的遞歸降低語法分析器能夠很容易地加入錯誤恢復,但須要針對每一處錯誤手工編寫代碼來恢復。像C#官方編譯器,給出的語法錯誤信息很是全面、精確、智能,全都是手工編寫的功勞
手寫遞歸降低的方式是目前不少編譯器採用的方式,若是你想寫一個商業質量的編譯器,這是首選的方
使用遞歸降低法編寫語法分析器無需任何類庫,編寫簡單的分析器時甚至連前面學習的詞法分析庫都無需使用
Attilax的總結是從上而下,先寫大框架組成法。。在在裏面的表達式裏面使用建設函數或者set函數注入類k...或者更好的辦法但基本思想是使用一個Stack,在進入一個新的做用域(大括號包圍的語句塊)時壓入一個新的HashSet,儲存這一做用域內聲明的變量。看成用域結束時彈出一個HashSet,這個做用域內的變量就從表裏刪除了
Attilax初次大概用了一天時間就解決了AST構建問題
語法分析。簡單而言,這一步就要完整地分析整個編程語言的語法結構。上回說到詞法分析的結果是將輸入的字符串分解成一個個的單詞流,也就是諸如關鍵字、標 識符這樣有特定意義的單詞。一種完整的編程語言,必須在此基礎上定義出各類聲明、語句和表達式的語法規則。觀察咱們所熟悉的編程語言,其語法大都有某種遞 歸的性質。例如四則運算與括號的表達式,其每一個運算符的兩邊,均可以是任意的表達式。好比1+a是表達式,
再好比if語句,其if的塊和else的塊中還能夠再嵌套if語句。咱們在詞法分析中引入的正則表達式和正則語言沒法描述這種結構,若是用DFA來解釋,DFA只有有限個狀態,它沒有辦法追溯這種無限遞歸。因此,編程語言的表達式,並非正則語言。咱們要引入一種表現能力更強的語言——上下文無關語言。
非終結符(nonterminal symbol),表明能夠繼續產生新符號的「文法變量」。 符號→表示非終結符能夠「產生」的東西。而上述產生式中的藍色id、+、(等符號,是具備固定意義的單詞,它們再也不會產生新的東西,稱做終結符(terminal symbol)。注
產生式通過一系列的推導,就可以生成各類徹底由終結符組成的句子。好比,咱們演示一下表達式(a + b) + c的推導過程:
E => E + E => (E) + E => (E + E) + E => (a + E) + E => (a + b) + E => (a + b) + c
推導過程當中的=>表明將當前句型中的一個非終結符替換成產生式右側的內容。以上推導過程當中,咱們每次都將句型中最左邊一個非終結符展開,因此這種推導稱爲最左推導。固然也有最右推導,不一樣之處就算是每次將句型中最右邊的非終結符展開:
可見,同一個結果能夠具備多種不一樣的推導過程。使用最左推導時,句型的左側逐漸變得只有終結符;而最右推導正好相反,推導過程當中句型的右側逐漸變得只有終結符,最終結果都是整個句子變爲終結符。全部符合文法定義的句子,均可以用文法的產生式推導出來
能夠看到最左推導和最右推導的語法分析樹是同樣的,這證實用相同的文法解析一樣的輸入也至少存在兩種不一樣的分析方法。後續篇章介紹的遞歸降低法就是一種最左推導的分析方法,而另外一類很是流行的LR分析器則是基於最右推導的分析方法。目前流行的編譯器開發方式是在語法分析階段構造一棵真正的語法分析樹,而後再經過遍歷語法樹的方法進行後續的分析,因此最左推導和最右推導的過程對咱們來說區別不大。
爲什麼這種語言和文法叫作「上下文無關」呢?其實這裏的「上下文無關」是指文法中的產生式均可以無條件展開爲箭頭右側的內容。另外存在一種上下文相關文法, 它的產生式都須要在必定條件下才能展開。上下文相關語言要比上下文無關文法複雜得多,而其沒有一種通用的方法能夠有效地解析上下文相關語言,所以它也不會 用在編程語言的設計當中。 也許已經意識到,即便是上下文無關文法和語言,也要比正則表達式和正則語言複雜得多。
到非終結符N有兩個產生式,因此在ParseNode方法的一開始咱們必須作出分支預測 。。分支預測的方法是超前查看(look ahead)。就是說咱們先「偷窺」當前位置前方的字符,而後判斷應該用哪一個產生式繼續分析
上面咱們採用的分支預測法是「人肉觀察法」,編譯原理書裏通常都有一些計算FIRST集合或FOLLOW集合的算法,能夠算出一個產生式可能開頭的字符, 這樣就能夠用自動的方法寫出分支預測,從而實現遞歸降低語法分析器的自動化生成。ANTLR就是用這種原理實現的一個著名工具。
其實我以爲「人肉觀察法」在實踐中並不困難,由於編程語言的文法都特別有規律,並且咱們每天用編程語言寫代碼,都頗有經驗了。
支持遞歸降低的文法,必須能經過從左往右超前查看k個字符決定採用哪個產生式。咱們把這樣的文法稱做LL(k)文法。這個名字中第一個L表示從左往右掃描字符串,這一點能夠從咱們的index變量從0開始遞增的特性看出來;而第二個L表示最左推導,想必你們還記得上一篇介紹的最左推導的例子。你們能夠用調試器跟蹤一遍遞歸降低語法分析器的分析過程,就能很容易地感覺到它的確是最左推導的(老是先展開當前句型最左邊的非終結符)。最後括號中的k表示須要超前查看k個字符
來看LL(k)文法的第二個重要的限制——不支持左遞歸。所謂左遞歸,就是產生式產生的第一個符號有多是該產生式自己的非終結符。下面的文法是一個直截了當的左遞歸例子: ,若是在編寫E的 遞歸降低解析函數時,直接在函數的開頭遞歸調用本身,輸入字符串徹底沒有消耗,這種遞歸調用就會變成一種死循環。因此,左遞歸是必需要消除的文法結構。解 決的方法一般是將左遞歸轉化爲等價的右遞歸形式: 你們應該緊緊記住這個例子,這不只僅是個例子,更是解除大部分左遞歸的萬能公式!
LR(k)文法的語法分析器。LR表明從左到右掃描和最右推導。LR型的文法容許左遞歸和左公因式,可是並不能用於遞歸降低的語法分析器,而是要用移進-歸約型的語法分析器,或者叫自底向上的語法分析器來分析。我我的認爲LR型語法分析器的原理很是優雅和精妙
做爲編程語言的語法分析器,不能在遇到語法錯誤的時候簡單地返回null,那樣程序員就很難修復代碼中的語法錯誤。咱們須要的是準確報告語法錯誤的位置,更進一步,是程序中全部的語法錯誤,而不只僅是頭一個。後者要求解析器具備錯誤恢復的 能力,即在遇到語法錯誤以後,還能恢復到正常狀態繼續解析。錯誤恢復不只僅能夠用在檢測出全部的語法錯誤,還能夠在存在語法錯誤的時候仍然提供有意義的解 析結果,從而用於IDE的智能感知和重構等功能。手寫的遞歸降低語法分析器能夠很容易地加入錯誤恢復,但須要針對每一處錯誤手工編寫代碼來恢復。像C#官 方編譯器,給出的語法錯誤信息很是全面、精確、智能,全都是手工編寫的功勞。又回到咱們是懶人這個殘酷的事實,能不能在讓解析器組合子生成的解析器自動具 有錯誤恢復能力呢?
若是要對失敗的情形進行錯誤恢復,有兩種可行的選擇:一、僞裝要解析的Token存在,繼續解析(這種作法至關於在原位置插入了一個單詞);二、跳過不匹配的單詞,從新進行解析(這種作法至關於刪除了 一個單詞)。若是漏寫一個分號或者括號,插入型錯誤恢復就能有效地恢復錯誤,若是是多寫了一個關鍵字或標識符形成的錯誤,刪除型錯誤恢復就能有效地恢復。 但問題是,咱們怎麼能在組合子的代碼中判斷出哪一種錯誤恢復更有效呢?最優策略是讓兩種錯誤恢復的狀態都繼續解析到末尾,而後看哪一種恢復狀態總體語法錯誤最 少。可是,只要有一個字符解析失敗,就要分支成兩個完整解析,那麼錯誤一旦多起來,這個分支的龐大程度將使得錯誤恢復沒法進行..咱們可讓兩條分支都解析到底,而後挑錯誤較少的分支做爲正式解析結果。但同上所述,這種作法的分支多得難以置信,效率上決定咱們不能採用。
爲了不效率問題,咱們須要一種「廣度優先」的處理方案。在遇到錯誤時產生的「插入」和「刪除」兩條分支,要同時進行,但要一步一步地進行。這裏所謂的一 「步」,就是指AsParser組合子讀取一個詞素。咱們看到四種基本組合子中,只有AsParser組合子會用scanner來真正讀取詞素,其餘組合 子最終也是要調用到AsParser組合子來進行解析的。咱們讓兩個可能的分支都向前解析一步,而後看是否其中一條分支的結果比另一條更好。所謂更好, 就是一條分支沒有進一步遇到錯誤,而另一條分支遇到了錯誤。若是兩條分支都沒有遇到錯誤,或者都遇到了錯誤,咱們就再向前推動一步,直到某一步比另一 步更好爲止。Union組合子也能夠採用一樣的策略處理。這是一種貪心算法的策略,咱們所獲得的結果未必是語法錯誤最少的解析結果,但它的效率是能夠接受 的
那麼怎麼進行「廣度優先」推動呢?咱們上次引入的組合子,當前的組合子沒法知道下一個要運行的組合子是什麼,更沒法控制下一個組合子只向前解析一步。爲了達到目的,咱們要引入一種新的組合子函數原型,稱做CPS(Continuation Pass-in Style)風格的組合子。不知道你們有多少人據說過CPS,這在函數式編程界是一種廣爲應用的模式,在.NET世界裏其實也有采用。.NET 4.0引入的Task Parallel Library庫中的Task類,就是一個典型的CPS設計範例。
而若是採用CPS,則是把B傳遞給A,這時咱們稱B是A的continuation,或者future。
自行決定如何調用future。這裏最關鍵的思想是實現延遲調用future,從而實現「廣度優先」的單步解析效果。
這個類裏咱們定義了整個解析器最終的一個future——它產生令全部分支判斷中止的StopResult。這裏最關鍵的是利用 result.GetResult虛方法推動廣度優先的分支選取,而且收集這條路線上全部的語法錯誤。咱們全部的語法錯誤就只有兩種:「丟失某單詞」(採 用了插入方式錯誤恢復)和「發現了未預期的某單詞」(採用了刪除方式錯誤恢復)。
private void ini() throws CantFindRitBrack {
// 定義一個堆棧,安排運算的前後順序
Stack<AbstractExpression> stack = ctx.stack;
List<Token> tokenList = (List<Token>) fsmx.getTokenList();
// 運算
for (int i = 0; i < tokenList.size(); i++) {
Token tk = tokenList.get(i);
switch (tk.value) {
case "(": // comma exp
AnnoDeclaration annoDeclar = (AnnoDeclaration) stack.pop();
int nextRitBrackIdx = getnextRitBrackIdx(i, tokenList);
List sub = tokenList.subList(i + 1, nextRitBrackIdx);
annoDeclar.setAssList(sub, ctx);
stack.push(annoDeclar);
i = nextRitBrackIdx;
break;
default: // var in gonsi 公式中的變量
AbstractExpression left2 = new AnnoDeclaration(
tokenList.get(i).value);
stack.push(left2);
}
}
// 把運算結果拋出來
this.expression = stack.pop();
}
public void setAssList(List subTokenList, Context ctx) {
Stack<AbstractExpression> stack = new Stack<AbstractExpression>();
List<Token> tokenList = subTokenList;
for (int i = 0; i < tokenList.size(); i++) {
Token tk = tokenList.get(i);
switch (tk.value) {
case ",": // comma exp
AbstractExpression right = new Assignment(tokenList.get(++i).value,tokenList.get(++i).value,tokenList.get(++i).value);
this.assignments.add((Assignment) right);
break;
default: // var in gonsi 公式中的變量
AbstractExpression left2 =new Assignment(tokenList.get(i).value,tokenList.get(++i).value,tokenList.get(++i).value);
this.assignments.add((Assignment) left2) ;
//stack.push(left2);
}
}
//this.setAssList((List<Assignment>) stack.pop());
}
所謂編程語言語義,就是這段代碼實際的含義。
語義分析是編譯器前端最複雜的部分。由於這些編程語言的語義都很是複雜。語義分析不像以前詞法分析、語法分析那樣,有一些特定的工具來幫助。這一部分一般都是要純手工寫代碼來完成。
好像attilax這個階段能夠沒有,忽略。。
在語義分析中,類型檢查是貫穿始終的一個步驟。像miniSharp這樣的靜態類型語言,類型檢查一般要作到:
1. 斷定每個表達式的聲明類型
2. 斷定每個字段、形式參數、變量聲明的類型
3. 判斷每一次賦值、傳參數時,是否存在合法的隱式類型轉換
4. 判斷一元和二元運算符左右兩側的類型是否合法(好比+不就不能在bool和int之間進行)
5. 將全部要發生的隱式類型轉換明確化
標識符在miniSharp裏主要有:類名、字段名、方法名、參數名和本地變量名。遇到每一個名稱,咱們必須解析出標識符表示的類、方法或字段的定義。
前兩次分別對類的生命和成員的聲明進行解析並構建符號表(類型和成員),第三次再對方法體進行解析。這樣就能夠方便地處理不一樣順序定義的問題。總的來講,三次遍歷的任務是:
1. 第一遍:掃描全部class定義,檢查有無重名的狀況。
2. 第二遍:檢查類的基類是否存在,檢測是否循環繼承;檢查全部字段的類型以及是否重名;檢查全部方法參數和返回值的類型以及是否重複定義(簽名徹底一致的狀況)。
3. 第三遍:檢查全部方法體中語句和表達式的語義。
通過完善的語義分析,咱們就獲得了一個具備完整類型信息,而且沒有語義錯誤的AST
咱們使用設計模式---解釋器模式來實現。。解釋器模式大大簡化了語義分析的過程。。
attilax初次作解釋器/編譯器,也只須要一天時間就能夠實現。。
前一階段咱們完成了編譯器中的重要階段——語義分析。如今,程序中的每個變量和類型都有其正確的定義;每個表達式和語句的類型都是合法的;每一 處方法調用都選擇了正確的方法定義。如今即將進入下一個階段——代碼生成。代碼生成的最終目的,是生成能在目標機器上運行的機器碼,或者能夠和其餘庫連接 在一塊兒的可重定向對象。代碼生成,和這一階段的各個優化手段,統稱爲編譯器的後端。目前大部分編譯器,在代碼生成時,都傾向於先將前段解析的結果轉化成一 種中間表示,再將中間表示翻譯成最終的機器碼。好比Java語言會翻譯成JVM bytecode,C#語言會翻譯成CIL,再經由各自的虛擬機執行;IE9的javascript也會先翻譯成一種bytecode,再由解釋器執行或 者進行JIT翻譯;即便靜態編譯的語言如C++,也存在先翻譯成中間語言,再翻譯成最終機器碼的過程。中間表示也不必定非得是一種bytecode,咱們 在語法分析階段生成的抽象語法樹(AST)就是一種很經常使用的中間表示。.NET 3.5引入的Expression Tree正是採用AST做爲中間表示的動態語言運行庫。那爲何這種作法很是流行呢?由於翻譯中中間語言有以下好處:
1. 使用中間語言能夠良好地將編譯器的前端和後端拆分開,使得兩部分能夠相對獨立地進行。
2. 同一種中間語言能夠由多種不一樣的源語言編譯而來,而又能夠針對多種不一樣的目標機器生成代碼。CLR的CIL就是這一特色的典型表明。
3. 有許多優化能夠直接針對中間語言進行,這樣優化的結果就能夠應用到不一樣的目標平臺。
本身動手開發編譯器(九)CPS風格的解析器組合子 - 裝配腦殼 - 博客園.htm
本身動手開發編譯器(十一)語義分析 - 裝配腦殼 - 博客園.htm
Atitit. 解釋器模式框架選型 and應用場景attilax總結 oao - attilax的專欄 - 博客頻道 - CSDN.NET.htm
Atitit.註解and屬性解析(2)---------語法分析 生成AST attilax總結 java .net - attilax的專欄 - 博客頻道 - CSDN.NET.htm
Atitit. 構造ast 語法樹的總結attilax oao - attilax的專欄 - 博客頻道 - CSDN.NET.htm