Stanford公開課《編譯原理》學習筆記(2)遞歸下降法

来源:https://www.cnblogs.com/dashnowords/archive/2019/10/07/11632103.html
-Advertisement-
Play Games

示例代碼托管在: "http://www.github.com/dashnowords/blogs" 博客園地址: "《大史住在大前端》原創博文目錄" 華為雲社區地址: "【你要的前端打怪升級指南】" [TOC] B站地址: "【編譯原理】" Stanford公開課: "【Stanford大學公開課 ...


目錄

示例代碼托管在:http://www.github.com/dashnowords/blogs

博客園地址:《大史住在大前端》原創博文目錄

華為雲社區地址:【你要的前端打怪升級指南】

B站地址:【編譯原理】

Stanford公開課:【Stanford大學公開課官網】

課程里涉及到的內容講的還是很清楚的,但個別地方有點脫節,建議課下自己配合經典著作《Compilers-priciples, Techniques and Tools》(也就是大名鼎鼎的龍書)作為補充閱讀。

一. Parse階段

詞法分析階段的任務是將字元串轉為Token組,而Parse階段的目標是將Token變為Parse Tree,本篇只是這部分內容最基礎的一部分。

CFG

CFGcontext free grammer,定義一種CFG語法規則需要聲明如下特征:

  • 一組終止符號集,也稱為“詞法單元”
  • 一組非終止符號集,也稱為“語法變數”
  • 一個開始符號集
  • 若幹產生式規則(產生式則就是指在當前CFG的語法下,產生符號->左右兩側可以互相替代)

CFG的基本轉換流程如下:

從隸屬於開始集S開始,嘗試將字元串中的非終止符X替換為終止集的形式(X->Y1Y2...Yn),重覆這個步驟直到字元串序列中不再有非終止符。這個過程被稱為Derivation(派生),它是一系列變換過程的序列,可以轉換為樹的形式,樹的根節點即為起始集合S中的成員,轉換後的每個終止集以子節點的形式掛載在根節點下,這棵生成的樹就被稱為Parse Tree,可以看出最後的結果實際上就是Parse Tree的葉節點遍歷結果。

當需要轉換的非終結字元有多個時,需要按照一定的順序來逐個推導,派生過程可以按照left-mostright-most進行,但有時會得到不同的合法的轉換樹,通常會通過修改轉換集語法或設定優先順序來解決。

Recursive Descent(遞歸下降遍歷)

Recursive Descent是一種遍歷parse tree的策略,是一種典型的遞歸回溯演算法,從樹的根節點開始,逐個嘗試當前父節點上記錄的非終止字元能夠支持的產生規則,並判斷其子節點是否符合這樣的形式,直到子節點符合某個特定的產生式規則,然後再繼續遞歸進行深度遍歷,如果在某個非終止節點上嘗試完所有的產生式規則都無法繼續向下進行使得子樹的葉節點都符合終止符號集,則需要通過回溯到上一節點並嘗試父節點的下一個產生式規則,使得迴圈程式可以繼續向後進行。課程里用了很多的數學符號定義和偽代碼來描述遞歸遍歷的過程,如果覺得太抽象不好理解可以暫時略過。需要註意左遞歸文法會使得遞歸下降遍歷進入死迴圈,在文法設計時應該避免,龍書中也提供了一種通用的拆分方法來解決這個問題。

二. 遞歸下降遍歷

【聲明】由於課程中並沒有看到從tokensparse tree的全貌,只能先逐步消化基礎知識。下文的過程只是筆者自己的理解(尤其是逐行分析的形式,因為尚未涉及任何結構性語法,所以通用性還有待考量),僅供參考,也歡迎交流指正。但對於直觀理解遞歸下降法而言是足夠的。

2.1 預備知識

本節中使用JavaScript來實現遞歸下降遍歷,目標代碼仍是上一篇博文中的示例代碼:

var b3 = 2;
a = 1 + ( b3 + 4);
return a;

經過上一節的分詞器後可以得到下麵的詞素序列:

[ 'keywords', 'var' ],
[ 'id', 'b3' ],
[ 'assign', '=' ],
[ 'num', '2' ],
[ 'semicolon', ';' ],
[ 'id', 'a' ],
[ 'assign', '=' ],
[ 'num', '1' ],
[ 'plus', '+' ],
[ 'lparen', '(' ],
[ 'id', 'b3' ],
[ 'plus', '+' ],
[ 'num', '4' ],
[ 'rparen', ')' ],
[ 'semicolon', ';' ],
[ 'keywords', 'return' ],
[ 'id', 'a' ],
[ 'semicolon', ';' ]

語法分析是基於語法規則的,所謂語法規則,通常是指一系列CFG表示的產生式,大多數開發者並不具備設計一套語法規則的能力,此處直接借鑒Mozilla中的Javascript引擎SpiderMonkey中的文法定義來進行基本產生式,由於Javascript語言中涉及的文法非常多,本節只篩選出與目標解析式相關的一部分簡化的語法規則(圖中標記為藍色的部分):

完整的語法規則可以查看【SpiderMonkey_ParserAPI】進行瞭解。

2.2 多行語句的處理思路

我們把上面的目標解析代碼當做是一段Javascript代碼,自頂向下分析時,根節點的類型是Program,它可以由多個Statement節點(語句節點)構成,所以本例中進行簡化後以semicolon(分號)作為詞素批量處理的分界點,每次將兩個分號之間的部分讀入緩衝區進行分析,由於上例中均為單行語句,所以理解起來比較簡單。

在更為複雜的情況中,代碼中包含條件語句,迴圈語句等一些結構化的關鍵詞時可能會存在跨行的語句,此時可以在遞歸下降之前先對緩衝區的詞素隊列進行基本的結構分析,如果發現匹配的結構化模式,就從tokens序列中將下一行(或多行)也讀入緩衝區,直到緩衝區中的所有tokens放在一起符合了某些特定的結構,再開始進行遞歸下降。

2.3 簡易的文法定義

為方便理解,本例中均使用關鍵詞縮寫來表示可能的語法規則集,如果你對Javascript語言有一定瞭解,它們是非常容易理解的

/**
 * 文法定義-生產規則
 * Program -> Statement
 * P -> S
 * 
 * 語句 -> 塊狀語句 | if語句 | return語句 | 聲明 | 表達式 |......
 * Statement -> BlockStatement | IfStatement | ReturnStatement | Declaration | Expression |......
 * S -> B | I | R | D | E
 * 
 * B -> { Statement }
 * 
 * I -> if ( ExpressionStatement ) { Statement }
 * 
 * R -> return Expression | null
 * 
 * 聲明 -> 函數聲明 | 變數聲明
 * Declaration -> FunctionDeclaration | VariableDeclaration
 * D -> F | V
 * 
 * F -> function ID ( SequenceExpression ) { ... }
 * 
 * V -> 'var | let | const' ID [= Expression | Null] ?
 * 
 * 表達式 -> 賦值表達式 | 序列表達式 | 一元運算表達式 | 二元運算表達式 |......
 * Expression -> AssignmentExpression | SequenceExpression | UnaryExpression | BinaryExpression | BracketExpression......
 * E -> A | Seq | U | BI | BRA |...
 * 
 * A -> E = E //賦值表達式
 * 
 * Seq -> ID,ID,ID//類似形式
 * 
 * //一元表達式
 * U -> "-" | "+" | "!" | "~" | "typeof" | "void" | "delete" E
 * 
 * //二元表達式
 * BI -> E "==" | "!=" | "===" | "!=="
         | "<" | "<=" | ">" | ">="
         | "<<" | ">>" | ">>>"
         | "+" | "-" | "*" | "/" | "%"
         | "|" | "^" | "&" | "in"
         | "instanceof" | ".."  E
 * 
 * //括弧表達式
 * BRA -> ( E )
 * 
 * N -> null  
 */

需要額外註意的是表達式Expression到賦值表達式AssignmentExpression的產生式,E的判斷規則里需要判斷A,而A的邏輯里又再次調用了E,這裡就是一種左遞歸,如果不進行任何處理,在代碼運行時就會陷入死迴圈然後爆棧,這也就是前文強調的需要在語法產生式設計時消除左遞歸的場景。這裡並不是說spiderMonkeyparserAPI是錯的,因為消除左遞歸的語法改造只是一種等價形式的轉換,是為了防止產生式產生無限遞推(或者說程式實現時進入無限遞歸的死迴圈)而做的一種形式處理,改造的過程可能只是引入了某個中間集合來消除這種場景的影響,對於最終的語法表意並不會產生影響。

下文示例代碼中並沒有進行嚴謹的"左遞歸消除",而是簡單地使用了一個E_集合,與原本的E進行一些微小的差異區分,從而避免了死迴圈。

2.4 文法產生式的代碼轉換

下麵將上一小節的語法規則進行代碼翻譯(只包含部分產生式的推導,本例中的完整代碼可以從demo或代碼倉中獲取):

//判斷是否為Statement
function S(tokens) {
    //把結尾的分號全部去除
    while(tokens[tokens.length - 1][0] === TT.semicolon){
        tokens.pop();
    }
    return B(tokens) || I(tokens) || R(tokens) || D(tokens) || E(tokens);
}

//判斷是否為BlockStatement  B -> { Statement } (本例中並不涉及本方法,故暫不考慮末尾分號和文法遞歸的情況)
function B(tokens) {
     //本例中不涉及,直接返回false
    return false;
}

//判斷是否為IfStatement I -> if ( ExpressionStatement ) { Statement }
function I(tokens) {
    //本例中不涉及,直接返回false
    return false;
}
//判斷是否為ReturnStatement  R -> return Expression | null
function R(tokens) {
    return isReturn(tokens[0]) && (E(tokens.slice(1)) || N(tokens.slice(1)[0]));
}

//判斷是否為聲明語句 Declaration -> FunctionDeclaration | VariableDeclaration
function D(tokens) {
    return F(tokens) || V(tokens);
}

//判斷是否為函數聲明  F -> function ID ( SequenceExpression ) { ... }
function F(tokens) {
    //本例中不涉及,直接返回false
    return false;
}

//判斷是否為變數聲明  V -> 'var | let | const' ID [= Expression | Null] ?
function V(tokens) {
    //判斷為1.單純的聲明 還是 2.帶有初始值的聲明
    if (tokens.length === 2) {
        return isVariableDeclarationKeywords(tokens[0]) && tokens[1][0] === TT.id;
    }
    return isVariableDeclarationKeywords(tokens[0]) && (A(tokens.slice(1))) || N(tokens.slice(1));
}

//....其他代碼形式雷同,不再贅述

2.5 逐行解析

解析時預設每次遇到一個分號時表示一個statement的結束,前文已經提及過對於多行語句的處理思路。實現時只需要將tokens序列一點點讀進buffer數組並從頂層的S方法啟動分析,即可完成自頂向下的推理過程。

 /**parser */
function parse(tokens) {
    let buffer = nextStatement(tokens);
    let flag = true;

    while (buffer && flag){

       if (!S(buffer)) {
           console.log('檢測到不符合語法的tokens序列');
           flag = false;
       } 
       buffer = nextStatement(tokens);
    }   
    
    //如果沒有出錯則提示正確
    flag && console.log('檢測結束,被檢測tokens序列是合法的代碼段');
}

//將下一個Statement全部讀入緩衝區
function nextStatement(tokens) {
    let result = [];
    let token;

    while(tokens.length) {
        token = tokens.shift();
        result.push(token);
        //如果不是換行符則
        if (token[0] === CRLF) {
            break;
        }
    }

    return result.length ? result : null;
}

2.6 查看計算過程

單步執行查看計算過程可以幫助我們更好地理解遞歸下降法的執行過程:

在demo所在目錄下打開命令行,輸入:node --inspect-brk recursive-descent.js,然後單步執行就很容易看出代碼在執行過程中如何實現遞歸和回溯:

三.小結

單純地遞歸下降法最終的結果只找出了不滿足任何語法規則的語句,或是最終所有語句都符合語法規則時給出提示,但並沒有得到一個樹結構的對象,也沒有向下一個環節提供輸出,如何在編譯過程中與後續環節進行連接還有待探索。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 關鍵詞:PostgreSQL 11、MySQL5.7 比較版本:PostgreSQL 11 VS MySQL5.7(innodb引擎) Oracle官方社區版 版權情況:PostgreSQL 11(免費開源)、MySQL5.7 Oracle官方社區版(免費開源) ...
  • MySQL作業分析 五張表的增刪改查: 完成所有表的關係創建 創建教師表(tid為這張表教師ID,tname為這張表教師的姓名) 創建班級表(cid為這張表班級ID,caption為這張表班級門號) 創建課程表(cid為這張表課程ID,cname為課程名稱,teacher_id為任課教師的ID) 創 ...
  • 今天小編要分享的還是Android Jetpack庫的基本使用方法,本篇介紹的內容是Jetpack Navigation組件,讓我們一起學習,為完成年初制定的計劃而努力吧! 組件介紹 導航,是指提供用戶在應用程式中的不同內容之間進行瀏覽、退出的交互功能。如我們在Android手機上常常用到的物理/虛 ...
  • axios現在最新的版本的是v0.19.0,本節我們來分析一下它的實現源碼,首先通過 gitHub地址獲取到它的源代碼,地址:https://github.com/axios/axios/tree/v0.19.0 下載後就可以看到axios的目錄結構,主目錄下有一個index.js文件,該文件比較簡 ...
  • 花費了幾周的時間斷斷續續的練習和模仿與使用JavaScript代碼實現了十大排序演算法。 裡面有每種演算法的動圖和靜態圖片演示,看到圖片可以自己先按照圖片的思路實現一下。 github中正文鏈接,點擊查看 兩年前端學習筆記:https://github.com/zhangyachang/Notes 歡迎 ...
  • 最近在學React Native,學到了CodePush熱更新。 老師講了兩種實現的方法,現將其記錄一下。 相比較原生開發,使用React Native開發App不僅能節約開發成本,還能做原生開發不能實現的熱更新功能。 使用原生技術開發App時,每次代碼做了改動後,都需要提交到應用商店進行審核,審核 ...
  • ## 今日內容: 1. JQuery 高級 1. 動畫 2. 遍歷 3. 事件綁定 4. 案例 5. 插件 ## JQuery 高級 1. 動畫 1. 三種方式顯示和隱藏元素 1. 預設顯示和隱藏方式 1. show([speed,[easing],[fn]]) 1. 參數: 1. speed:動畫 ...
  • 前言 在開發的時候,有時在命令工具裡面,要多開兩個視窗分別啟動前端項目和後端服務介面,有沒有辦法將整個項目一起啟動呢 答案是有,前端和後端連載一起啟動,適用於前端為vue或React,後端為nodejs的項目。 只需用到一個npm包concurrently模塊,通過package.json配置實現。 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...