跳轉到內容

Irony - 語言實現工具包/簡介

來自 Wikibooks,開放世界中的開放書籍

Irony 基礎教程


在本教程中,我將逐步介紹如何建立一個名為“GridWorld”的簡單語言語法。請記住:首先,本教程並不打算提供有關 Irony 工具的詳盡知識,其次,強烈建議使用 Irony 下載中的示例專案。

 

第 1 章 -- GridWorld

I. GridWorld 簡介

我們將從建立一個簡單的語言開始,該語言描述了具有特定高度和寬度的網格,從網格中的某個位置開始,並在網格中移動。以下是一些可能的 GridWorld 語言原始碼:

 

建立一個 10x10 的網格。

從位置 1,1 開始。                                          (1)

向下移動 3 格。                                          (2)

向右移動 3 格。                                          (3)

向上移動 1 格。                                          (4)

 

以下是結果,假設 (0,0) 是 (行, 列) 的形式,並且是最左上角的方塊。

 


0

0

0

0

0

0

0

0

0

0

0

1

0

0

0

0

0

0

0

0

0

2

0

0

0

0

0

0

0

0

0

2

0

0

4

0

0

0

0

0

0

2

3

3

3

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

0

 

您可能已經推斷出,在不使用任何解析工具的情況下解決這個問題並不困難。我們語言的前兩行在每一個程式中都是一樣的,只是“10x10”和“1,1”中的數字不同,接下來的幾行非常重複。也許如果程式設計師被分配到 驗證此玩具語言的語法,程式會變得更復雜。即使如此,在這種情況下,這也將是一個相對容易的任務。

 

但是,考慮為 Python 或 C# 等更復雜的語言編寫相同的程式,儘管並非不可能,但從頭開始使用原始方法將需要大量時間。Irony 提供了一種強大的方法來解析語言結構。這個 GridWorld 玩具問題是一個簡單的基礎,可以幫助您學習如何在未來處理更復雜的語法。

 

II. 建立 GridWorld 語法類

使用 Irony 解決此問題的第一步是建立一個語法類。此類將充當解析原始碼的通用模式。首先,我們需要獲取 Irony 庫。

 

1.     下載 Irony。訪問 https://github.com/IronyProject/Irony 並獲取最新版本。

2.     開啟主專案並在 Visual Studio 中構建它。

3.     這將在“irony-XXXX/Irony/bin/Debug”中生成 Irony.dll。

將 Irony.dll 新增到您的專案中。

 

現在,建立一個擴充套件 Irony.Grammar 的類

 

using Irony.Parsing;

 

public class GridWorldGrammar : Grammar

{

public GridWorldGrammar() {}

}

 

此時,我們可以選擇使用區分大小寫。Grammar 類有一個建構函式,它接受一個布林值來定義此特性,因此我們可以使用以下程式碼來支援諸如“moVE RigHT 1”之類的短語(預設情況下,語法 區分大小寫的)

 

public GridWorldGrammar() : base(false) {}

 

在這個類中,我們使用 BNF 定義 GridWorld 語言的規則。我們的語言結構包含 3 個部分:建立語句、開始語句和一個或多個移動語句。我們將包含所有這三個部分的實體稱為 程式createStatement 由“Create”和“a”以及數字、"by"、另一個數字、"grid" 和句點定義。 moveStatement 包含 方向 數字。我們語言中的所有其他內容都以類似的方式定義。在 BNF 中,這是…

 

程式   :==   createStatement   startStatement   moveStatements

createStatement   :==   “Create”   “a”   number   “by”   number   “grid”   “.”

startStatement   :==   “Start”   “at”   “location”   number   “,”   number   “.”

moveStatements   :==   moveStatement +

moveStatement   :==   “Move”   direction   number   “.”

direction   :==   “up”   |   “down”   |   “right”   |   “left”

 

BNF,即巴克斯-諾爾正規化,是 Irony 用於理解語法定義的重要表示法。如果您不熟悉它,不用擔心,它很容易學習。網上有很多快速教程,而且 Google 是您的好朋友。

 

幸運的是,透過 Irony 使用覆蓋運算子,我們可以將此 BNF 描述幾乎直接地轉換為 C# 語法。首先,我們必須定義終端和非終端。終端通常是預定義的結構,例如數字或字串。這裡,我們唯一的終端是數字,我們將根據正則表示式將其定義為“一個或多個數字的序列”。

 

RegexBasedTerminal number = new RegexBasedTerminal("number", "[0-9]+");

 

我們語言中的所有其他內容都以類似的方式定義為非終端。

 

NonTerminal program = new NonTerminal("program"),

createStatement = new NonTerminal("createStatement"),

startStatement = new NonTerminal("startStatement"),

moveStatements = new NonTerminal("moveStatements"),

moveStatement = new NonTerminal("moveStatement"),

direction = new NonTerminal("direction");

 

NonTerminal 建構函式中的每個字串在我們遍歷解析樹時都會很重要。現在我們可以將 BNF 語句轉換為 Irony 可以理解的內容。以下是我們語言的實際內容。

 

program.Rule = createStatement + startStatement + moveStatements;

 

這應該看起來很熟悉;這是我們 BNF 規則中的第一個語句。

this+that 直觀地意味著“thisthat.” 類似地,this|that 意味著“this that” 正如 BNF 中一樣。

 

createStatement.Rule = ToTerm("Create") + "a" + number + "by" + number

+ "grid" + ".";

startStatement.Rule = ToTerm("Start") + "at" + "location" + number + ","

+ number + ".";

 

只要字串文字是規則語句中使用的第一個元素,如上面的兩個規則,最好在字串周圍放置一個 ToTerm() 函式。這不是必需的,但如果不使用 ToTerm,則會發生錯誤的解析。

 

moveStatements.Rule = MakePlusRule(moveStatements, moveStatement);

moveStatement.Rule = ToTerm("Move") + direction + number + ".";

direction.Rule = ToTerm("up") | "down" | "right" | "left";

 

不幸的是,沒有一種非常簡單的方法來描述“一個或多個”和“零個或多個”規則。這些分別被稱為“加”和“星”規則,它們源於 Regex 表示式中的類似概念。Irony 使用 MakePlusRule 和 MakeStarRule 函式來定義它們。兩者在語法上都以相同的方式使用,MakePlusRule 可以從上面的 moveStatements 規則中看到。


還需要一個語句來完成語法

 

this.Root = program;

 

以及另一個將有助於保持樹“乾淨”的語句

 

MarkPunctuation("Create", "a", "grid", "by", "Start", "at",

"location", ",", ".", "Move");

 

我們將在第四部分更詳細地討論最後一個語句。就這樣!我們已經充分定義了 GridWorld 語法以解析 GridWorld 程式。有關完整的 GridWorldGrammar 類的資訊,請參閱附錄。

 

三. 解析原始碼並驗證語法

假設我們有一個 GUI,其中包含一個文字框、一個按鈕和一個標籤。我們的文字框將允許使用者鍵入或貼上以 GridWorld 語法表示的原始碼。一旦使用者將程式碼放在那裡,他們按下按鈕,標籤就會指出語法是否為正確形式。

 

使用 Irony,這是一個非常簡單的操作。以下是一個函式,它返回給定原始碼與給定語法物件是否有效。

 

public bool isValid(string sourceCode, Grammar grammar)

{

      LanguageData language = new LanguageData(grammar);

      Parser parser = new Parser(language);

      ParseTree parseTree = parser.Parse(sourceCode);

      ParseTreeNode root = parseTree.Root;

      return root != null;

}

 

該函式使用的方法很簡單:嘗試使用給定語法的規則解析給定的原始碼。如果此嘗試失敗,則原始碼無效。否則,它是有效的。在這種情況下,我們知道解析失敗是因為解析樹的根不存在(為 null 值)。

 

顯然,實現此程式碼的方法有很多,就像在上一節中描述 GridWorld 語言的方法有很多一樣。強烈建議探索其他可能性,加深對 Irony 工作原理的理解,從而充分利用 Irony 工具。

 

四. 遍歷語言樹並驗證內容

我們已經瞭解了 Irony 如何提供一種簡單而快速的方法來驗證語法。現在我們將看看如何檢查解析的語言樹的內容。以下程式碼是一個函式,它返回解析的語言樹的根。如您所見,它看起來與上面的 isValid 函式非常相似。

 

public ParseTreeNode getRoot(string sourceCode, Grammar grammar)

{

      LanguageData language = new LanguageData(grammar);

      Parser parser = new Parser(language);

      ParseTree parseTree = parser.Parse(sourceCode);

      ParseTreeNode root = parseTree.Root;

      return root;

}

 

透過使用此函式,我們可以以健壯的方式探索和顯示原始碼。我們將使用以下函式在 DOS 螢幕中顯示樹

 

public void dispTree(ParseTreeNode node, int level)

{

for(int i = 0; i < level; i++)

Console.Write("  ");

Console.WriteLine(node);

 

foreach (ParseTreeNode child in node.ChildNodes)

            dispTree(child, level + 1);

}

 

現在,讓我們解析一些程式碼並比較輸出。回想一下我們在第三部分語法結尾使用的這個語句

 

MarkPunctuation("Create", "a", "grid", "by", "Start", "at",

"location", ",", ".", "Move");

 

如果我們沒有包含這個語句,從圖 1 中的原始碼解析出來的樹將看起來像圖 2 中的樹。然而,由於我們添加了這個語句來“幫助保持樹的整潔”,解析出來的樹看起來像圖 3。正如你所見,MarkPunctuation 是一個非常實用的函式。

 


建立一個 10x10 的網格。

從位置 1,1 開始。

向下移動 3。

program

  createStatement

    Create (關鍵字)

    a (關鍵字)

    10 (數字)

    by (關鍵字)

    10 (數字)

    grid (關鍵字)

    . (關鍵符號)

  startStatement

    Start (關鍵字)

    at (關鍵字)

    location (關鍵字)

    1 (數字)

    , (關鍵符號)

    1 (數字)

    . (關鍵符號)

  moveStatements

   moveStatement

     Move (關鍵字)

     direction

        down (關鍵字)

     3 (數字)

      . (關鍵符號)

program

  createStatement

    10 (數字)

    10 (數字)

  startStatement

    1 (數字)

    1 (數字)

  moveStatements

   moveStatement

     direction

        down (關鍵字)

     3 (數字)

圖 1                                                                        圖 2                                                                                          圖 3

 

注意:此樹中的名稱並非源於我們在定義語法時使用的 Terminal 和 NonTerminal 變數的名稱,而是源於在宣告每個變數時傳送給每個變數建構函式的字串引數。

 

現在,有許多方法可以去讀取這棵樹併產生一些輸出。請注意,這意味著你的語言是一種解釋型語言,而不是編譯型語言;你必須編寫直譯器。我編寫了一個函式,它使用圖 3 中的樹格式來建立一個類似於第一部分中的網格。請參見附錄或本教程附帶的示例程式碼。

 

檢查內容以及驗證語法是驗證語言原始碼的重要組成部分。這是因為能夠確定哪些程式碼部分是錯誤的或正確的,可以讓開發者與使用者進行溝通,通常透過語法高亮顯示。

 

例如,我為從語言樹顯示網格而編寫的函式,如果使用者編寫超出網格的 GridWorld 程式碼,或者告訴直譯器從網格上不存在的位置開始,就很容易被破壞。這些錯誤當然不會被上面看到的語法驗證函式捕獲;它們實際上是執行時錯誤。開發者可以決定檢查這些型別的錯誤,然後將有問題的數字突出顯示為紅色。因此,透過使用內容檢查器可以輕鬆避免使用者的困惑和沮喪。

 

這就是使用 Irony 建立一個簡單的特定領域語言解析器所需的全部內容!請檢視程式碼附錄一或附帶的原始碼,以檢視過去幾節中討論的內容。

 

下一部分將討論如何使用 Irony 的一些更高階的工具,然後我將講解如何維護一種稍微複雜一些的語言,名為“Manchester Syntax”,它是在 2010 年夏天建立的。

 

V. Irony 的工具

有關如何使用 Irony 的語法資源管理器,請參閱從 http://irony.codeplex.com/ 下載的 irony-XXXXX 資料夾中的 README.txt 檔案。

華夏公益教科書