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 文件。