Irony - 語言實現工具包/簡介
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 直觀地意味著“this 在 that.” 類似地,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 檔案。