跳转到内容

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

华夏公益教科书