從編譯原理看一個解釋器的實現

来源:http://www.cnblogs.com/OceanEyes/archive/2017/06/18/implement_a_interpreter.html
-Advertisement-
Play Games

『設計模式』中有一個模式可以解釋特定的語法規則,它就是解釋器模式(Interpreter Pattern)。不同於常見的策略模式或者是工廠模式,解釋器模式在.NET或者JDK中並不常見,而且在業務上也很少會去解釋特定的語法,所以它並不被廣泛使用。一個解釋器可大可小,大可以是複雜的編譯器,小也可以是一 ...


『設計模式』中有一個模式可以解釋特定的語法規則,它就是解釋器模式(Interpreter Pattern)。不同於常見的策略模式或者是工廠模式,解釋器模式在.NET或者JDK中並不常見,而且在業務上也很少會去解釋特定的語法,所以它並不被廣泛使用。一個解釋器可大可小,大可以是複雜的編譯器,小也可以是一個簡單的字元串解析,但本質上它們都是對特定的語法做出合理的解釋。

解釋器在游戲領域的應用

雖然解釋器模式很少使用,但在在游戲開發中,還是很常見的。比如你在戰鬥時,普通攻擊和魔法攻擊一定會產生不同的傷害,游戲設計者會為技能設計不同的『公式』,簡單如我方的攻擊力-敵方的防禦力,同時『公式』還可以加入參數,如$critRate代表一個爆發率。故游戲的技能傷害如下圖所示:

游戲里的『公式』本質上是字元串,很像數學表達式,但又比它更高級,可以加入自定義的參數,所以『公式』更像是數學表達式的超集。既然談到了數學表達式,那麼有必要知道怎樣去解析一個數學表達式。

千萬不要小看這個任務,實際上要做一個計算器是非常複雜的。假設輸入一個字元串:-(1+(2+3)x4-5),註意這是一個字元串。解決方案有兩種:

  • while遍歷字元串,將括弧、運算符、數字等取出來,根據運算符左結合以及優先順序計算
  • 將表達式轉化成二叉樹形式,二叉樹的父節點是運算符,左右子節點代表數字,通過遞歸遍歷樹,將左右節點的數字運算之後放入父節點,直至到達根節點

很顯然第一種方式簡單直白,但很繁重,代碼的易讀性也不佳,第二種是目前最好的解決方式,將表達式轉化為二叉樹。所以難點在於怎樣將表達式轉化為一棵二叉樹?

這需要瞭解數據結構相關知識,表達式-(1+(2+3)x4-5)又被稱為中序排序,中序排序不能生成一棵二叉樹,你需要將中序排序轉化為前序排序或者後序排序,然後根據中序排序和前序排序生成二叉樹,相關演算法自行搜索,不做累贅。

我在閱讀了《編譯原理》第1,2章之後,還有另外一種方式將表達式生成二叉樹形式,這也是編譯的基本原理。

一個編譯器的前端模型

我們以最簡單的算術表達式舉例,編譯器在分析階段把一個字元序列分為各個組成部分,最終生成一棵抽象語法樹(abstract syntax tree),如下所示:

表達式語法定義

語法,顧名思義,是一種特定的描述方法。我們學習的英語語法,又或者是程式語言的語法,都有嚴格的格式要求。對於算術表達式而言,比如9-5+2,3-2語法是兩個數字之間必須出現+,-,如果出現9+-5,那麼這就是錯誤的語法

那我們怎麼來制定語法呢?在編譯原理領域,使用一個通用的表示方法來描述語法,這個方法就是上下文無關文法BNF範式

比如上述的算術(+和-)表達式:9-5+2,我們可以推導出如下BNF範式:

list->list+digit|list-digit|digit

digit->0|1|2|3|4|5|6|7|8|9

list代表一個表達式序列,digit代表數字,箭頭->可以讀作“可以具有如下形式”,而豎線|代表或的意思。

詞法分析器

詞法分析器讀入源程式中的字元序列,將他們組織為具有詞法含義的詞素,生成並輸出代表這些詞素的詞法單元(Token)

語法分析器

語法分析器根據詞法單元,以語法分析樹的形式構建表達式,最終形成一顆抽象的語法樹(abstract syntax tree),這是一種表示了層次化的結構。

語法分析樹

如果非終端節點A有一個產生式A->XYZ,那麼在語法分析樹中就可能有一個標號為A的內部節點,該節點有三個子節點,從左向右標號為X,Y,Z。內部節點對應於產生式的頭,它的子節點對應於產生式的體:

BNF範式構建

數學表達式的特點

運用編譯原理的知識,編寫一個自定義的解釋器,我們需要如下三個步驟:

  • BNF範式來描述游戲『公式
  • 詞法分析器獲得詞法單元Token,對應的類是LexicalAnalyzer
  • 語法分析器根據Token構建抽象樹,對應的類是Parser

我在一開始就提到過,游戲里的『公式』很像數學表達式,那麼數學表達式有什麼廣泛和通用的特點?

首先數學表達式由數字和運算符構成,並且運算符有左結合性和優先性:

  • 結合性:依照慣例,9+5+2等價於(9+5)+2,9-5-2等價於(9-5)-2。當一個運算分量,比如上述的5左右兩側都有運算符時,我們需要一些規則來決定哪個運算符被應用於該運算分量。我們說運算符“+”是左結合的,因為當一個運算分量左右兩側都有“+”號時,它屬於其左邊運算符。加,減,乘,除四種算術運算符都是左結合。
  • 優先性:在算術中,乘法和除法比加法和減法具有更高的優先順序。因此在表達式9+5x2和9x5+2中,都是運算分量5首先參與x運算。

算術表達式的BNF構建

通過對數學表達式的瞭解,我們知道一個數學表達式有數字、運算符等組成,並且運算符是左結合和有優先性,那怎樣去構建它的BNF範式呢?

我們創建兩個非終結符號expr(表達式)term(項) ,分別對應這兩個優先順序層次,並使用另一個非終結符號factor(因數)來生成表達式的基本單元。

那什麼是factor呢?

我們可以將因數(factor)理解成不能被任何運算符分開的表達式。『不能分開』的意思是說當我們在任意因數的任意一邊放置一個運算符,都不會導致這個因數的任何部分分離出來,成為這個運算符的運算分量。當然,因數本身作為一個整體可以成為該運算符的一個運算分量。如果這個因數是由一個括弧括起來的表達式,那麼這個括弧將起到保護其不被分開的作用。

factor->digit|(expr)

digit->0|1|2|3|4|5|6|7|8|9

那什麼是term呢?

一個(不是因數)項(term)是一個可能被高優先順序的運算符x和/分開,但不能被低優先順序運算符分開的表達式。

term->term x factor|term / factor|factor

那什麼是expr呢?

一個(不是因數也不是項)的表達式可能被任何一個運算符分開。

expr->expr+term|expr-term|term

因此最終得到的BNF範式是:

expr->expr+term|expr-term|term

term->term x factor|term/factor|factor

factor->digit|(expr)

使用這個BNF範式時,一個表達式就是一個由+或-分割開來的項(term)列表,而項是由x或者/分隔的因數(factor)列表。請註意,任何由括弧括起來的表達式都是一個因數。

這個BNF範式的語法分析樹為如下所示:

求值時,從root節點遍歷二叉樹,如果節點有子節點,遞歸的方式遍歷下去,直到是葉子節點為止,接著將左子樹和右子樹取得的值放入它們的根節點,最後root節點的值就是表達式最終的值。

開始實現解釋器

有了準備之後,接下來就是實現解釋器,它可以解釋游戲中的『公式』。

1.) 創建一個數學表達式類MathExpression,根據面向對象思想,它封裝了數據和行為,由於篇幅有限,只展示其骨架:

public class MathExpression
{
    private readonly string _expression;        
    public int CurrentIndex{}
    public bool IsIndexOutOfRange{}
    public bool IsEndOfString{}
    public char CurrentChar{}
    public char GetSpecificCharByIndex(int index){}
}

2.) 創建一個詞法分析器LexicalAnalyzer,獲取對應的詞法單元Token:

switch (_mathExpression.CurrentChar)
{
    case '+':
        token = Token.Add;
        _mathExpression.CurrentIndex++;
        break;
    case '-':
        token = Token.Sub;
        _mathExpression.CurrentIndex++;
        break;
    case '*':
        token=Token.Mul;
        _mathExpression.CurrentIndex++;
        break;
    case '/':
        token = Token.Div;
        _mathExpression.CurrentIndex++;
        break;
    case '(':
        token = Token.OParen;
        _mathExpression.CurrentIndex++;
        break;
    case ')':
        token = Token.CParen;
        _mathExpression.CurrentIndex++;
        break;
    case '$':
        if (_mathExpression.GetSpecificCharByIndex(_mathExpression.CurrentIndex + 1) =='c')
        {
            _mathExpression.CurrentIndex += 2;
            token = Token.Param;
        }
        else
        {
            _mathExpression.CurrentIndex++;
            token=Token.Illegal;
        }
        break;
    default:
        if (char.IsDigit(_mathExpression.CurrentChar))
        {
            token = GetDigitsFromString();
        }else if (char.IsLetter(_mathExpression.CurrentChar))
        {
            token = GetSineCosineFromString();
        }
        else
        {
            throw  new Exception("Illegal Token");
        }
        break;
}

3.) 值得一提的事情,怎樣從字元串中獲取數字,數字有兩種形式:整數和小數點形式,通過有窮自動機在不同的狀態間跳轉並記錄下數字的索引下標,直到遇到非數字退出,有窮自動機如下所示:

一個有窮自動機的狀態判斷代碼如下:

do
{
    isEndOfString = _mathExpression.IsEndOfString;
    currentChar = _mathExpression.CurrentChar;

    switch (_currentState)
    {
        case State.Init:
            if (char.IsDigit(currentChar))
            {
                _currentState = State.Integer;
                if (!isEndOfString)
                {
                    _mathExpression.CurrentIndex++;
                }
            }
            else
            {
               //Init狀態非數字則退出
               _currentState= State.Quit;
            }
            break;
        case State.Integer:
            if (currentChar == '.')
            {
                _currentState = State.Float;//輸入小數點,狀態轉移到Float
                if (!isEndOfString)
                {
                    _mathExpression.CurrentIndex++;
                }
            }
            else
            {
                if (!char.IsDigit(currentChar))//既不是數字也不是小數
                {
                    _currentState = State.Quit;
                }
                else
                {
                    if (!isEndOfString)
                    {
                        _mathExpression.CurrentIndex++;//讀取下一個字元
                    }
                }
            }
            break;
        case State.Float:
            if (!char.IsDigit(currentChar))//非數字,退出
            {
                _currentState = State.Quit;
            }
            else
            {
                if (!isEndOfString)
                {
                    _mathExpression.CurrentIndex++;
                }
            }
            break;
        case State.Quit:
            break;

    }
} while (_currentState != State.Quit && !isEndOfString);

4.)通過語法解析器Parser構建表達式樹,每個節點都是一個抽象Expression

public abstract class Expression
{
    public abstract double Evaluate(Context context);
}

Expression根據類型不同有常量表達式,二元表達式,一元表達式等,一個常見的二元表達式如下:

public class BinaryExpression:Expression
{
    private Expression _leftExpression;
    private Expression _rightExpression;
    private Operator _operator;

    public BinaryExpression(Expression leftExpression,Expression righExpression,Operator op)
    {
        _leftExpression = leftExpression;
        _rightExpression = righExpression;
        _operator = op;
    }
    public override double Evaluate(Context context)
    {
        switch (_operator)
        {
            case Operator.Plus:
                return _leftExpression.Evaluate(context) + _rightExpression.Evaluate(context);
            case Operator.Minus:
                return _leftExpression.Evaluate(context) - _rightExpression.Evaluate(context);
            case Operator.Mul:
                return _leftExpression.Evaluate(context) * _rightExpression.Evaluate(context);
            case Operator.Div:
                return _leftExpression.Evaluate(context) / _rightExpression.Evaluate(context);
        }
        return Double.NaN;
    }
}

可以看到左子樹和右子樹同樣是Expression

5.)到目前為止,可以說是萬事俱備,只欠東風了,這個『東風』就是怎麼樣去構建表達式樹。已知的是,一個 expr 就是一個由+或-分割開來的項( term )列表,而項是由x或者/分隔的因數( factor )列表。

expr->expr+term|expr-term|term

private Expression Expr()
{
    Token old;
    Expression expression = Term();
    while (_currentToken==Token.Add|| _currentToken==Token.Sub)
    {
        old = _currentToken;
        _currentToken = _lexicalAnalyzer.GetToken();
        Expression e1 = Expr();

        expression=new BinaryExpression(expression,e1,old==Token.Add?Operator.Plus:Operator.Minus);
    }
    return expression;
}

term->term x factor|term/factor|factor

private Expression Term()
{
    Token old;
    Expression expression = Factor();

    while (_currentToken==Token.Mul || _currentToken==Token.Div)
    {
        old = _currentToken;
        _currentToken = _lexicalAnalyzer.GetToken();

        Expression e1 = Term();
        expression=new BinaryExpression(expression,e1,old==Token.Mul?Operator.Mul:Operator.Div);
    }

    return expression;
}

factor->digit|(expr)

private Expression Factor()
{
    Token token;
    Expression expression;
    if (_currentToken==Token.Double)
    {
        expression=new NumericConstant(_lexicalAnalyzer.GetDigits());
        _currentToken = _lexicalAnalyzer.GetToken();
    }
    else if (_currentToken == Token.Param)
    {
        expression=new Var();
        _currentToken = _lexicalAnalyzer.GetToken();
    }
    else if (_currentToken==Token.OParen)
    {
        _currentToken = _lexicalAnalyzer.GetToken();
        expression = Expr();
        if (_currentToken!=Token.CParen)
        {
            throw new Exception("Missing Closing Parenthesis\n");
        }
        _currentToken = _lexicalAnalyzer.GetToken();
    }
    else if(_currentToken==Token.Add || _currentToken==Token.Sub)
    {
        var old = _currentToken;
        _currentToken = _lexicalAnalyzer.GetToken();
        expression = Factor();

        expression=new UnaryExpression(expression,old==Token.Add?Operator.Plus:Operator.Minus);

    }
    else
    {
        throw new Exception("error");
    }
    return expression;
}

最後生成的樹結構如下所示:

小結

本文為大家介紹了怎樣從編譯原理的角度來實現一個解釋器。在游戲領域,需要解釋器來解釋自定義的『公式』。這個『公式』的語法往往是和上下文無關的,又被稱為BNF範式。解釋器的核心就是怎樣構建一棵抽象的表達式樹,這需要詞法分析和語法分析的相關知識。

參考代碼如下:https://github.com/MEyes/uInterpreter


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

-Advertisement-
Play Games
更多相關文章
  • jdk1.7.0_79 本文實際上是對上文《13.ThreadPoolExecutor線程池之submit方法》的一個延續或者一個補充。在上文中提到的submit方法里出現了FutureTask,這不得不停止腳步將方向轉向Java的Future模式。 Future是併發編程中的一種設計模式,對於多線 ...
  • 一、註入分類 Bean實例在調用無參構造器創建空值對象後,就要對Bean對象的屬性進行初始化。初始化是由容器自動完成的,稱為註入。根據註入方式的不同,常用的有兩類:設值註入、構造註入、實現特定介面註入。由於第三種方式採用侵入式編程,污染代碼,所以幾乎不用。 1、設值註入 2、構造註入 二、命名空間註 ...
  • 題目描述 有N(2<=N<=600000)塊磚,要搭一個N層的塔,要求:如果磚A在磚B上面,那麼A不能比B的長度+D要長。問有幾種方法,輸出 答案 mod 1000000009的值. 輸入輸出格式 輸入格式: 第一行: N,D 第二行: N個數,表示每塊磚的長度。 輸出格式: 方案數,輸出要mod ...
  • 個人的一點參考總結,如有雷同,純屬巧合! 1、hashmap的實現原理以及hashtable的線程安全是怎麼實現的?HashMap其實也是一個線性的數組實現的,所以可以理解為其存儲數據的容器就是一個線性數組。首先HashMap裡面實現一個靜態內部類Entry,其重要的屬性有 key , value, ...
  • 1,NodeJS 安裝阿裡大於模塊 切換到項目目錄使用npm 安裝阿裡於模塊 npm i node-alidayu --save 2,aliyu官網使用淘寶賬戶登錄 登錄阿裡大於 https://doc.alidayu.com/doc2/index.htm 1登錄後點擊管理中心 2點擊應用管理 》創 ...
  • 很多新手引用Boost庫編程,在ubuntu下編譯時候有時候會出現如下錯誤: test04.cpp:(.text+0x2c): undefined reference to `boost::program_options::options_description::m_default_line_le ...
  • 題目背景 戰爭已經進入到緊要時間。你是運輸小隊長,正在率領運輸部隊向前線運送物資。運輸任務像做題一樣的無聊。你希望找些刺激,於是命令你計程車兵們到前方的一座獨木橋上欣賞風景,而你留在橋下欣賞士兵們。士兵們十分憤怒,因為這座獨木橋十分狹窄,只能容納一個人通過。假如有兩個人相向而行在橋上相遇,那麼他們兩個 ...
  • 把下麵的配置複製到 .m2/settings.xml配置文件中。 github地址:https://github.com/ae6623/Zebra/blob/master/maven-repo-settings-ali.xml 阿裡maven倉庫地址:http://maven.aliyun.com/ ...
一周排行
    -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.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...