# 課程資訊 禮拜一非同步教學 禮拜三面授 評分: 程式作業: 50% 小考: 3% 手寫作業: 12% 期中考: 15% 期末考: 15% 點名: 5% # introduction * 編譯器廣義來說是一種轉換工具;包含高階語言跟組合語言之間的轉換、不同系統執行檔間的轉換 (binary translation) 等等 * 設計編譯器時需要考慮以下重點 * 翻的內容須保證正確 * 翻出來的程式執行效率高 (翻出來的程式要能充分利用硬體資源) * 翻譯時間必須在合理時間內 * compiler vs interpreter * compiler: 先翻譯,再執行 * interpreter: 邊翻譯,邊執行 * static time vs dymanic time * static time: 編譯時間 * dymanic time: 執行時間 ## compiler 架構 ![image](https://hackmd.io/_uploads/SJQjYV73T.png =80%x) ### 前端 #### Lexical Analyzer * 目的是將程式語言的 statement 轉換成 token ![image](https://hackmd.io/_uploads/HyMbIVX3p.png) #### Syntax Analyzer * 分析程式語法,並建構 syntax tree ![image](https://hackmd.io/_uploads/BkGPv47ha.png =50%x) * 中間節點: 運算子 * 中間節點的子節點: 運算元 #### Semantic Analyzer * 語意分析,確認程式是否符合邏輯 ex: type checking 會把 a[-100] 判定成錯誤 ### 中間層 #### Intermsdiate Code Generator * 將 syntax tree 轉成 * 中間表示式可以視為組合語言的 psudo code ![image](https://hackmd.io/_uploads/r1qbKNmnp.png =40%x) ### 後端 #### Code Optomizer * 在不改變邏輯的情況下,進一步優化中間表示式 ![image](https://hackmd.io/_uploads/HJ1ut47np.png =40%x) #### Code Generator * 轉成真正的組合語言輸出 ![image](https://hackmd.io/_uploads/Sy20tNm3T.png =40%x) # Lexical Analyzsis * 在這個步驟中,會透過 Lexical Analyzer (又稱為 scanner) 將 statement 切成 token ## 工作原理 ![image](https://hackmd.io/_uploads/Syd5h3k66.png =70%x) * Parser (Syntax Analyzer) **主動**向 Lexical Analyzer 要下一個 token * Lexical Analyzer 回傳一個 token * 兩者互動間透過 Symbol table 紀錄需要的訊息 ## 名詞解釋 * token * 是程式語言語法的最小單位 * 某種程式語言的 token 種類是一個有限集合 * 通常會由 \<token name (type), attribute value> 的形式來描述一個 token * token name: 這個 token 的種類名稱 * attribute: token 的實際內容會存放至 symbol table 中,attribute-value 則是指向該 table 的指標 ![image](https://hackmd.io/_uploads/ry16VTkTT.png =40%x) * pattern * 針對某種 token 的定義與描述 * ex: 變數: 由大小寫字母或數字或底線組成,第一個字不能是數字 * Lexeme * 符合某種 token 定義與描述的實際字串 ![image](https://hackmd.io/_uploads/BkIKl6kpp.png =70%x) * 實際例子 ![image](https://hackmd.io/_uploads/H19yrTJpT.png =70%x) 補充: Semantic values: 就是一個 token 的 attribute value,將會影響 semantic analysis ## 設計流程 (必考) ### 第一步: 定義 token 種類 決定哪些 token 種類是必要的 ![image](https://hackmd.io/_uploads/rkKx01x66.png =70%x) ### 第二步: 定義符合這個 token 的條件 (pattern) 使用 regular expression 進行定義 名詞定義 * alphabet (字母表): 可以用來組成 Lexeme 的字元 * string: 由 alphabet 中的字元組成的字串 * language: string 的集合。language 允許我們用有限的描述來定義一個集合。language 可以用以下方式產生 regular expression (RE)可以被視作用於描述 token type 的一種 language regular expression 規則: * |: or * *: 重複任意 0~無限次 * 次方: 重複指定次 * (): 優先決定 推廣規則: * +: 重複任意 1~無限次 * ?: 重複任意 0~1次 * []: 裡面的元素任意挑一個 * -: 連續 * ^: 除此之外 regular expression 範例: * language -> 代表的集合 ![image](https://hackmd.io/_uploads/ryTtn1gaa.png =60%x) ![image](https://hackmd.io/_uploads/BkeOT1eT6.png =60%x) ![image](https://hackmd.io/_uploads/S1eHvM-aa.png =15%x) ![image](https://hackmd.io/_uploads/SkSYDfWa6.png =15%x) ![image](https://hackmd.io/_uploads/rJa9vGW6p.png =20%x) ![image](https://hackmd.io/_uploads/rJHjDz-aT.png =30%x) ![image](https://hackmd.io/_uploads/Hy0owfWT6.png =50%x) regular definition: 用來定義別名 * 別名 -> 實際代表的 RE * ex: * letter_ -> [A-Za-z_] * digit -> [0-9] * id (變數命名規則) -> letter_(letter_ | digit)* 補充: match 演算法 ![image](https://hackmd.io/_uploads/ryd2nC16p.png =70%x) * lexemeBegin 將作為 match 的起始位置 * forward 將不斷往後找,直到 match 其中一個 token 的規則 ### 第三步: 定義 pattern match algorithm (Lexeme) * 此階段目標: 將 RE 的規則轉成程式碼 * 總結整體流程: 將 RE 轉成 NFA -> 將 NFA 轉成 DFA -> 拿 DFA 與實際的字串嘗試進行匹配 * NFA 跟 DFA 都是一種 Transition diagrams * Transition diagrams 可以直接被程式實作 #### Transition diagrams * 使用 Transition diagrams 可以表示 RE 的匹配過程 * 能實現 input string 的比對 * 著重在 case 之間的轉換 Transition diagrams 範例 ![image](https://hackmd.io/_uploads/HJzAVUBaT.png =50%x) * 留意 case 4 與 case 8,的部分,other 會抓到下一個 token 的字元,故抓完後要還回去 實作過程 ![image](https://hackmd.io/_uploads/HyuCgS4Ca.png =60%x) * 在進行 token 的比對的時候,遵循 "最長比對原則" * 即使比對到了 final state,仍繼續往下比對,直到進入 dead state 為止 * 同時維護三個變數 (因為有可能遇到,離開 final state 後,過幾步又回到 final state 的情況) * C: **用來紀錄當前的 state** * L: 用來紀錄 "上一個遇到的 final state",**該 final state 的編號** * P: 用來紀錄 "上一個遇到 final state 時,比對到哪一個字元",紀錄**該字元的位置** (1-indexed) (由於比對結束後,需要退回去 "上一個遇到 final state 的那個字元" 的位置,供下一個 token 得比對,故需要此變數紀錄) #### NFA * NFA (非確定性有限自動機): 利用 Transition diagrams,來檢測某個字串是否符合該 Transition diagrams 所代表之 RE 描述的技術 * NFA 可以被視作是一種 Transition diagrams * 規則: * **給定狀態跟輸入,下一個狀態可能有多種組合** * ex: 給定 Transition function: (1,a) = {1,2} (給定狀態 1 跟輸入 a),下一個可能的狀態可能是狀態 1 或 2 * **可以有 ε-move**,在不需要任何輸入的情況下,便可跳至其他的 state 中 * ε-closure(s): 某個 state 在不需要任何輸入的情況下,能移動到的 state 集合 ![image](https://hackmd.io/_uploads/S1M46ISTa.png =70%x) * 演算法整體流程: 判斷一個字串是否符合 NFA 的描述 ![image](https://hackmd.io/_uploads/B1JP1PSTT.png =80%x) 1. 擴展起點能走到的地方 2. 利用下一個遇到的字元進行移動 3. 移動後擴展新的 state 能走到的地方 4. 不斷重複 2, 3,直到所有字元都消耗完畢 5. 判斷最終走到的那些 state 是否跟 Final state 有交集。便可判斷某個字串是否符合 RE 的規則 * 演算法應用舉例 * 在計算上,需要把所有可能的 state 都寫出來,當遇到 Transition diagrams 未指定的行為時,則視作匹配失敗(即不可能走到終點) ![image](https://hackmd.io/_uploads/HyFBYIrpa.png =50%x) * 上圖說明了 "aaba" 在 Transition diagrams 的匹配過程 * 當 Transition diagrams 包含 ε-move 時,在移動每一步後,都需要透過 ε-closure(s) 擴展 state ![image](https://hackmd.io/_uploads/S1raaUHaT.png =70%x) #### DFA * DFA (確定性有限自動機): 利用 Transition diagrams,來檢測某個字串是否符合該 Transition diagrams 所代表之 RE 描述的技術 * **給定狀態跟輸入,下一個狀態只可能有一種組合** * **不可以有 ε-move** * 演算法整體流程 ![image](https://hackmd.io/_uploads/Hy6BzDHap.png =80%x) * 跟 NFA 相比,不需要進行 ε-move,且下一個狀態只可能有一種組合,故每個步驟中只可能有一個可能的 state ## Lexical analyzer generator 功能: 將使用者用 RE 定義的 token,轉換成 lexical analyzer 的 sourse code ### RE to NFA (必考) * 使用 Thompson’s construction algorithm,便可有系統性的將 RE 轉成 Transition diagrams 針對 ε ![image](https://hackmd.io/_uploads/r1yaJd4Ra.png =30%x) 針對一般字元 ![image](https://hackmd.io/_uploads/SyyfgdNAa.png =30%x) 針對 s | t (or 運算) ![image](https://hackmd.io/_uploads/rJO4gd4C6.png =40%x) * 開頭與結尾各新增一個 state,並用 ε-move 連結到 s 跟 t 針對 st (concat 運算) ![image](https://hackmd.io/_uploads/SJGYlOVAp.png =30%x) * 將 s 的結尾與 t 的頭連接在一起 針對 s* (重複0~無限次運算) ![image](https://hackmd.io/_uploads/H1PxbOVC6.png =40%x) * 開頭與結尾各新增一個 state,並用四個 ε-move 連接 舉例: (a|b)*abb ![image](https://hackmd.io/_uploads/rylRP0GlgR.png =70%x) ### NFA to DFA (必考) 演算法推演 ![image](https://hackmd.io/_uploads/SyAur9Aa6.png =70%x) 1. 從 1 出發,1 的 ε-closure 為 {1, 2, 3, 5, 8},把這個集合重新定義成 A 2. 定義 State A 的下一步 * 執行 move(A, a),move(A, a)={4, 9} * {4, 9} 的 ε-closure 為 {2, 3, 4, 5, 7, 8, 9},因為這個集合之前沒有出現過,把這個集合重新定義成 B * 執行 move(A, b),move(A, b)={6} * {6} 的 ε-closure 為 {2, 3, 5, 6, 7, 8},因為這個集合之前沒有出現過,把這個集合重新定義成 C。至此,State A 定義完成 3. 定義 State B 的下一步 * 執行 move(B, a),move(B, a),move(B, a)={4, 9} * {4, 9} 的 ε-closure 為 {2, 3, 4, 5, 7, 9},即回到 State B * 執行 move(B, b),move(B, b),move(B, b)={6, 10} * {6, 10} 的 ε-closure 為 {2, 3, 5, 6, 7, 8, 10},因為這個集合之前沒有出現過,把這個集合重新定義成 D。至此,State B 定義完成 4. 定義 State C 的下一步 * 執行 move(C, a),move(C, a)={4, 9} * {4, 9} 的ε-closure 為 {4, 9},即回到 State B * 執行 move(C, b),move(C, b)={6} * {6} 的ε-closure 為 {2, 3, 5, 6, 7, 8},即回到 State C 5. 定義 State D 的下一步 * 執行 move(D, a),move(D, a)={4, 9} * {4, 9} 的ε-closure 為 {4, 9},即回到 State B * 執行 move(D, b),move(D, b)={6, 11} * {6, 11} 的ε-closure 為 {2, 3, 5, 6, 7, 8, 11},因為這個集合之前沒有出現過,把這個集合重新定義成 E 6. 定義 State E 的下一步 * 執行 move(E, a),move(E, a)={4, 9} * {4, 9} 的ε-closure 為 {4, 9},即回到 State B * 執行 move(E, b),move(E, b)={6} * {6} 的ε-closure 為 {2, 3, 5, 6, 7, 8},即回到 State C 7. 全部 State 的下一步都知道了,結束 根據重新定義的結果,畫出 DFA ![image](https://hackmd.io/_uploads/BkXej9ATa.png =60%x) * 有包含 NFA final state 的那些新 State,便是新的 final state ### Minimizing DFA states (不考) * 如果有兩個 State,move 的動作完全一致,則可以合併之 # syntax Analysis * 相較於 Lexical Analyzer,處於主動的角色 * 設計目標: * 必須要能正確解析錯誤 * 必須能正確的 recover 錯誤發生的過程 * 速度應該要夠快 * error recovery 策略: * Panic mode: 把發生錯誤的那行 statement 去掉 * error production: 將錯誤情況一併列舉進語法分析中 * global correction: 僅可能的在執行最小改變的狀況下,進行全域變動讓程式變正確 * phrase level: 修改局部內容(ex: 加一個分號) ## Context-Free Grammars (CFG) * 可以視作類似 RE 的工具,用來描述 "語法" * RE 能描述的東西,CFG 也可以描述 * 由以下四個元素組成: * Terminals: 不會再轉換的 Context (小寫字母) * NonTerminals: 還可以再轉換的 Context (大寫字母、小寫斜體) * Productions: 轉換關係式 * Start symbol: 起始式 (ex: S) ![image](https://hackmd.io/_uploads/Sym-R65Rp.png =50%x) * derivation step 指的是透過 Productions 重寫當前結果一次,a sequence of derivation step 稱為 derivation * 當前的 Context 有多個可以展開的項時,有兩種思路可以展開 * leftmost derivation: 每次都挑選最左邊的 NonTerminals 去展開 * rightmost derivation: 每次都挑選最右邊的 NonTerminals 去展開 ### Parse Trees * 使用 "樹" 保留 CFG 展開的過程 * 如果尊崇 leftmost 或 rightmost derivation,則 Parse Trees 保證唯一,且 leftmost 及 rightmost derivation 建構出的 Parse Trees 相同 ![image](https://hackmd.io/_uploads/SJ8zVC906.png) 將展開的過程,轉換成樹 ![image](https://hackmd.io/_uploads/rJlwFURRT.png =70%x) ### Push Down Automata (PDA) * 用於自動化生成 Parse Trees 的程式 ex: 使用 left derivation 建構一棵 parse tree,嘗試匹配 cad ![image](https://hackmd.io/_uploads/B1OMppDRa.png =50%x) ![image](https://hackmd.io/_uploads/rkF8paDAa.png) * 從 S 開始,將 S 轉換成 c, A, B * A 可能有兩種轉換方式(由於是 leftmost,故先挑 A 而非 B 展開),先將 A 轉換成 a, b -> 匹配失敗,backtrack * 再將 A 轉換成 a * 將 B 轉換成 d * 觀察葉節點 cad,以成功匹配到所求字串,結束 PDA 還有一個未解的難題,也就是當遇到多個 production rule 可選擇時,不知道要選哪個 LL(k) Parsing: 使用 leftmost derivation,且往前偷看 k 個 input,以決定要使用哪個 production rule ### LL(1) Parsing 用於將 CFG 轉成程式 預處理: ![image](https://hackmd.io/_uploads/Bkmn6ZZ1A.png =80%x) * FIRST(): **某條 production rule 不斷推到底後,有可能排在第一個位置的 terminal** 有哪些 * 心法: 1. 直接看 * ex: FIRST(S) = {if} 2. 若是某個 production rule 推到底後,排在第一位的可能是其他 nonTerminal,則該 nonTerminal 擁有的所有 FIRST() 都是這個 production rule 的 FIRST() ex: FIRST(L) = {if} (因為 L 可以推到 S) 3. 如果推到底後,ε 也可能出現的話也要記 * ex: FIRST(L) = {ε} ![image](https://hackmd.io/_uploads/ByI11fZkA.png =80%x) * FOLLOW(): **緊跟在這個 Nonterminals 的 terminal 有可能是誰,由其他 production rule 推得** * 心法: 1. 針對某條 production rule 代表的 nonTernimal,直接看 "->" 右邊出現的該 nonTerminal 中,後一個 terminal 是什麼 * ex: FOLLOW(S) = {else} 2. 針對某條 production rule 代表的 nonTernimal,直接看 "->" 右邊出現的該 nonTerminal 中,後一個 nonTerminal 的 FIRST() 有哪些 3. 若 A → α B,則 FOLLOW(B) 包含所有 FOLLOW(A) 的 terminal (出現在 A 後面的 terminal 也會出現在 B) (α 代表一堆任意數量的 Terminal 與 nonterminal) * ex: FOLLOW(E) = {else} 4. 若 A → α B β 且 β 可能變成 ε,則 FOLLOW(B) 包含所有 FOLLOW(A) 的 terminal (出現在 A 後面的 terminal 也會出現在 B) 5. ε 也可能出現的話不需要記 6. 如果這個 Nonterminals 是 start state,則多記一個 "$" 符號 * ex: FOLLOW(S) = {$} #### Recursive Descent Parsing 根據 FIRST() 跟 FOLLOW(),直接推出程式碼 match function ![image](https://hackmd.io/_uploads/BkG2LCfeA.png =50%x) * 用來配對一個 terminal nonternimal 寫法 ![image](https://hackmd.io/_uploads/Sk1KDAGeC.png =70%x) * 假設 First(L) = {IF, BEGIN, PRINT, ε} * 假設 Follow(L) = {end} * 針對某個 nonterminal 的函式規則: 1. 若是某個 terminal 存在於 該 nonterminal 的 FIRST() 中,則新增 "case 該terminal",並在後面跟著對應的 CFG * ex: case PRINT: S(); match(SEMI);L();break; 3. 若是該 nonterminal 包含 ε,則針對存在於該 nonterminal Follow() 中的 terminal,新增"case 該terminal",並在後面跟著對應的 CFG * ex: case END: break; #### Table-Driven Predictive Parsing (必考) 根據 FIRST() 跟 FOLLOW(),建立 Parsing table 建立規則 1. Parsing table 的左邊放 Nonterminals,上面放 terminal,紀錄的是 "當 Nonterminals 遇到 terminal 時,該怎麼轉換" 2. 對於每條 grammer A -> ... * 若某個 terminal 在 First(A) 中,table[A, 該 terminal] = 該 grammer * 若 ε 在 First(A) 中,table[A, follow(A)中的 terminal] = 該 grammer ($ 也要) ![image](https://hackmd.io/_uploads/SJWiCDqJA.png) parsing table 範例: ![image](https://hackmd.io/_uploads/rJBjIOq1R.png) 有了 Parsing table 後,便可以開始匹配 input ![image](https://hackmd.io/_uploads/B10pL_qJC.png =70%x) * Matched: 當前匹配成功的部分 * Stack: 初始化為 start state + $ * Input: 想要匹配的字串 * 每一步的動作中 * 當 Stack 的 Top 是 Nonterminals 時,利用 parsing table 轉換 Stack 中的 grammers * 當 Stack 的 Top 是 terminal 時,進行 match 的動作 ### ambiguous grammers 某些 CFG 可能導致建構出來的 parse tree 不只一種 例如: ![image](https://hackmd.io/_uploads/rJVuVUqJR.png =50%x) 解決方法為 1. 重寫整個 grammer 2. 新增一些額外的限制規則 ### left recursion left recursion 會導致 LL(1) Parsing 失敗 例如 A -> A α | β 在嘗試匹配 βααα 時,不知道要選 A α 還是 β 好 解決方法: 新增一個 nonternimal,將原本的 grammer 等價轉換 ![image](https://hackmd.io/_uploads/HJ8gZ2jJC.png) ex: ![image](https://hackmd.io/_uploads/SklcbPnsk0.png =70%x) 注意: left recursion 也可能是非直接的 ![image](https://hackmd.io/_uploads/rkcmDnjyC.png =70%x) * 將 A -> Sd 替換成 A -> Aad | bd 時,才出現 left recursion ### left factoring left factoring 會導致 LL(1) Parsing 失敗 例如 A -> α β1 A -> α β2 這時候會不知道要選哪一條 statement 解決方法: 新增一個 nonternimal A',將原本的 grammer 等價轉換 ![image](https://hackmd.io/_uploads/rJich3syR.png =30%x) ex: ![image](https://hackmd.io/_uploads/HkNLphjJ0.png =50%x) ### error recovery 發生匹配失敗的情況時的處理方式 預處理: ![image](https://hackmd.io/_uploads/rktu_0iy0.png =70%x) * 把所有在 Follow(A) 中的 token,對應於 parsing table 中的位置設成 sync (若該位置不為空) 處理邏輯: * 若是 Stack 除了 $ 外只剩一個元素,則 pop input ![image](https://hackmd.io/_uploads/Bkvi5CokR.png =70%x) * 若是遇到了 "synch" 的情況,則將一個 token 從 Stack 中移除 ![image](https://hackmd.io/_uploads/Hk3n9CoyA.png =70%x) * 若是 Stack 跟 input 的 terminals 不相同,則將一個 token 從 Stack 中移除 # Semantic Analysis ## SDD (syntax-directed definition) * syntax-directed definition 是一種 context-free grammer,但對於每個 grammar symbol,都新增一些 semantic attributes * 例如 A 是 grammar symbol,a 是 semantic attributes,則可以寫成 A.a * semantic attributes 又分成兩類 * synthesized attribute: 該屬性的值由其 parent 或是 siblings 得到 * inherited attribute: 該屬性的值由其 chuld 得到 * Terminals 不可以包含 inherited attribute * 對於 context-free grammer 中的每條 production rule,都可包含數個 semantic rules ![image](https://hackmd.io/_uploads/r1NLunhgA.png =50%x) * F.val = digit.lexval 指的是將 digit 實際的數值,賦值給 F.val ### Annotated Parse Trees * 包含 semantic attributes 的 parse tree 又被稱為 Annotated Parse Trees #### 構築 1. 畫出一般的 Parse Trees 2. 根據 syntax-directed definition 的那張 "production 跟 semantic rules 對應關係" 的表格,計算出所有 grammar symbol 的 semantic attributes * dependency: 在計算某些 grammar symbol 的 semantic attributes 前,可能需要先計算其他 grammar symbol 的 semantic attributes ![image](https://hackmd.io/_uploads/Skso5h2e0.png =50%x) * 計算 E.val 前,需先計算 E1.val 跟 T.val 範例: ![image](https://hackmd.io/_uploads/BymW-33VC.png) ![image](https://hackmd.io/_uploads/Bkbl-n2E0.png) #### Dependency Graphs * 在 Annotated Parse Trees 上,進一步使用箭頭描述 semantic attributes 的依賴關係 * 當某個 semantic attributes A 需要另一個 semantic attributes B 才能計算時,我們稱 A depend on B ![image](https://hackmd.io/_uploads/Hk7G-h3N0.png) * 數字代表順序 ## Syntax-directed translation scheme (SDT) 與 SDD 的唯一差別是,會指定 semantic attributes 用到的時機 (用大括號指明) ex: ![image](https://hackmd.io/_uploads/S1tZahhgC.png =80%x) * 在處理完 T 後,處理 R 之前,便會立刻執行 {R.i := T.nptr} * 在處理完 R 後,結束之前,便會立刻執行 {E.nptr :=R.s} ### 程式實作 (Top-Down Translators) --- 以 ANTLR 作為範例 * 若今天要以 ANTLR 來實作以下的 cfg ![image](https://hackmd.io/_uploads/SkIfCJT40.png) * 程式實作 ![image](https://hackmd.io/_uploads/BkMBCyaVA.png) ![image](https://hackmd.io/_uploads/BJy8AJaNC.png) * {ids.in:=type.t}: 將 type.t 作為參數,傳入 ids 中 * {declare.flag=ids.flag}: flag 將作為 declare 的回傳值 * {type.t:=1}: 在 type 中,使 t=1 * {type.t:=2}: 在 type 中,使 t=2 * {addSymtab(id.entry, ids.in)}: 使用 JAVA 的 HashMap.put() 來實現 ## abstract syntax tree * 是編譯器後端所需的中心資料結構,可以在 parsing 的過程中順便建立 * 省略了後端用不到的資訊 ![image](https://hackmd.io/_uploads/rJHFSxLb0.png =50%x) * 左邊為 Parse Trees * 右邊為 abstract syntax tree * 中間節點為 operator * 葉節點為變數或常數 * abstract syntax tree api * mknode(op, left, right) * 創造一個 operator 為 op 的 abstract syntax tree 中間節點,並使其分別指向 left 跟 right 兩個 節點 * mkleaf(id, entry) * 創造一個葉節點,用來儲存 id 的值是多少 (entry 是一個指向 symbol table 的指標) * mkleaf(num, val) * 創造一個葉節點,用來儲存 num 的值是多少 ex: ![image](https://hackmd.io/_uploads/BJq8BlLWA.png =80%x) * 像這樣利用 SDT 語法,便可在 parsing 的過程中建立 abstract syntax tree ![image](https://hackmd.io/_uploads/r1q1TFYEA.png =70%x) ## type checking * 每個 ID (變數) 都有一個 semantic attributes "type" * 透過 semantic attributes,便可檢查 type 在操作上是否有不合法之處 ![image](https://hackmd.io/_uploads/B1XqKlLb0.png =70%x) * 此範例負責檢查進行 mod 操作的兩個變數,是否都是 int 型態 # intermediate code generator ## Three-Address Code * 這種程式可以被視為轉成組合語言之前的前置語言 * Three-Address code 包含兩個重要觀念 * Address: 可以是變數 (ex: a)、常數 (ex: 3.14)、temp (ex: t1)三者之一 * instruction: 一條指令中,等號右邊最多只會包含一個 oprand * 當原本的程式碼中包含多個運算的 statement 時,Three-Address Code 會將其拆開 * ex: a=b*c+d 拆成 * t1=b*c * t2=t1+d * 可以透過 parsing 時, 來順便產生,讓接下來的後端能夠使用 ### Three-Address Code 儲存方式 #### Quadruples * 每個 row 都是一條 instruction * 四個欄位分別紀錄 oprand、第一個參數、第二個參數、該指令的計算結果 ![image](https://hackmd.io/_uploads/rJZs9dRMA.png) #### Triples * 每個 row 都是一條 instruction * 三個欄位分別紀錄 oprand、第一個參數、第二個參數 * (0) 代表其值來自第 0 條指令的計算結果,其他如 (1)、(2) 等以此類推 ![image](https://hackmd.io/_uploads/BkJJodCfC.png) #### Indirect Triples * 相較於 Triples,多了一個欄位,用來紀錄指令的執行順序 * 第 35 條指令的執行結果存在 (0)、第 36 條指令的執行結果存在 (1)... * 在日後的優化中,編譯器可能會調動指令的執行順序,此時新增的欄位將會非常有用 (只需交換 "紀錄指令的執行順序" 的那個欄位,而不需動到存放 op 的那個 table) ![image](https://hackmd.io/_uploads/HkAR6m5NC.png) * 交換第 1、2 條指令後的結果 ### Static single-assignment form (SSA form) * 目的是讓 conpiler 在程式碼更動時,能夠清楚識別要改哪些地方 * 在這種格式中,每個變數只能被賦值一次 ![image](https://hackmd.io/_uploads/ryX24_0M0.png =50%x) * 可以觀察到,Three-Address Code 的變數 p 在 SSA form 的格式中,分成 p1, p2, p3 ### 利用 syntax-directed definition (SDD) 產生 Three-Address Code * 名詞解釋 * Temp(): 呼叫後,將會回傳一個獨一無二的暫時變數名 (如 t1, t2 等等) * newlabel(): 呼叫後,將會回傳一個獨一無二的 label (用於 if, goto 等等) * S.code: 該 attribute 用來紀錄 S 的 Three-Address Code * E.addr: 該 attribute 用來紀錄 E 的 Address (即 Three-Address code 中的 Address) * top.get(id): 取得 id 所代表的實際變數名 * ||: 代表緊跟在後,可以視作 "+" ![image](https://hackmd.io/_uploads/SyQkGEq4A.png) * 為了能進一步的簡化,我們可以假設 gen() 生成的新 code 會加在原先產生的 code 之後;如此一來,上面的 grammer rule 便可以進一步改寫成 ![image](https://hackmd.io/_uploads/rk7vG49EC.png =80%x) ## 其他應用 * Array access: 計算轉成組合語言時所需的 base 跟 bias * Type Conversion: 當 operator 兩端的變數型態不一樣時,進行型態轉換 * Flow-Control: 生成 label,進行流程控制 * Boolean Expression: 實作 boolean 運算 (如邏輯 and、邏輯 or 等等) # runtime evironment ## 儲存變數相關資訊 offset: 目的是用來記錄下一個變數,在記憶體中的存放位置 top.put(): 將變數、資料型態、 offset 存至 symbol table 中 ## 儲存 struct 相關資訊 * 當儲存 struct 時,會先將原本的 symbol table 跟 offset 用 stack 存起來 * 新配置一個 symbol table,且使 offset = 0 * 像紀錄變數一樣,紀錄 struct 中的元素 * 將新的 symbol table 以及 offset 的資訊存起來,如此一來便可紀錄 struct 的相關資訊 ## symbol table 詳細介紹 * 前端通常會將資料寫進 symbol table 中 * 後端通常會存取 symbol table 中的內容,以幫助其產生組合語言 * symbol table 中通常會紀錄 * 變數名稱 * 型態 * 位置 ## scope 問題 * 當有兩個變數名稱相同,但層級不同時 (ex: 全域變數與區域變數),此時需要額外做處理 * 處理方式: * 每一個 struct block 都包含一個 symbol table,用來紀錄這個區域中宣告的變數 * 先看最裡面的 struct block,如果找不到,再看外一層的 struct block,以此類推 實作: * 每創建一個 symbol table 時,會有一個指標 prev,指向上一個 symbol table * 對新的 symbol table 進行正常的寫入操作 * 遇到 scope 問題時,便可以先檢查當前的 symbol table,若找不到便可透過 prev 指標來檢查外一層的 symbol table # Code generator ## controll flow graph (必考) 原本的 three address code,不適合分析與優化,故需將原程式進一步轉成 controll flow graph * 一個 controll flow graph 可能包含許多 basis graph * 每一個 basis graph 一定是 "single entry, single exit" ### 決定 basis block * 首先,找出每個 basis block 的 leader * 所有指令的第一道指令 * jump 指令的 target * 緊跟在 jump 指令之後的指令 * 畫出程式執行流程,在不同的 basis block 間的跳轉關係 ![image](https://hackmd.io/_uploads/B18AhHc4C.png =30%x) ![image](https://hackmd.io/_uploads/B1NyTr5EC.png =40%x) ## Call Graph 描述了不同 fucntion call 之間的關係 ## Next-Use information * 若在一個 basis block 中,有一個變數 x 在第 i 行被賦值,且被第 j 行使用前,都沒有被重新賦值,則稱 "a_{i} is live at j" * 第 i 行一定要有該變數的賦值操作 * 第 j 行一定要有該變數的使用操作 舉例 ![image](https://hackmd.io/_uploads/rJI0qIqNA.png =70%x) * 變數 a 在第 1 行被賦值,且被第 3 行使用前,都沒有被重新賦值,則稱 "a_{1} is live at 3" ## code generator * 一切操作都是在同一個 block 進行的,跳至不同 block 時,用來紀錄的 descriptor 接會刷新 * code generator 需要持續追蹤哪個變數在哪個 register 中,以避免不必要的 load/store 操作 * Redister descriptor: 紀錄 register 目前存放的變數 * Address descriptor: 紀錄變數最新的值放在哪裡 (可以連結 symbol table) * getReg(instruction): 輸入給定一條 Instruction,選擇並分配適當的 register 給該指令會用到的變數 (需要查看 Redister descriptor、Address description、其他訊息來決定) * load 原則: 當變數需要被用到,但沒有被 Redister descriptor 紀錄時 * LD R, x: * 使得 Redister descriptor 中的 register R 只包含 x * 將 register R 視作額外的 address,交由 Address descriptor 紀錄 * store 原則: 當變數離開某個 basis block 時還活著,需要把其值存起來 * ST x, R: * 使 Address descriptor 紀錄變數 x 自己的記憶體位址 ex: * 算術運算操作: * 初始化時,所有 register 都沒有在使用,各變數都存在自己的 address 上 ![image](https://hackmd.io/_uploads/HJBVZR5EC.png =60%x) * t=a-b: * 呼叫 getReg(),該 api 告訴我們各變數分配的 register 是誰,a=>R1, b=>R2, t=>R2 * 由於 a 最新的值不在 register 中,於是生成一條 load 指令,將 a 的值 load 至 r1 * 更新 Redister descriptor: R1 = a * 更新 Address description: 變數 a 最新的值存在 a 跟 R1 * 變數 b 同理 ![image](https://hackmd.io/_uploads/B15xGA9E0.png =60%x) * 進行減法運算,並將計算結果 t 放在 R2 * 更新 Redister descriptor: R2 = t * 更新 Address description: 變數 b 最新的值只存在 b * 更新 Address description: 變數 t 最新的值存在 R2 ![image](https://hackmd.io/_uploads/SJcqfAqNC.png =60%x) * 賦值操作 * 此範例的初始化 Redister descriptor 跟 Address descriptor ![image](https://hackmd.io/_uploads/HJ1ci0cE0.png =60%x) * a=d * 呼叫 getReg(),該 api 告訴我們各變數分配的 register 是誰,a, d =>R2 * 由於 d 最新的值不在 register 中,於是生成一條 load 指令,將 d 的值 load 至 r2 * 更新 Redister descriptor: R2 = d * 進行賦值運算,更新 Redister descriptor: R2 = a, d * 更新 Address description: 變數 a 最新的值只存在 R2 * 儲存操作 * 此範例的初始化 Redister descriptor 跟 Address descriptor ![image](https://hackmd.io/_uploads/SkuzmkjER.png =60%x) * 當 Exit the basic block 時 * 由於變數 a 跟 d 最新的值不在 Address 中,故將 a 最新的變數值存在 Address a,d 最新的變數值存在 Address d ![image](https://hackmd.io/_uploads/H1J9XysVR.png =60%x) ### getReg(instruction) 邏輯 1. 若某變數已經在 Redister descriptor 中了,則直接沿用 2. 若某變數不在 Redister descriptor 中,但 Redister descriptor 還有空閒空間,則將該空閒空間配置出去 3. 若 Redister descriptor 沒有空閒空間配置出去,則必須移除其中一個已經在 Redister descriptor 中的變數 1. 若移除的變數,在其他地方還保有最新的值的話,則直接移除 2. 若移除的變數本來這輪就會更新,則直接移除 3. 若移除的變數,在未來不在被使用,則直接移除 4. 先把要移除的變數用 Store 指令存起來,接著再移除 ### graph coloring * register 的分配是個抽象的難題,但可以轉成 graph coloring 方便進一步計算 * 可以將各變數存活時間畫出來 ![image](https://hackmd.io/_uploads/Bk7HSfsNA.png =50%x) * 接著畫圖,點為各變數,若兩變數存活時間有重合,則建立一條邊;接著為每個點著色,顏色即代表分配給該變數的 register ID ## Data Dependence * 目的是要確認兩條 instruction 是否能對調 * 若調整之後,Data Dependence 不變,則對調操作合法 * **若程式碼包含 if-else 等條件判斷,則所有可能的情況都要確認過** ### Flow dependence * **先定義再使用** ![image](https://hackmd.io/_uploads/Sk1iEMV4C.png =20%x) * 語法展示 ![image](https://hackmd.io/_uploads/HJGS_MoE0.png =20%x) ### Anti-dependence * **先使用再(重新)定義** ![image](https://hackmd.io/_uploads/BJBo_fjVR.png =20%x) * 語法展示 ![image](https://hackmd.io/_uploads/SJ6o0zjV0.png =20%x) ![image](https://hackmd.io/_uploads/B1-wtzi40.png =15%x) ### Output dependence * **連續兩次重新定義** ![image](https://hackmd.io/_uploads/rkVVFzs4R.png =20%x) * 語法展示 ![image](https://hackmd.io/_uploads/BJUrKMoEC.png =20%x) ### 範例 ![image](https://hackmd.io/_uploads/H1Jgm2h4C.png) ![image](https://hackmd.io/_uploads/ByWW7hhEC.png)