65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 的新解析器生成器

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.96/5 (68投票s)

2013年10月7日

LGPL3

25分钟阅读

viewsIcon

330198

downloadIcon

1360

LLLPG,Loyc LL(k) 解析器生成器:现在解析 C# 本身!

引言

LLLPG (Loyc LL(k) 解析器生成器) 是一个相对较新的 C# 递归下降解析器生成器,其功能集优于 ANTLR 版本 2。

在本文中,我假设您已经知道什么是解析器词法分析器;如果不知道,请点击链接。如果您以前没有编写过解析器,文章 #2 将补充您的知识。

LLLPG 是我在尝试使用 ANTLR 的 C# 模块并遇到无法克服的 C# 特有错误后决定创建的一个系统。此外,我对 ANTLR 生成的代码不满意;我认为我可以生成更简单、更高效的代码。“制作一个 LL(k) 解析器生成器有多难?”我好奇地想。答案是:实际上相当难。

制作 LLLPG 花了我比预期更长的时间(什么,5年?),但是……迟到总比没有好,对吧?虽然 ANTLR 在那段时间里取得了一些进步,但它仍然以 Java 为中心,我认为 LLLPG 的优势仍然值得考虑,即使 ANTLR 中所有主要的 C# 特有错误都已修复(我不知道是否已修复,但 C# 版本仍然落后于 Java 版本)。

通常,您将使用 LLLPG Visual Studio 自定义工具(又名单文件生成器)

LLLPG 不是像 ANTLR 那样的专用工具。相反,LLLPG 旨在嵌入到另一种编程语言中。虽然您可以像使用其他解析器生成器一样使用 LLLPG,但它实际上只是我正在制作的名为 Enhanced C# 的编程语言中的一个“宏”——您可能会使用的数百个宏之一,也许将来您会自己编写一两个宏。

截至 2016 年,Enhanced C# 尚未完成;只有它的两个组件准备就绪(解析器和名为 LeMP(词法宏处理器)的宏运行器)。但希望您会发现它相当用户友好和有趣。

更新LLLPG 现在有自己的主页,有关它的文章将发布在那里。从现在开始,CodeProject 文章将不再更新

LLLPG 的其他设计元素包括

  • LL(k) 及其优点。 有几种类型的解析器生成器,例如 LALR(1)、PEG、LL(1)、LL(k) 和 LL(*)。其中,我认为 PEG(解析表达式语法,通常用 Packrat 解析器实现)和 LL(k)/LL(*)(ANTLR 2 和 ANTLR 3/4 分别)是当今编写新语法最受欢迎的(有些人也使用正则表达式,但正则表达式比“适当的”解析器生成器功能弱得多,因为它们不支持完全递归)。除了纯 LL(k) 之外,LLLPG 还有一些额外的、高级的功能,因为一些编程语言仅用 LL(k) 语法很难表达。LL(k) 有两个主要优点:潜在的高性能(尤其是当 k 降低时),以及相对易于理解的输出。说实话,ANTLR 3/4 比 LLLPG 更强大,因为先行值 k 是无限的,但无限先行并非免费;如果您的目标是编写一个快速解析器,您无论如何都可能会将自己限制在 LL(k)。在 LLLPG 中,您仍然可以使用零宽度断言进行无限先行,只是它不是自动的;您必须要求它。
  • 简单、简洁的输出。最小的运行时包袱。 LLLPG 尝试生成轻量级解析器,类似于您手动编写的(略微冗长,但还不错)。我没有尝试过 ANTLR 4,但 LLLPG 通常比 ANTLR 3 生成更简单的输出。此外,与 ANTLR 不同,LLLPG 被设计为不需要运行时库;它只需要一个您可以从我这里复制或自己重写(如果您喜欢重复发明轮子)的单个基类(实际上在实践中是两个,一个用于词法分析器,一个用于解析器)。
  • 速度重于美观。 LLLPG 试图生成易于理解的代码,但它有选择地使用 gotoswitch 语句以最大化性能。
  • 无异常。 上次我使用 ANTLR 时,它仍然依赖异常进行回溯。LLLPG 不会;实际上,它根本不进行回溯,除非您使用语法谓词,这会创建特殊的、回溯的子解析器,称为“识别器”。
  • 专注于预测。 LLLPG 旨在专注于一项工作并尽可能做好:LL(k) 预测分析。LLLPG 不会尝试为您做所有事情:它不构建令牌,它不创建语法树。您是一名程序员,并且已经拥有一种编程语言;所以我假设您足够了解如何设计自己的 Token 类和语法树类。如果我为您设计和构建语法树,我想我只会增加学习曲线:您不仅要学习如何使用 LLLPG,还要学习我的类库!不,LLLPG 的主要目标是消除手动编写 LL(k) 解析器中最困难且容易出错的部分:弄清楚要选择哪个分支,或要调用哪个方法。LLLPG 仍然让您负责其余部分。话虽如此,我确实设计了一个通用语法树,作为 Loyc 项目的一部分,称为 Loyc 树LNode),但 LLLPG 并非旨在帮助您使用它们。即便如此,我希望您会考虑使用 Loyc 树。在内部,LLLPG 的实现大量使用它们。还有一个标准 Token 类型。
  • 速度与功能平衡。 LLLPG 支持 LL(k)、零宽度断言和其他功能,使其比直接只支持 LL(1) 的 Coco/R 更强大。它不如 ANTLR 强大,后者支持 LL(*) 及更高级别,但通过编写 LL(k) 语法而不是 LL(*) 语法,理论上您可以获得更快的解析器。我无法在功能上与 ANTLR 竞争;Terrance Parr 编写 ANTLR 及其前身已经快 20 年了,我想。但是,您可能不需要所有那些高级功能;我相信我选择的功能集足以满足所有现代语言的需求,尽管它并不能让所有事情都变得简单。我在 LLLPG 中编写了 EC# 解析器这一事实证明了它的实际价值。

“废话少说!快给我看看这东西!”

好的,让我们开始吧!这是一个非常简单的例子(EC#)

LLLPG (lexer) {
    public rule Digit @[ '0'..'9' ];
    public rule Integer @[ Digit+ ];
};  

输出是:

public void Digit()
{
    MatchRange('0', '9');
}
public void Integer()
{
    int la0;
    Digit();
    for (;;) {
      la0 = LA0;
      if (la0 >= '0' && la0 <= '9')
        Digit();
      else
        break;
    }
} 

就是这样!所以这里有一些相关的知识点(我喜欢项目符号列表,你不喜欢吗?我希望人们多用一些!)

  • 首先,为了使这个例子简单扼要,我没有使用任何“using”语句,也没有将这段代码封装在 namespaceclass 中。我的小迷你语言不关心这些;输出反映输入,所以输出同样不会有任何“using”语句,也不会被封装在 classnamespace 中。垃圾进,垃圾出。如果你想让输出被封装在一个类声明中,你必须将输入封装在一个类声明中。
  • 语法必须封装在 LLLPG 块中。对于词法分析器,使用“LLLPG (lexer)”,对于解析器,使用“LLLPG (parser)”。两者的区别在于对终结符(字符或标记)的处理方式
    • 词法分析器模式只理解整数和字符输入,但针对这种输入进行了优化。它不接受命名常量,只接受字面数字和字符,因为它无法判断名称可能指的是哪个数字(编辑:现在有一个用于命名常量的 alias 语句)。此模式假设 -1 表示 EOF(文件结束)。请注意,先行变量的类型是 int,而不是 char,因为 char 无法保存 -1,即 EOF 的表示。幸运的是,C# 并不介意(char 会隐式转换为 int,尽管反之则不行)。
    • 解析器模式不理解数字,只理解符号。理论上,解析器模式也可以用于词法分析器;主要问题是它不理解范围运算符 ..,因此您必须编写诸如 '0'|'1'|'2'|'3'|'4'|'5'|'6'|'7'|'8'|'9' 这样的怪物,而不是 '0'..'9'。真糟糕。解析器模式要求您定义一个名为 EOF 的 C# 符号来表示文件结束。在解析器模式下,如果没有名为 Foo 的规则,则名为 Foo 的符号被假定为终结符。
  • 每个规则都会转换为一个同名的方法。规则上的属性,例如 publicunsafe(你为什么在解析器中使用不安全代码,小聪明?)都会转移到输出中。规则支持一些属性,例如 [FullLLk],它们由 LLLPG 本身理解并从输出中去除。private 属性具有特殊含义,并将导致与未标记为私有的规则略有不同的解释。
  • LLLPG 要求您定义一个名为 LA0 的属性,该属性返回当前输入字符;它还期望一个 LA(int k) 方法用于其他先行值。作为优化,LLLPG 将 LA0 缓存到变量 (la0) 中,以防 LA0 是虚方法调用或类似的东西。
  • 词法分析器模式需要存在一个名为 MatchRange() 的方法(两种模式都期望一系列 Match() 方法用于匹配特定字符或令牌)。此方法的任务是测试输入字符是否匹配指定范围,如果不匹配则发出错误消息。不匹配时,您可以抛出异常,打印消息,或做任何适合您的操作。成功时,MatchRange() 应返回被消耗的字符,以便您可以将其存储在变量中(如果需要)。
  • + 运算符表示“一个或多个”。Digit+ 完全等同于 Digit Digit** 表示“零个或多个”,* 运算符被转换为一个 for 循环,如您在输出中看到的(您可能知道,for (;;) 表示“无限循环”;它等同于 while (true))。
  • 这个例子还展示了 LL(k) 解析器的主要特征:预测。if (la0 >= '0' && la0 <= '9')语句正在执行一个名为“预测”的任务,这意味着它正在决定要走哪个分支(是 Digit 吗?还是退出循环?)。它必须跨越规则来完成这项工作:每个规则都需要对其调用的所有其他规则进行分析,此外还需要对规则本身进行分析。在这种情况下,Integer 必须对 Digit 的内容非常熟悉。想想看,这有点浪漫。
  • 规则的主体包含在 @[...]; 中。为什么不是大括号或括号?因为 LLLPG 嵌入在另一种编程语言中,它不能改变宿主语言的语法。构造“public rule Foo @[...];”实际上被 EC# 解析为属性声明,只是用 @[...] 代替了通常的 {...}@[...] 是一种称为令牌字面量的东西,它是一个令牌列表(实际上是一个,匹配成对的 ( ) { } [ ])。EC#(或 LES)解析器收集所有令牌并存储它们以备后用。解析完整个源文件后,宏处理器让 LLLPG 有机会接收令牌树并将其转换为其他内容。LLLPG 运行其自己的独立解析器来处理令牌树。最后,它用生成的普通 C# 代码替换 LLLPG 块。我稍后会更详细地解释这一点。

另一个例子

我的下一个例子几乎很有用:一个简单的词法分析器。

using System;

enum TT { // Token types
    Integer, Identifier, Operator
}
LLLPG (lexer) {
    private token Letter @[ ('a'..'z'|'A'..'Z') ];
    private token Id  @[ (Letter|'_')
                         (Letter|'_'|'0'..'9')* ];
    private token Int @[ '0'..'9'+ ];
    private token Op  @[ '+'|'-'|'*'|'/' ];
    public token TT Token @
      [  Op  {return TT.Operator;}
      |  Id  {return TT.Identifier;}
      |  Int {return TT.Integer;}
      ];
};  

在这个例子中,`using System` 和 `enum` 是完全标准的 C# 代码,它将原样打印出来(好吧,它会被重新格式化。空格和注释不会保留。)其余部分是增强 C# 和 LLLPG 代码的混合。

与 ANTLR 生成文本输出并将大括号 `{...}` 中的动作视为纯文本不同,LLLPG 完全解析其输入,并以 Loyc 树而非文本的形式生成输出。一个独立的库负责将 Loyc 树格式化为 C# 代码(我欢迎志愿者为 C++ 和 Java 等其他语言编写输出库。您不仅将帮助 LLLPG 本身,还将帮助整个 Loyc 项目!如果您有兴趣,请告诉我。)

这是生成的代码

using System;
enum TT
{
    Integer, Identifier, Operator
}
void Letter()
{
    Skip();
}
void Id()
{
    int la0;
    la0 = LA0;
    if (la0 >= 'A' && la0 <= 'Z' || la0 >= 'a' && la0 <= 'z')
      Letter();
    else
      Match('_');
    for (;;) {
      la0 = LA0;
      if (la0 >= 'A' && la0 <= 'Z' || la0 >= 'a' && la0 <= 'z')
        Letter();
      else if (la0 == '_')
        Skip();
      else if (la0 >= '0' && la0 <= '9')
        Skip();
      else
        break;
    }
}
void Int()
{
    int la0;
    MatchRange('0', '9');
    for (;;) {
      la0 = LA0;
      if (la0 >= '0' && la0 <= '9')
        Skip();
      else
        break;
    }
}
void Op()
{
    Skip();
}
public TT Token()
{
    int la0;
    la0 = LA0;
    switch (la0) {
    case '*':
    case '+':
    case '-':
    case '/':
      {
        Op();
        return TT.Operator;
      }
      break;
    default:
      if (la0 >= 'A' && la0 <= 'Z' || la0 == '_' || 
                la0 >= 'a' && la0 <= 'z') {
        Id();
        return TT.Identifier;
      } else {
        Int();
        return TT.Integer;
      }
      break;
    }
}

这个例子演示了一些新事物

  • 请注意,Letter()Op() 方法只是调用 Skip(),这意味着“前进到下一个输入字符”。这是因为 LLLPG 已经分析了语法并检测到在所有调用 Letter()Op() 的地方,调用者已经验证了输入!因此 Letter()Op() 不必检查输入,它已经保证是正确的。这是一种称为预匹配分析的优化。注意:为了使此优化生效,规则必须显式标记为私有;否则,LLLPG 假定该规则可能由语法之外的代码调用。
  • 同样,有一些语句如 if (la0 == '_') Skip(); 而不是 if (la0 == '_') Match('_');。用户提供的 Match(x) 方法必须检查 LA0 是否与 x 匹配,但 Skip() 可以跳过检查。
  • 大括号中的 return 语句称为动作。LLLPG 解析所有动作,但重要的是要理解 LLLPG不理解它们。例如,我使用了 return 语句,但 LLLPG 不理解 return 语句的作用,并且在语法分析中不考虑它。在这种情况下,return 语句会提前从方法返回,而 LLLPG 不知道这一点。但在这种情况下,LLLPG 不需要知道这一点(因为如果它知道,它无论如何都会生成相同的输出),所以使用 return 语句是安全的。然而,在更复杂的情况下,您可能会通过执行意外的控制流而在解析器中引入错误。因此,请明智地使用它。
  • 我用 `token` 代替了 `rule`。实际上,在这个例子中,`rule` 和 `token` 之间没有任何区别;两者生成相同的代码。我想现在介绍 `token` 是错误的,因为我还没有展示出区别。总之,有这种“token”模式,它被称为 `token` 是因为它通常在词法分析器中使用比解析器更有意义。但是,呃,你也可以在解析器中使用它。我不知道,也许它需要一个新的名字。
  • LLLPG 会在认为代码可能更高效时使用 `switch` 语句。这里它使用 `switch()` 来匹配 `Op()`。然而,它试图在代码大小和速度之间取得平衡。它不为 `Id()` 使用 `switch` 情况,因为它需要 53 个“`case`”标签来匹配它(26 个大写 + 26 个小写 + `'_'`),这似乎过多。

LeMP 处理模型

如果您只对解析器生成器感兴趣,请跳过此部分,因为我现在想讨论 LLLPG 所基于的精妙技术。事实上,您可以跳过文章的其余大部分内容,直接转到第 2 部分

事情是这样的。我设计了一种名为 Enhanced C# 的语言,它现在才开始成形。这种语言旨在与 C# 大约 99.7% 向后兼容,并且解析器大约完成了 93%(例如,缺少 LINQ 支持)。目前还没有 EC# 编译器,但有一个打印机。只需几行代码,您就可以解析一段 EC# 代码并再次打印出来

// The using statements change the default printer and parser (the default
// is LES, so change to EC#).
using (LNode.PushPrinter(Ecs.Parser.EcsLanguageService.Value.Printer))
using (ParsingService.PushCurrent(Ecs.Parser.EcsLanguageService.Value))
{
    string input = @"{ your_code_here(Any_EC#_code_will_work); }";
    LNode code = ParsingService.Current.ParseSingle(input, MessageSink.Console);
    string output = code.ToString();
}

由于 EC# 是 C# 的超集,LLLPG 能够通过使用 EC# 打印机生成 C# 代码,只要它只使用 C# 语言的子集。

前面您看到了一张 LINQPad 的截图,其中 LLLPG 的输入是 LES 代码。LES 不是一种编程语言,它只是一种语法,别无其他。Loyc 项目的核心思想之一是将编程语言“模块化”为一系列可重用的组件。因此,编译器不是为一种语言编写一个大型编译器,而是通过混合和匹配组件来构建。其中一个组件是 Loyc 树Loyc.Syntax.dll 中的 LNode)。另一个组件是 LES 解析器(它是 Loyc 树的文本表示)。第三个组件是 EC# 解析器,第四个组件是 EC# 打印机,第五个组件是 LeMP,宏处理器

引导 LLLPG

为了让 LLLPG 支持 EC#,我需要一个 EC# 解析器。但是如何为 EC# 创建一个解析器呢?显然,我想使用 LLLPG 来编写解析器,但是如果没有解析器,就无法轻松地向 LLLPG 提交语法!在编写了 LLLPG 核心引擎和 EC# 打印机之后,我做了以下事情来创建 EC# 解析器

  1. 我使用 C# 运算符重载和辅助方法作为在纯 C# 中编写 LLLPG 解析器的基本方式(示例测试套件)。
  2. 以这种方式编写解析器非常笨拙,所以我决定不能以这种方式编写整个 EC# 解析器。相反,我设计了一种在语法上比 EC# 简单得多的新语言,称为 LES。这种语言不仅将作为 LLLPG 的原始输入语言,而且将作为通用的语法树交换格式——一种表示任何编程语言语法树的方式:“代码的 XML”。
  3. 我通过在纯 C# 中以编程方式调用 LLLPG(运算符重载等)编写了 LES 的词法分析器和解析器。
  4. 我编写了 MacroProcessor(我后来将其命名为“LeMP”,即“词法宏处理器”的缩写)以及一个提供命令行接口的包装类 名为 CompilerMacroProcessor 的作用是扫描语法树,查找对“宏”的调用,宏是源代码转换器(稍后详细介绍)。它递归地调用这些转换器,直到代码中不再有宏调用。最后,Compiler 将结果打印为文本。
  5. 我在 LES 之上构建了一个小型“宏语言”,它将 LeMP(宏处理器)与一组小宏结合起来,使 LES 看起来很像 C#。这些宏旨在将 LES 转换为 C#(您可以直接在 LES 中编写 C# 语法树,但它们有点丑)。
  6. 我编写了一些额外的宏,允许您从 LES 中调用 LLLPG。
  7. 我修改了 LES 解析器,使其也能在派生类中解析类似 @[ a* | b ] 的 LLLPG 代码(滥用“可重用代码”的耻辱)。
  8. 我编写了 LES 的词法分析器和解析器,用 LES 本身实现
  9. 我于(2013 年 10 月)发表了这篇文章。
  10. 我用 LES 编写了 EC# 的词法分析器和解析器,在此过程中发现了 LLLPG 中的一堆新 bug(我已修复)
  11. 我添加了一个额外的宏(ECSharpRule)以支持 EC# 中的 LLLPG。
  12. 最后,为了测试 EC# 解析器,我用 EC# 重写了 LLLPG 的语法
  13. 我更新了这篇文章(2014 年 2 月)。

终于,引导完成,你可以在 EC# 中编写 LLLPG 解析器了!

宏是 LISP 的一个基本特性,我正在将其移植到更广阔的非 LISP 语言世界。

宏(在 LISP 意义上,而非 C/C++ 意义上)只是一个方法,它接受一个语法树作为输入,并生成另一个语法树作为输出。这是一个宏的例子

[SimpleMacro("Name::Type", 
  "Defines a variable or field in the current scope.", "#::")]
public static LNode ColonColon(LNode node, IMacroContext context)
{
    var a = node.Args;
    if (a.Count == 2) {
        LNode r = node.With(S.Var, a[1], a[0]);
        r.BaseStyle = NodeStyle.Statement;
        return r;
    }
    return null; // leave code unchanged
}

这个宏是 LeMP 的“LES Prelude 类”的一部分。它的作用是接收 LES 的“变量声明”,例如 `x::Foo`,并将其更改为 C# 语言服务理解的不同树:`#var(Foo, x)`,它表示 `Foo x;`。

输入 `x::Foo` 被表示为对 `#::` 的调用,带有两个参数 `x` 和 `Foo`。`ColonColon()` 旨在转换对“`#::`”的调用。它检查是否有两个参数,并交换它们,同时将调用目标从 `#::` 更改为 `S.Var`,后者是 `#var` 的别名。C# 节点打印机认为 `#var(type, name)` 表示变量声明,并使用更熟悉的语法“`type name`”打印它。

关键是,LLLPG 被定义为一个“宏”,它将您的 `LLLPG (lexer) { ... }`; 或 `LLLPG (parser) { ... }`; 语句作为输入,并返回另一个表示 C# 源代码的语法树。作为一个宏,它可以与其他宏(如 `ColonColon` 宏)和谐共存。

LLLPG 的输入语言:EC# 和 LES

增强 C# (EC#)

正如我所提到的,增强 C# 是一种基于 C# 的语言,其编译器尚未存在(我正在寻找志愿者帮助构建编译器,敬请关注)。然而,解析器确实存在,所以我可以谈谈 EC# 支持的一些新语法。实际上,EC# 中有相当多的新语法;我只告诉您与 LLLPG 相关的语法。

令牌字面量

Loyc.Syntax.Lexing.TokenTree eightTokens = @[
    This token tree has eight children 
    (specifically, six identifiers and two parentheses. 
     The tokens inside the parentheses are children of the opening '('.) 
];

LLLPG 是一种“领域特定语言”或 DSL,这意味着它是一种特殊用途的语言(用于创建解析器)。

令牌树是一种允许 DSL(领域特定语言)而无需宿主语言中“动态语法”的技术。与某些“可扩展”语言不同,EC# 和 LES 没有“可扩展”解析器:您无法向它们添加新语法。然而,EC# 和 LES 确实具有令牌字面量,它们是令牌的集合。解析完源文件后,宏可以在嵌入在更大语法树中的令牌树上运行自己的解析器。这就是 LLLPG 所做的。

EC# 允许在任何允许表达式的位置使用令牌树。它还允许您使用令牌树而不是方法体,或者而不是属性体。所以当 EC# 解析器遇到这些语句时

rule Bar @[ "Bar" ];
rule int Foo(int x) @[ 'F' 'o' 'o' {return x;} ];

解析器实际上将其视为属性或方法声明。LLLPG 的 `ECSharpRule` 宏随后将其转换为不同的形式,如下所示

// This is also valid EC# source code
#rule(void, Foo, (), "Bar");
#rule(int, Foo, (#var(int, x),), ('F', 'o', 'o', { return x; }));

主要的 LLLPG 宏负责将其转换为最终输出

void Bar()
{
  Match('B');
  Match('a');
  Match('r');
}
int Foo(int x)
{
  Match('F');
  Match('o');
  Match('o');
  return x;
}

块调用语句

get { return _foo; }
set { _foo = value; } 

在 C# 中,您可能已经注意到“get”和“set”不是关键字。除非它们在属性声明中。在 C# 中,它们根据其位置“成为”关键字。

EC# 概括了这个概念。在 EC# 中,无论它们出现在哪里,getset不是关键字。相反,get {...}set {...} 仅仅是一种新型语句的示例,即块调用语句,它有两种形式

identifier { statements; }                  // form 1: simple
identifier (argument, list) { statements; } // form 2: with args

这些语句被认为与以下形式的方法调用完全等价

identifier({ statements; });                 // form 1: simple
identifier(argument, list, { statements; }); // form 2: with args

所以 LLLPG 块

LLLPG (parser(laType(TokenType), matchType(int))) {
   rule Foo @[ ... ];
}

是一个块调用语句,等同于

LLLPG (parser(laType(TokenType), matchType(int)), {
   rule Foo @[...];
});

块作为表达式

string fraction = "0.155";
float percent = 100 * { if (!float.TryParse(fraction, out float tmp)) tmp = -1; tmp };

在 EC# 中,`{大括号块}` 可以用作表达式,这解释了像 `foo(x, { y(z) })` 这样的方法调用意味着什么。当一个块在表达式内部使用时,就像这样,如果最后一个语句末尾没有分号,则块中的最后一个语句将成为整个块的返回值。在这种情况下,`tmp` 是块的结果,所以 `percent` 将被设置为 `15.5`。或者更确切地说,在 EC# 编译器编写完成后,它被设置为 `15.5`。在此之前,这只是一个有趣但无用的语法树。

就这样的语句而言

LLLPG (parser(laType(TokenType), matchType(int)), { rule Foo @[...]; });

外部括号中的所有内容都传递给属于 LLLPG 的宏,该宏(长话短说)将其转换为 C# 代码。

这些信息足以理解 LLLPG 的工作原理。希望现在您理解了 LLLPG 作为嵌入在 EC# 中的 DSL 的概念。

Loyc 表达式语法 (LES)

LES 与 EC# 非常相似,特别是其词法分析器(例如,"strings"'c' 字符和 @identifiers 在两种语言之间几乎相同)。它有

  • 令牌字面量 @[...]
  • 块作为表达式
  • 超表达式,其功能与 EC# 中的块调用语句类似,也用于表示 def 函数声明、if 语句、for 循环、rule 以及几乎所有其他“特殊”语法。

重要:所有基于 LES 的解析器现在都需要有以下第一行

import macros LeMP.Prelude.Les;

最初这不是必需的,因为 LES“前导”宏是自动导入的。然而,LES 前导宏可能会干扰正常的 C# 代码,所以它不再自动导入(宏编译器不了解输入语言,所以它不知道是否应该导入宏)。

LLLPG 首次发布时,您必须使用 LES,所以我编写了以下部分,描述了 LES 代码和 C# 代码之间的关系。如果您愿意,您仍然可以用 LES 编写解析器,但大多数读者当然会更喜欢 EC#。

LeMP/LES 与 C# 的异同

我不想用所有细节来烦你,但 LES 和 C# 之间最显著的区别是 LES 根本没有关键字。像 `if` 和 `while` 这样的词不是因为实际使用的词而被特殊解析,而是因为语句的格式

总之,这是一个列表

相似之处

以下语句在 LeMP/LES 和 C# 中含义相同

return;
return x;
continue;
break;
var x = value;  // Only "var" works this way. Most variable declarations
                // look different in LeMP/LES (see below)
x = y++ * 10;   // The same operators are available; however, you
                // MUST include a space between different operators
                // because e.g. ++* would be treated as a single operator
Console.Write("Hi"); // Call methods the same way, but do not add a space
                // between the method name and argument list.

区别

在许多情况下,区别仅仅在于您需要额外的分号或大括号。

using System.Collections.Generic;  // C#
import System.Collections.Generic; // LES: 'using' works, but gives a warning.

class Foo { // C#
  public Foo() : this(0) { }
  public Foo(int num) { }
}
class Foo { // LES: add 'cons' and semicolons; call base() as first statement
  public cons Foo() { this(0); };
  public cons Foo(num::int) { };
};

class Foo : BaseClass, IEnumerable<object> { }  // C#
class Foo(BaseClass, IEnumerable!object) { };   // LES (no space after 'Foo')

int Square(int x)       { return x*x; }  // C#
def Square(x::int)::int { return x*x; }; // LES

void Quit() { throw new QuitException(); }    // C#
def Quit()  { throw (new QuitException()); }; // LES

int x, y; string s;                 // C#
x::int; y::int; s::string;          // LES

int[] list1;      List<int> list2;  // C#
list1::array!int; list2::List!int;  // LES

int? maybe = null;       bool flag = true;
maybe::opt!int = @null;  flag::bool = @true;

int more = (int)(num * 1.5);
more::int = (num * 1.5) -> int;

while (x) Foo();      // C#
while (x) { Foo(); }; // LES (semicolon required)
while x   { Foo(); }; // LES (parens optional)

do x/=2; while (x>y);     // C#
do { x/=2; } while (x>y); // LES (add braces)

for (x = 0; x < 10; x++) Console.WriteLine(x);      // C#
for (x = 0, x < 10, x++) { Console.WriteLine(x); }; // LES (note the commas)

foreach (var item in list) { Process(item); }  // C#
foreach (item \in list)    { Process(item); }; // LES

if (c) return 0;      // C#
if (c) { return 0; }; // LES (semicolon required)
if c   { return 0; }; // LES (parens optional)

if (list.Count > 0) str.Append(list[i]);         // C#
unless list.Count == 0 { str.Append(list[i]); }; // LES

if (inc) x++; else x--;      // C#
if (inc) {x++;} else {x--;}; // LES (semicolon required)
if inc   {x++;} else {x--;}; // LES (parens optional)

switch (x) { case 123: break; default: break; }          // C#
switch (x) { case 123; break; default; break };          // LES: no colons
switch (x) { case 123 { break; }; default { break; }; }; // LES: can use braces

try { } catch (Exception ex) { } finally { }; // C#
try { } catch ex::Exception  { } finally { }; // LES

事实上,在许多情况下,大括号并非必需,但只要您不完全理解 LES 的工作原理,就应该包含它们。许多未提及分号的错误消息实际上指的是缺少分号;LeMP 会感到困惑,因为像这样的代码

if (a) { ... } // missing semicolon
if (b) { ... }  

被成功解析为单个语句 if (a, {...}, if, b, {...}),宏“if”不理解它,因为参数太多。

总结

显然,我在这里只是触及了皮毛,但这几乎足以作为介绍。下一步,您可以尝试本文随附的演示词法分析器和解析器。

SFG 自定义工具 (LllpgForVisualStudio.exe 1.0) 中的一个 bug 涉及微软提供的奇怪异常

InvalidCastException:无法将类型为 'System.__ComObject' 的 COM 对象强制转换为接口类型 'Microsoft.VisualStudio.Shell.Interop.IVsGeneratorProgress'。此操作失败是因为 COM 组件上对 IID 为 '{BED89B98-6EC9-43CB-B0A8-41D6E2D6669D}' 的接口的 QueryInterface 调用因以下错误而失败:不支持此接口(来自 HRESULT 的异常:0x80004002 (E_NOINTERFACE))。

已在 1.1.0 版本中修复,因此错误消息将显示在正常错误列表中,而不是消息框中。

本系列的其他文章已完成。以下是未来文章中涵盖的主题列表

  • LL(k) 到底是什么意思?
  • 基类的要求BaseLexerBaseParserForList)。
  • 设置先行:默认情况下,LL(k) 中的 k 为 2:LLLPG 最多基于 2 个字符或令牌的先行来做出决策。使用 [k(4)] 规则属性允许 4 个字符的先行(或在整个语法,即 LLLPG 语句上使用 [DefaultK(4)] 属性)。在歧义语法中,提高 k 将以惊人的速度增大输出大小。请负责任地使用 k。
  • 管理歧义:大多数计算机语言实际上有些歧义。LLLPG 将首先检测歧义并使用歧义输入示例(“对于输入如 «0x_»,备选项 (2, 4) 存在歧义”)警告您。分支按优先级顺序指定,您可以通过用 /(其工作方式与 | 相同,但倾斜)分隔分支来抑制警告。如果与退出分支存在歧义,您可以使用“greedy”或“nongreedy”来指定循环或退出,哪个应该具有更高的优先级。
  • 动作,表示为 {code;},是插入到解析器中的代码片段。实际上,关于它们没什么可说的。
  • 集合反转运算符 ~ :如果 TT.Foo 指代一个令牌类型,那么 ~TT.Foo 匹配除 TT.Foo 之外的任何单个令牌。
  • 保存输入:终端的值可以赋值给变量,使用 x:='a'..'z' 创建变量 x,使用 x='a'..'z' 将值赋给现有变量 x,或者使用 xs+='a'..'z' 通过调用 list.Add(MatchRange('a', 'z')) 将值添加到列表 'xs'。
  • 错误处理:默认情况下,LLLPG 生成的预测代码假设输入始终格式良好。但是有几种处理错误的方法,包括“错误分支”,如 (a | b | error { HandleError(); }),以及 [NoDefaultArm] 语法属性。或者,您可以将一个分支指定为默认(默认的默认分支是最后一个分支或退出分支)。
  • 真正的 LL(k):LLLPG 默认不使用“真正的”LL(k),它使用一种略微弱一些的预测系统,类似于 Terrance Parr 的“线性近似先行”,只是我的版本功能更强大一些。有时我的伪 LL(k) 不够强大,所以您可以将实验性的 [FullLLk] 属性添加到您的语法或单个规则中,这将导致使用“真正的”LL(k)。
  • 语义谓词,表示为 &{expr},用于消除歧义。如果两个分支存在歧义,您可以使用语义谓词为一个或两个分支添加条件,该条件将用于消除歧义;expr 只是一个 C# 表达式(如果输入文件是 LES,则为 LES 表达式)。
  • 句法谓词,又称零宽度断言,表示为 &(foo),非常相似,只是 LLLPG 不运行 C# 表达式,而是测试输入是否匹配语法 foo。句法谓词可以执行无限先行,因此您可以使用它们来突破 LL(k) 语法的限制。
  • 在 LLLPG 中,循环 (foo*)、可选元素 (foo?) 和分支 (a|b|c) 都是一个核心谓词 Alts 的实例。LLLPG 将 (a|b|c)* 中的循环和分支臂视为一个单元。
  • 门控,表示为 p => m,将“预测”与“匹配”分开。基本上,它用于让 LLLPG 行为不合逻辑,如果您能找到一个合法的使用场景,您会理所当然地感到自豪。=> 的优先级高于 |a | b => c | d 被解析为 a | (b => c) | d
  • 关于 Loyc 及其库的一切.

就是这样了!希望你们喜欢我的解析器生成器,伙计们,我很乐意回答你们的问题。但你们可能没有问题。因为我几乎涵盖了所有内容。

历史

  • 2013 年 10 月 7 日:LLLPG 0.9 随本文发布
  • 2013 年 11 月 19 日:LLLPG 更新至 0.91。更新了演示以使其更简洁并消除对 Loyc 库的依赖。LLLPG 0.91 包含一些 bug 修复,一个新的 alias(X = Y) 命令,并消除了对 IntSet 的依赖。Visual Studio 扩展即将推出...
  • 2013 年 11 月 26 日:第 2 部分发布,包含 Visual Studio 扩展。
  • 2014 年 2 月 8 日:LLLPG 1.0 发布,支持 EC#;演示和文章已更新。演示现在默认使用 EC#(LES 版本仍然包含)并支持“数学”表达式,如 2(2 + 5) => 14。
  • 2014 年 2 月 23 日:第 3 部分发布,包含 LLLPG 1.1.0 和多个新演示。
  • 2014 年 2 月 25 日:第 4 部分发布。
  • 2015 年 6 月 19 日:第 5 部分发布
  • 2016 年 3 月 3 日:LLLPG 更新了新版本的 LeMP - 请访问新的 LeMP 主页
© . All rights reserved.