C# 的新解析器生成器
LLLPG,Loyc LL(k) 解析器生成器:现在解析 C# 本身!
- LLLPG-1.3.2:在 CodeProject 上发布的最新版本。新版本请访问新主页。
引言
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 试图生成易于理解的代码,但它有选择地使用
goto
和switch
语句以最大化性能。 - 无异常。 上次我使用 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
”语句,也没有将这段代码封装在namespace
或class
中。我的小迷你语言不关心这些;输出反映输入,所以输出同样不会有任何“using
”语句,也不会被封装在class
或namespace
中。垃圾进,垃圾出。如果你想让输出被封装在一个类声明中,你必须将输入封装在一个类声明中。 - 语法必须封装在
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
的符号被假定为终结符。
- 词法分析器模式只理解整数和字符输入,但针对这种输入进行了优化。它不接受命名常量,只接受字面数字和字符,因为它无法判断名称可能指的是哪个数字(编辑:现在有一个用于命名常量的
- 每个规则都会转换为一个同名的方法。规则上的属性,例如
public
或unsafe
(你为什么在解析器中使用不安全代码,小聪明?)都会转移到输出中。规则支持一些属性,例如[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# 解析器
- 我使用 C# 运算符重载和辅助方法作为在纯 C# 中编写 LLLPG 解析器的基本方式(示例测试套件)。
- 以这种方式编写解析器非常笨拙,所以我决定不能以这种方式编写整个 EC# 解析器。相反,我设计了一种在语法上比 EC# 简单得多的新语言,称为 LES。这种语言不仅将作为 LLLPG 的原始输入语言,而且将作为通用的语法树交换格式——一种表示任何编程语言语法树的方式:“代码的 XML”。
- 我通过在纯 C# 中以编程方式调用 LLLPG(运算符重载等)编写了 LES 的词法分析器和解析器。
- 我编写了
MacroProcessor
类(我后来将其命名为“LeMP”,即“词法宏处理器”的缩写)以及一个提供命令行接口的包装类 名为Compiler
。MacroProcessor
的作用是扫描语法树,查找对“宏”的调用,宏是源代码转换器(稍后详细介绍)。它递归地调用这些转换器,直到代码中不再有宏调用。最后,
将结果打印为文本。Compiler
- 我在 LES 之上构建了一个小型“宏语言”,它将 LeMP(宏处理器)与一组小宏结合起来,使 LES 看起来很像 C#。这些宏旨在将 LES 转换为 C#(您可以直接在 LES 中编写 C# 语法树,但它们有点丑)。
- 我编写了一些额外的宏,允许您从 LES 中调用 LLLPG。
- 我修改了 LES 解析器,使其也能在派生类中解析类似
@[ a* | b ]
的 LLLPG 代码(滥用“可重用代码”的耻辱)。 - 我编写了 LES 的词法分析器和解析器,用 LES 本身实现。
- 我于(2013 年 10 月)发表了这篇文章。
- 我用 LES 编写了 EC# 的词法分析器和解析器,在此过程中发现了 LLLPG 中的一堆新 bug(我已修复)
- 我添加了一个额外的宏(
ECSharpRule
)以支持 EC# 中的 LLLPG。 - 最后,为了测试 EC# 解析器,我用 EC# 重写了 LLLPG 的语法。
- 我更新了这篇文章(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# 中,无论它们出现在哪里,get
和 set
都不是关键字。相反,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) 到底是什么意思?
- 基类的要求(BaseLexer 和 BaseParserForList)。
- 设置先行:默认情况下,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 主页。