LLLPG、Loyc 树和 LES,天啊!
LLLPG 是一个递归下降解析器生成器,其功能集(和语法)与 ANTLR version 2 相当。
背景:LLLPG
我通常不会让这些内部的“规划”帖子发布到 CodeProject 上,但我的规划已经达到了一个高级阶段,我认为听到关于我的新解析器生成器以及我关于一种名为 LES 或 LEL(取决于你用它做什么)的新编程语言的计划的一些反馈会很有趣。这个解析器生成器叫做 LLLPG (Loyc LL(k) Parser Generator)。“Loyc”意为“你选择的语言”(Language of Your Choice),它是我的一个项目,旨在创建一套通用工具,用于创建编程语言和在不同语言之间翻译代码*;同时,LL(k) 是计算机语言中最流行的三种解析器类型之一(其他是 PEG 和 LALR(1)……也许 ANTLR 的 LL(*) 现在比 LL(k) 更受欢迎,但我将 LL(*) 视为 LL(k) 的一个简单变体)。
LLLPG 是一个递归下降解析器生成器,其功能集(和语法)与 ANTLR version 2 相当。LLLPG 1.0 已完成约 80%。所有缺失的只是一个可以接受文本形式语法的解析器,以及一些修饰,但最后 15% 是最重要的——如果没有一个友好、易用的输入语言,没有人会想使用 LLLPG,而目前 LLLPG 无法解析自己的输入。
我不会像 ANTLR 那样使用专用的输入语言;不,LLLPG 被设计为嵌入到另一种编程语言中。最初,您将像使用 ANTLR 一样使用 LLLPG,即在编译时作为预构建步骤调用一个 exe,生成 C# 代码,然后编译。但最终,LLLPG 将仅仅是另一种语言的语法扩展,是您可能安装的数百个扩展之一。您将把 LLLPG 添加为编译时引用,就像今天您可能将“System.Core”添加为运行时引用一样。这意味着使用解析器生成器将不再比使用任何其他库更困难。当我完成时,您将能够轻松地将解析器嵌入到任何您想要的地方;创建一个在编译时生成和优化的解析器将不再比今天创建一个“编译好的正则表达式”更困难。但当然,“编译好的正则表达式”实际上是在运行时编译的,而 LLLPG 将完全在编译时工作*
LLLPG 的设计理念与 ANTLR 大相径庭。ANTLR 是一个“大杂烩”式的解析器生成器;它试图为您做所有事情,它需要自己的运行时库,如果您真的想精通 ANTLR,您会购买解释如何使用它的书。我无法在功能上与 ANTLR 竞争;Terrance Parr 编写 ANTLR 及其前身我想已经将近 20 年了。相反,LLLPG 的设计注重简洁和速度,其功能倾向于正交而非相互交织;它相当容易学习,它倾向于生成快速解析器,生成的代码简单、易于理解且特定于 C#,并且它不需要特殊的运行时库,只需要一个您可以自己编写或复制到您的项目中的单一基类。以下是它的功能集
- 它生成一个基于预测的 LL(k) 解析器,不带回溯。k 的值是最大预读长度;它可以为每个规则单独定制。
- 它会警告您歧义,您可以轻松抑制不相关的警告(较早的替代方案具有优先权)。与 ANTLR 类似,LLLPG 通过示例警告(“替代方案 (2, 4) 对于输入如«0x_»是模糊的”)。如果与退出分支存在歧义,您可以将循环标记为贪婪或非贪婪以抑制警告。
- 它支持*语义谓词*,表示为&{expr},用于消歧。如果两个分支存在歧义,您可以使用语义谓词对一个或两个分支设置一个条件,该条件将用于消歧;expr 只是一个 C# 表达式。
- 句法谓词,表示为 &(foo),非常相似,不同之处在于 LLLPG 不是运行 C# 表达式,而是测试输入是否匹配语法 "foo"。句法谓词,也称为零宽度断言,可以执行无限预读,因此您可以使用它们来摆脱 LL(k) 语法的限制。
- 一个*门*,表示为`p => m`,是一种先进(但非常简单)的机制,它将“预测”与“匹配”分开。
- 性能优化包括“预匹配分析”、switch() 语句生成、将预读终端保存到变量中,以及不使用任何异常。
- Alts (替代方案,即分支点) 对于意外输入有两种错误处理模式。LLLPG 可以 (1) 简单地运行“默认分支”,或 (2) 生成一个错误处理分支(自动或手动)。默认分支通常是最后一个分支,但这可以被覆盖(无论是为了协助错误处理还是作为性能优化)。
- 动作,表示为 {code;},是插入到解析器中的代码片段。
- 终端的值可以通过 x:='a'..'z' 赋值给变量 'x',x='a'..'z' 赋值给已存在的变量 'x',或者 xs+='a'..'z' 将值添加到列表 'xs'。
当 LLLPG 正式发布时,我将在 CodeProject 文章或一系列文章中重复所有这些内容,并提供更多细节。目前,以上信息只是本文真正主题的背景。
LLLPG 的自举
当你制作一个接受文本输入的解析器生成器时,你自然希望用它自己的语言来表达解析器生成器自身的语法,如果可能的话,你还希望使用自己的解析器生成器来解析该语言。但是,在没有任何解析器的情况下,你如何开始呢?制作一个能够“自我解析”的解析器的过程称为自举。
我首先利用了 C# 运算符重载。我目前正在使用以下 C# 代码编写解析器:
最终语法 | C# 语法 |
---|---|
'x' | C('x') -- 或者 Sym("foo") 用于 符号 \foo |
'a'..'z' | Set("[a-z]") |
'[' OtherRule ']' | C('[') + OtherRule + C(']') |
a | b | c | a | b | c |
a / b / c | a / b / c -- 但是 `a b / c d` 需要括号: `(a + b) / (c + d)` |
Foo* | Star(Foo) |
Foo+ | Plus(Foo) |
Foo? | Opt(Foo) |
&{i==0} | And("i==0") -- 使用特殊的 #rawText 节点来保存 "i==0" |
{Foo(bar);} | Stmt("Foo(bar)") |
a => b | Gate(a, b) |
请注意,我的单元测试必须使用 C# 语法,因为在描述 LLLPG 最终输入语言的大型语法之前必须编写测试。
我最初的自举计划是使用 LLLPG 在 C# 中为 EC#(EC# 是增强型 C#,其特性之一是允许嵌入式 DSL 的“token literal”)或者至少为自举所需的 EC# 部分编写一个解析器。一旦使用上述映射编写了这个部分解析器,我就可以在“部分 EC#”中编写第二个解析器,而这个第二个解析器将能够解析完整的 EC# 以及自身。
然而,在规划解析器时,我被 EC# 语言的复杂性(当然是因为它的前身 C#)所困扰,我现在正计划创建一个更容易解析的新语法,暂定名为 LES,“Loyc 表达式语法”*。
语法树交换格式
Loyc 计划的一个主要部分是代码的“交换格式”概念。简单来说,这个概念就是“代码的 XML”——一种表示*任何*语言语法树的通用方式。这种交换格式将
- 通过提供一种将树从一种语言*增量*更改以符合另一种语言规则的方式,协助在不同语言之间转换代码的工具。
- 如果编译器在内部使用 Loyc 树,它可以通过提供编译器在各个阶段的中间输出的简单文本表示,帮助编写编译器和编译器扩展的人。
- 将编译时元编程推广到整个软件行业。
让我澄清一下,我并不倡导 XML 作为交换格式。事实上,我讨厌 XML!如果我再键入一次 `<`,我可能会尖叫。相反,我希望创建一个标准语言来描述语法树,就像 XML 或 JSON 用于描述数据一样。(事实上,我的语言也应该适用于描述数据,就像 S-表达式 可以描述数据一样,尽管那不是它的主要目的)。
该交换格式将文本序列化和反序列化为内存中的一种形式,称为*Loyc 树*。Loyc 树中的每个节点都是以下三种之一:
- 一个标识符,例如变量、类型、方法或关键字的名称。
- 一个字面量,例如整数、双精度浮点数、字符串或字符。
- 一个“调用”,表示方法调用,或“类”或“for 循环”之类的构造。
与大多数编程语言不同,Loyc 标识符可以是任何字符串——任何字符串。甚至像 `\n\0`(换行符和空字符)这样的标识符也受支持。这种设计保证了 Loyc 树可以表示地球上任何编程语言的标识符。类似地,字面量可以是任何对象,但当我这样说时,我指的是存在于内存中的 Loyc 树。当 Loyc 树被序列化为文本时,显然它将仅限于某些类型的字面量(取决于用于序列化的语言)。
每个 Loyc 节点也带有一个“属性”列表(通常为空),每个属性本身就是一个 Loyc 树。
Loyc 树与 LISP 树密切相关,并从中获得启发。如果你听说过 LISP,那么 Loyc 树基本上就是 S-表达式 的 21 世纪版本。S-表达式和 Loyc 树的主要区别在于
- 每个“调用”都有一个“目标”。LISP 用 `(method arg1 arg2)` 表示方法调用,而 Loyc 用 `method(arg1, arg2)` 表示方法调用。在 LISP 中,方法名只是列表中的第一个项;但大多数其他编程语言将“目标”与参数列表分开,因此是 `target(arg1, arg2)`。如果你想创建一个简单的列表而不是方法调用,标识符“#”(暂定)约定上表示“列表”,如 `#(item1, item2, item3)`。
- 每个节点都有一个属性列表。属性的概念受 .NET 属性启发,因此您会发现 .NET 属性将在 Loyc 树中由 Loyc 属性表示也就不足为奇了。此外,像“public”、“private”、“override”和“static”这样的修饰符将通过向定义附加属性来表示。
- 一个“调用”,表示方法调用,或“类”或“for 循环”之类的构造。按照惯例,语言内置的构造使用以“#”开头的特殊标识符,例如 ` #class ` 或 ` #for ` 或 ` #public `。
- 每个节点都有一个关联的源文件和两个整数,用于标识原始构造在源代码中占用的字符范围。如果 Loyc 树是通过编程方式创建的,将使用一个虚拟源文件和一个零长度范围。
显然,Loyc 树需要一种文本格式。然而,我认为我可以做得比*仅仅*一个交换格式更好,所以我有一个计划,将 LES 既打造成一个交换格式,又打造成一个通用的编程语言。
由于 LES 可以表示任何语言的语法,我认为将其设计成*无关键字*是明智的。因此,暂定地,LES 没有任何保留字,几乎完全由“表达式”构成。但是“表达式”支持一种名为“超表达式”的语法糖,它类似于其他几种语言中基于关键字的语句。
我做出一个有些激进的决定,让 LES 对空白符部分敏感。我做出这个决定并非轻率,因为我通常更喜欢在表达式中忽略空白符*,但空白符敏感性是使其无关键字的关键。稍后会详细介绍。
我最初的计划是使用 Enhanced C# 的一个子集作为我的“代码 XML”。然而,由于 EC# 基于 C#,它继承了一些非常奇怪的语法元素。考虑到这样一个事实:(a<b>.c)(x) 被归类为“强制转换”,而 (a<b>+c)(x) 被归类为方法调用(当我用 HTML 编写时,< 让我刚才尖叫。抱歉打扰了)。像这样的特性产生了不必要的复杂性,这在 AST 交换格式中不应该存在。
认识 LES:暂定设计
在我描述 Loyc 表达式语法的设计时,请记住这都是暂定的,我可能会被说服以不同的方式设计它。我有以下目标:
- 简洁:LES 不是 XML!它不应被自己的语法所拖累。
- 熟悉度:LES 应该类似于流行的语言。
- 实用性:尽管 LES 可以表示任何语言的语法树,但其语法应该足够强大,成为通用语言的基础。
- 轻量级括号:我厌倦了在 C 家族语言中输入大量的括号。LES 是我减少程序员每天进行 Shift-9 和 Shift-0 操作的机会。
- 简单性:解析器应该尽可能简单,同时仍然满足上述目标,因为 (1) 这将是 Loyc 树的默认解析器和打印器,并将与 Loyc 树代码捆绑在一起,因此它不应该非常大,并且 (2) 这是 LLLPG 的自举语言,所以 (为了我自己) 它不能太复杂。
首先让我描述 LES 的一个简单子集,“前缀表示法”。前缀表示法只不过是将所有内容都表达为方法调用的形式。基本上它是 LISP,但使用 Loyc 树而不是 S-表达式。
例如,C# 代码像
if (x > 0) {
Console.WriteLine("{0} widgets were completed.", x);
return x;
} else
throw new NoWidgetsException();
可以用前缀表示法表示为
#if(@#>(x, 0), @`#{}`(
@#.(Console, WriteLine)("{0} widgets were completed.", x),
#return(x)
),
#throw(#new(NoWidgetsException()))
);
前缀“@”引入了一个包含特殊字符的标识符,然后为带有**非常**特殊字符的标识符添加反引号。因此在 ` @`#{}` ` 中,实际的标识符名称是 `#{} `,这是一个表示大括号块的内置“函数”。类似地,`@#>` 和 ` @#. ` 代表标识符 “#>” 和 “#.”。请注意,“#”**不**被视为特殊字符,也不是运算符;在 LES 中,“#”只是一个标识符字符,就像下划线“_”在 C 家族中是一个标识符字符一样。然而,“#”**根据惯例**是特殊的,因为它用于标记表示编程语言中关键字和运算符的标识符。
纯前缀表示法有点笨拙,所以不会被大量使用。LES 将有内置运算符,这样上面的代码可以用更友好的方式表示:
#if x > 0 {
Console.WriteLine("{0} widgets were completed.", x);
#return x;
}
#throw #new(NoWidgetsException());
然而,这种表示法初次看到时可能会令人困惑。“为什么没有 #else?”你可能会想。“我该如何开始解析它呢?”你可能会说。
好吧,这是我的计划:
- 大括号 {...} 只是子表达式,将被视为对特殊标识符 `#{} ` 的调用。在大括号内部,`#{} ` 伪函数的每个参数都将以分号结尾(最终语句的分号是可选的)。
- 要创建包含标点符号的标识符,请使用 `@` 前缀。此类标识符可以包含标点符号和标识符字符,例如 ` @you+me=win! `。请注意,当我谈论“标点符号”时,字符 `, ; { } ( ) [ ] ` " # ` 不算作标点符号 (` # ` 算作标识符字符)。如果需要这些额外的特殊字符,请将标识符用反引号括起来:` @`{\n}` `。反引号中允许使用转义序列,包括 `\` ` 用于反引号本身。标识符可以在不使用 @ 的情况下包含撇号,例如 `That'sJustGreat `(有几种语言在标识符中使用撇号)。但是,撇号不能是第一个字符,否则它将被解释为字符字面量(例如 'A' 是字符 'A')。
- 将有无限数量的二元运算符,如 +、-、!@$*、>> 等。像 `&|^` 这样的运算符将对应于对 `#&|^` 的调用,例如 `x != 0` 只是 ` @#!=(x, 0) ` 的友好表示。由于简洁是 LES 的一个重要设计元素,每个运算符的优先级将根据运算符的名称和一组固定规则自动确定。 例如,运算符 `~=` 将根据“名称以'='结尾的运算符将具有与赋值运算符'='相同的优先级”的规则而具有非常低的优先级。这种设计允许 LES 成为“上下文无关”的;LES 解析器几乎是无状态的,不需要任何“符号表”。阅读 LES 的人只需要知道优先级规则即可确定他或她从未见过的新运算符的优先级。
- 名称不以 # 开头的运算符可以用 `\` 前缀构成。此类运算符可以包含字母数字和标点符号,例如 `\div` 和 `\++` 是二元或前缀运算符;`a \div b` 等价于 `div(a, b)`,而 `a \++ b` 等价于 ` @++(a, b) `。同样,如果需要特殊字符,请将运算符用反引号括起来:`\`/*\n*/` `。
- LES 将有“超表达式”,它们是多个表达式并置在一起,除了一个空格外没有分隔符。如果一个“超表达式”包含多个表达式,第一个表达式包含一个调用目标,其他表达式是参数。例如,`a.b c + d e;` 被解析为三个独立的表达式:`a.b`、`c + d` 和 `e`。在收集这个表达式列表后,解析器将*第一个表达式中的最终“紧密”表达式*视为“调用”其他表达式(下面解释)。因此,这三个表达式共同将是 `a.b(c + d, e);` 的简写。*
我意识到“超表达式”最初可能令人困惑,但我认为这种设计允许以一种相当优雅的方式编写“表达式”,就好像它们是“语句”一样。例如,以下是一个有效的表达式:
if a == null {
b();
} else {
c();
};
它实际上意味着
if(a == null, { b(); }, else, { c(); });
反过来,它是以下内容的简写
if(@#==(a, null), @`#{}`( b(); ), else, @`#{}`( c(); ));
所有这三个输入都解析成相同的 Loyc 树。
请注意,这些在 LES 中都没有“内置”含义。LES 不关心是否有“else”,不关心你用的是“#if”还是“if”,LES 不给任何东西赋予内置语义。LES 仅仅是一种语法——Loyc 树的文本表示。
所以,对于“为什么没有 #else”或“为什么你在一个地方用 '#if',在另一个地方用 'if'”之类的问题,首先,LES **没有定义任何规则**。编程语言定义了这类规则,但 LES 不是编程语言。它只是一个数据结构的文本表示。
解析器解析一系列表达式是相当容易的。基本上,当你看到两个标识符并排(或其他“原子”,如数字或大括号块),像 `foo bar`,那就是超表达式中两个表达式的边界。当解析器看到一个超表达式时,它必须决定如何将“尾部”表达式连接到“头部”表达式。例如,这个超表达式:
a = b.c {x = y} z;
由表达式“a = b.c”、“{x = y}”和“z”组成。这些表达式将如何组合?我的计划基本上是,其他表达式被添加到第一个表达式中,就像你写了这样:
a = b.c({x = y}, z);
所以,就好像分隔第一个和第二个表达式的第一个空格字符被替换为 '(',末尾添加了一个 ')',并且在其他表达式之间添加了逗号。“b.c”是我所说的“紧密表达式”,因为表达式中的运算符(例如“.”)是紧密绑定的(高优先级),并且表达式通常不带空格字符编写。还感到困惑吗?
“超表达式”的要点是,您可以编写看起来像是内置于语言中的语句,即使它们是由一个固定功能解析器解释的,该解析器并不知道它们意味着什么。例如,在 LES 中,您可以编写
try {
file.Write("something");
} catch(e::Exception) {
MessageBox.Show("EPIC I/O FAIL");
} finally {
file.Dispose();
};
解析器处理它,而无需知道“try”、“catch”和“finally”的含义。
超表达式还允许您在表达式中嵌入语句式构造,例如这样:
x = (y * if z > 0 z else 0) + 1;
LES 解析器会理解“z > 0”、“z”、“else”和“0”是“if”调用中的参数。
LES 的语法高亮引擎应高亮显示“紧密表达式”,以指示超表达式是如何解释的。此语法高亮规则会高亮显示“if”和“try”等词,当它们以这种方式使用时(但不会高亮“else”或“finally”,它们只是“if”和“try”的参数;解析器无法知道它们有任何特殊含义)。
不过,事情并没有我暗示的那么简单。如果第一个表达式已经以方法调用结束:
a = b.c(foo) {x = y} z;
我认为将其解释为
a = b.c(foo, {x = y}, z);
而不是
a = b.c(foo)({x = y}, z);
这种例外情况的动机将在稍后解释。
每个子表达式只能存在一个超表达式。例如,一个带有两组括号的表达式 `... (...) ... (...) ...` 最多可以包含三个超表达式:一个在外部级别,每个括号组中一个。
你不能在没有括号的情况下“嵌套超表达式”。例如,`if x if y a else b else c` 形式的表达式可以解析为 `if(x, if, y, a, else, b, else, c)`,但这没有任何意义,因为第二个“if”只是第一个“if”的 8 个参数之一;没有任何东西表明它有特殊之处。
你有没有注意到这个计划是空格敏感的?
在 LES 中,你通常用空格分隔表达式。运算符——即标点符号序列——如果可能,被假定为二元运算符,否则为一元运算符。因此,“x + y”和“- z”分别被理解为带有二元和一元运算符的单个表达式。当你将这两个表达式并排放置时,例如“- z x + y”,它被视为两个独立的表达式。另一方面,如果你写“x + y - z”,这被理解为单个表达式,因为“-”在第二种情况下被假定为二元运算符。“x + y (- z)”再次解析为两个表达式。运算符和参数之间的空白符影响不大;“x+y (-z)”也是可接受的,并且含义相同。但是表达式*之间*的空格很重要;“x+y (-z)”是两个独立的表达式,而“x+y(-z)”是一个表达式,表示“x + (y(-z))”;在这里,y 是方法调用的目标。
我知道这不理想。但根据我的经验,大多数程序员会这样写:
if (...) {...}
for (...) {...}
lock (...) {...}
while (...) {...}
switch (...) {...}
在关键字和左括号之间有一个空格,而他们会这样写:
Console.WriteLine(...);
var re = new Regex(...);
F(x);
在标识符和被调用的方法之间**没有空格**。因此,在没有真正关键字的语言中,使用空格规则来识别表达式的第一个单词是否应被视为“关键字”似乎是合理的,尽管不理想。
所以空格规则的存在是为了在没有关键字的语言中识别“关键字语句”。LEL 可以解析任何看起来像关键字语句的东西:
if ... {...};
for ... {...};
lock ... {...};
while ... {...};
switch ... {...};
unless ... {...};
match ... {...};
但是请注意,每个语句末尾都需要一个分号来将其与后面的语句分开。在“主题”(例如,“if c”中的“c”)周围不需要括号,但是如果存在括号,空格字符将确保语句仍然被正确理解。像上面这样的语句会自动转换为普通调用,
if(..., {...});
for(..., {...});
lock(..., {...});
while(..., {...});
switch(..., {...});
unless(..., {...});
match(..., {...});
LES 还有另一种空格敏感的方式。由于 LES 包含无限数量的运算符,因此两个相邻的运算符之间必须有空格。例如,你不能将 `x * -1` 写成 `x*-1`,因为 `*-` 将被视为一个名为 ` #*- ` 的单一运算符。
LES 中的属性用方括号表示:
[Flags] enum Emotion {
Happy = 1; Sad = 2; Angry = 4; Horndog = 65536;
};
关于 LES,我还可以提更多细节,但你已经了解了基本思想。
认识 LEL:完全相同的语言
LEL(“Loyc 表达式语言”)将是一种构建在 LES(“Loyc 表达式语法”)之上的编程语言。它将使用**完全**相同的解析器作为 LES;事实上,我们可以说它“使用”LES,因为 LES 只包含一个解析器,别无其他。LEL 然后将赋予语法以意义。例如,在 LES 中,“if”一词只是一个标识符,它没有内置含义。但在 LEL 中,“`if`”将被定义为一个标准宏,该宏创建一个标准的“`#if`”表达式,这将是 EC# 和 LEL 都将在内部使用的某种通用语言的一部分。
所以在 LEL 中,
if a > 0 {
b();
} else {
c();
};
其含义正如你所期望:如果 a > 0 则调用 b(),否则也调用 b()。然而,LEL 将不是一种典型的编程语言。“if”不会像 C# 中那样是内置语句。相反,LEL 将是一种宏密集的语言,“if”将是一个标准宏。如果你不知道,宏是一种在编译时运行的特殊方法。当然,LEL 宏将与 C/C++ 宏无关,C/C++ 宏给“宏”这个词带来了坏名声。像 C/C++ 宏一样,滥用 LEL 宏也是很有可能的;但与 C/C++ 宏不同,LEL 宏将极其强大。
我还没有详细制定宏系统;我一定会阅读更多关于 Nemerle 的资料以获取灵感。
LEL 的语义将基于 EC#,最初它将编译为纯 C#,因此它无法超越 C# 的运行时能力,尽管它仍然可以通过将标点符号映射到字母来允许奇怪的标识符名称;例如,` @Save&Close ` 在 C# 中将表示为 `Save_and_Close` 之类的东西。
像类和 using 语句之类的东西用 LES 超表达式支持起来并不难,我正在考虑使用 :: 来声明变量:
using System.Collections.Generic;
[pub] class Point(ICloneable.[Point])
{
[pub] X::int; // pub for public, see?
[pub] Y::int;
};
我不是一个缩写狂,但 [public] 需要括号,这很不幸,所以我将通过允许你缩写属性来弥补这种额外的语法噪音:public 缩写为 pub,private 缩写为 priv,static 缩写为 stat,virtual 缩写为 virt,等等。此外,和 C# 一样,你可以用逗号分隔属性或使用多个括号组:`[pub,stat]` 和 `[pub][stat]` 是等效的。
为了允许 `::` 用于变量声明,我将 `::` 的优先级设置在点号(`.`)之下,尽管在 C# 和 C++ 中 `::` 的优先级与点号相同。这使得 `s::System.String` 可以解析为 `s::(System.String)`。(你可能想知道我为什么不只用一个冒号 `:` 来做这个;嗯,现在我先把它作为我的小秘密。)
“.[...]”是泛型类型参数的语法;`ICloneable.[Point]` 的解释是 `#of(ICloneable, Point)`,这意味着 C# 中的 `ICloneable<Point>`。我也可以考虑使用 D 语言的语法(`ICloneable!Point`),这还没有定论。然而,C++/Java/C# 语法 `ICloneable<Point>` 是不可能的。它很难解析,并且会使“超表达式”概念高度模糊。
支持 C 风格的变量声明,例如
String x;
这似乎是可能的,但实际上并非如此;`String x` 仅仅意味着 `String(x)`——它是一个普通的函数调用。
方法声明语法有点令人头疼。我正在考虑使用这种语法:
[pub] Square(x::int)::int { x * x };
它被解析为
[pub] (Square(x::int)::int)({ x * x });
[pub] @#::(Square(x::int), int)({ x * x }); // equivalent
[pub] @#::(Square(@#::(x, int)), int)(@`#{}`(x * x)); // equivalent
我只是不确定如何以编译器能够理解这种特殊表示的方式编写 LEL 的规范。我还会,就这一次,希望解除方法名与其参数之间不能有空格的限制,这样你就可以写:
[pub] Square (x::int)::int { x * x };
但这种形式的语法树却大相径庭:
[pub] Square((x::int)::int, { x * x });
这与对名为“Square”的宏的调用基本无法区分,所以我有点困惑。嗯……
让我们再次讨论这个奇怪的空格规则,因为你可能会想,如果空格不正确会发生什么。显然,错误将是 (1) 忘记在 '(' 之前添加空格,以及 (2) 在不应该有空格的地方使用了空格。
首先考虑以下示例:
if(c) { a(); } else { b(); };
这在大多数情况下仍然有效,因为如果第一个表达式的末尾已经有一个参数列表,其他表达式会被添加到该现有参数列表中(如我之前提到的)。所以这个示例的解释与正确版本相同,
if (c) { a(); } else { b(); };
这意味着
if((c), { a(); }, else, { b(); });
但这种错误还有另一种形式,看起来像这样:
if(x+y) > z { a(); } else { b(); };
这可以解析为
if(x+y) > z({ a(); }, else, { b(); });
“if”宏本身可能会生成一个多少能理解的错误消息,例如“'if' 需要 2 或 3 个参数,但只收到一个。你看起来忘记在 'if' 后面加一个空格。”这之后会抱怨没有名为 'z' 的方法接受 3 个参数。
第二种错误,即在不该有空格的地方添加空格,其影响因情况而异。在非常简单的情况下,例如:
foo (x);
解释是 `foo((x));` 所以它有效。但在其他情况下,例如:
x = foo (x) + 1;
a = foo (b, c);
解释不同
x = foo((x) + 1);
a = foo((b, c));
在第二种情况下,(b, c) 被解析为一个元组(我还没有决定 LES 中元组的表示方式),作为*单个参数*传递给 foo()。
不幸的是,代码仍然可以解析,但意义发生了变化。这两种错误的D要防御是语法高亮:`foo` 在后面跟着空格时会显示不同的颜色,没有空格时则不同。我只希望这种区别就足够了。
假设 foo() 是一个方法,我想 LEL 可以优先使用宏的“超表达式”语法,如果使用超表达式来调用普通方法,则发出警告(解析器会将“超表达式”语法的使用记录在 NodeStyle 中)。
我目前的想法是,LEL 的标准宏将本质上将 LEL 转换为“内置”构造,然后这些构造将被编译,并且这些构造将与 EC# 构造相同或非常相似,以便 LEL 和 EC# 可以共享相同的后端。通过这种设计,LEL 可以轻松转换为 C#,因为我计划编写一个 EC# 到 C# 的编译器。所以“if”宏,例如,将接收如下输入:
if (a) b else c;
并产生如下输出:
#if (a) b c;
或者换句话说,
#if((a), b, c);
现在回到 LLLPG
请记住,除了设计两种新的编程语言(LEL 和 EC#)之外,我还正在编写一个解析器生成器,它将用于描述这两种语言。在编写 LES 解析器之后,我将用它来描述 EC# 的完整语法。
请注意,我已为 EC# 语言编写了一个完整的*节点打印器*,解析器生成器使用它来生成纯 C# 代码(EC# 是 C# 的超集,因此解析器生成器只是生成一个表示 C# 代码的 Loyc 树,并使用 EC# 节点打印器将其打印出来)。
现在,我可以在纯 C# 中定义一个语法,像这样:
Rule Int = Rule("Int", Plus(Set("[0-9]")), Token);
// represents the grammar '0'..'9'+, which in LES might be written
// Int @[ '0'..'9'+ ];
// Here, @[...] is a "token literal". It is tokenized by LES, but
// otherwise uninterpreted. This allows the LLLPG macro that is
// parsing the grammar to interpret it instead.
然后我可以生成这样的 C# 代码:
LLLPG.AddRule(Int);
LNode result = LLLPG.GenerateCode();
string CSharpCode = result.Print();
其中“LNode”是“Loyc 节点”,即 Loyc 树的数据类型。以下是生成的代码:
{
private void Int()
{
int la0;
MatchRange('0', '9');
for (;;) {
la0 = LA0;
if (la0 >= '0' && la0 <= '9')
Skip();
else
break;
}
}
}
我的最终目标是让您以 EC# 语法编写语法,它看起来会像这样:
using System;
using System.Collections.Generic;
[[LLLPG(IntStream)]]
class MyLexer {
private Number ==> @[
&('0'..'9'|'.')
'0'..'9'* ('.' '0'..'9'+)?
];
private Id ==> @[ IdStart IdCont* ];
private IdStart ==> @[ 'a'..'z'|'A'..'Z'|'_' ];
private IdCont ==> @[ IdStart|'0'..'9' ];
private Spaces ==> @[ ' '|'\t'|'\n'|'\r' ];
public Token ==> @[ Id | Spaces | Number ];
public void NormalMethod()
{
NormalCSharpCode();
}
}
“==>”是新的 EC#“转发运算符”。不用管它是什么意思,因为在这种情况下,我**劫持了它**来表达 LLLPG 规则。再次强调,@[...] 是 LLLPG 将解析的 token literal。“IntStream”指的是一个用于代码生成的辅助类;它意味着输入是一系列整数(实际上是字符,加上 -1 表示 EOF)。
那么,我如何从这里到那里呢?这是我的自举过程计划。
LLLPG' IntStream (
[pub] class MyLexer() {
[priv] rule Number
&('0'::'9'|'.') +
'0'::'9'\\* + ('.' + '0'::'9'\\+)\\?;
[priv] rule Id IdStart IdCont\\*;
[priv] rule IdStart 'a'::'z'|'A'::'Z'|'_';
[priv] rule IdCont IdStart|'0'::'9';
[priv] rule Spaces ' '|'\t'|'\n'|'\r';
[pub] rule Token Id | Spaces | Number;
}
});
在这里,我将“..”改为“::”,因为“::”的优先级高于“|”和“+”(“..”的优先级低于两者)。反斜杠 `\\` 告诉 LES 解析器,`\\*` 和 `\\?` 运算符是后缀运算符,而不是二元运算符。这种语法并非完全舒适,但它比纯 C# 表示更好。
LLLPG IntStream (
[pub] class MyLexer() {
[priv] Number @[
&('0'..'9'|'.')
'0'..'9'* ('.' '0'..'9'+)?
];
[priv] Id @[ IdStart IdCont* ];
[priv] IdStart @[ 'a'..'z'|'A'..'Z'|'_' ];
[priv] IdCont @[ IdStart|'0'..'9' ];
[priv] Spaces @[ ' '|'\t'|'\n'|'\r' ];
[pub] Token @[ Id | Spaces | Number ];
}
});
被翻译成上述形式。因此,从 LES 到 LLLPG 规则的翻译是一个两阶段过程,涉及两个独立的解释器。这样做是为了允许第三方宏在两个阶段**之间**运行。我目前的想法是 LEL 可以包含“高优先级”和“低优先级”词法宏。首先运行高优先级宏,然后在单独的通道中运行低优先级宏。
请注意,我在第一个代码列表中使用了“LLLPG'”,但在第二个列表中使用了“LLLPG”。LLLPG 将是高优先级宏,LLLPG' 是低优先级第二阶段。这允许其他词法宏在第一阶段之后处理代码,这将有助于终端用户在编译时完成生成语法等奇特操作。
老实说,我从未认真地用 LISP 风格的宏系统(或支持 DSL 的语言)编程过,所以我的想法可能有缺陷。我想我们会看到结果。
- 首先,我将用 C# 编写 LES 的语法,并使用 LLLPG 进行代码生成。
- 接下来,我将编写一个 LEL 的骨架版本,它可以运行简单的非卫生宏(卫生宏我可以在以后,当有更多基础设施时再添加)。
- 其中一个宏将为 LLLPG 收集输入。请记住,此时还没有解析器能够处理 token literal——即没有解析器能够理解像 ` "0x" ('0'..'9'|'a'..'f')+ ` 这样的输入。所以我将使用 LES 作为中间语言。LES 支持以 `\\` 开头的后缀运算符,所以我可以用这样的语法来表达语法:
- LLLPG 宏将获取来自 LES 解析器的 Loyc 树,并将其转换为一系列“规则”对象。这些对象是 LLLPG 核心引擎的“真正”输入。这个转换过程将用纯 C# 编写。
- 接下来,我将使用此语法编写一个用于令牌字面量的解析器。该解析器将简单地将令牌字面量转换为您上面看到的格式,因此:
- 随着 LLLPG 在 LES 中完全实现,我终于可以用 LES 编写整个 EC# 语言的解析器了。(请记住,LES 代码会被翻译成 C# 进行编译。)一旦完成,LLLPG 也可以以最小的改动支持 EC# 作为宿主语言。自举完成!
我在此欢迎各位提出评论和问题。
(更新:我编写了一个 LES 解析器。进入第二步。)