这篇文章介绍了无文法:一种描述具体语法树的新形式主义。无文法背后的思想很简单,而且比具体的实现更有价值。尽管如此,这里提供了一个实现:
非语法描述具体的语法树 - 一组数据类型(或者,如果您愿意,也可以是一组树)。
那么,“描述语法树”到底是什么意思?它为什么有用?在编写IDE时,核心数据结构之一是具体的语法树。它是一棵全保真的树,详细地表示原始源代码,包括括号、注释和空格。CST用于语言的初始分析。它们也是重构的词汇表类型。虽然重构的最终结果是文本差异,但树修改是一种更方便的内部表示。
在最低层,cst通常是unityped的:有一些Node超类,它有一个Node子节点集合和一个可选的Node父节点。在这个原始层的顶部,提供了一个更类似AST的API:struct有一个.name()和一个.field()列表,等等。这个类型化的API很大!对于锈检分析器,它由130多个类型组成!而且它也比典型的AST更详细:struct还有.l_curly()和.r_curly()。
更糟糕的是,这个API改变了很多,特别是在一开始,您可以从直接嵌套在Struct下面的.field()开始,然后为花括号之间的所有内容引入一个StructFields节点,以便与枚举变体共享代码。
简而言之,用手写这篇文章很糟糕:-)非语法是一种简明地描述语法树结构的符号,代码生成器可以使用它来用目标语言构建API。如果你听说过ASDL,那么对于具体的语法树,非语法就是ASDL。对于RUST分析器的情况,这意味着接受以下输入:
Implast::AttrsOwner for Module{}Impast::VisibilityOwner for Module{}Implast::NameOwner for Module{}Impll Module{pub FN mod_Token(&;self)->;option<;SyntaxToken&>;{...}pub FN Item_List(&;self)-<;option<;ItemList>;{...}pub FN分号_Token(&;self)->;option<;SyntaxToken>;{...}}。
在典型的解析器生成器中,可以通过从相同的语法生成解析器和语法树来实现类似的功能。这在某种程度上是有效的,但是存在一个固有的问题,即您想要用于编程API的树的形状和实现解析器所需的语法的形状通常是不同的。像左递归消除这样的“技术”转换不影响语法描述的语言,但是完全改变了解析树的形状。相反,非语法只关注第二个任务,这从根本上降低了语法的复杂性。在锈蚀分析器中,它与手写的解析器配对。
非语法被视为普通(上下文无关)语法,它描述了语言的超集。例如,对于编程API,将逗号分隔的列表中的逗号作为列表元素的一部分来处理可能很方便(rust分析器还没有这样做,但它应该这样做)。这会导致以下非语法,这显然不能准确地处理逗号:
同样,非语法定义了二元和一元表达式,但没有指定它们的相对优先级和结合性。
一个有趣的副作用是,最终得到的语法非常便于人类阅读。例如,一个完整的可用于生产的Rust语法需要大约600行短行:
现在我们已经回答了“为什么”这个问题,让我们来看看非语法是如何起作用的。
与语法一样,非文法使用一组终端和非终端进行操作。终端是原子上不可分割的标记,如关键字fn或分号;非终端是由其他节点和标记组成的复合内部节点。
令牌(终端)使用单引号进行拼写:';+';,';fn';,';ident';,';int_number';。令牌在非语法之外定义,不需要声明即可使用Them.按照惯例,关键字和标点符号使用自身表示,而其他令牌使用LOWER_VOKEN_CASE。由于非语法描述树,因此它使用解析器令牌而不是词法分析器令牌。这意味着上下文相关关键字(';)被识别为单独的令牌(';默认值)。对于像';<;<;';这样的复合令牌也是如此。
节点(非终端)在语法中通过关联节点名称和规则来定义。非语法本身是一组节点定义。按照惯例,节点使用UpperCamelCase命名。每个节点必须精确定义一次。规则是一组记号和节点上的正则表达式。
语法=节点*节点=名称:';ident';';RuleRule=';ident';//字母标识符|';Token_ident';//单引号字符串|Rule*//串联|Rule(';|';Rule)*//交替|Rule';?';//零或一次重复|Rule';*';//Klenee star|';(';Rule';)';//分组|标签:';ident';';:';规则//标签规则
唯一不寻常的是可选标签。默认情况下,生成的代码中的名称是自动从类型派生的,但是标签可以用作覆盖,或者如果有歧义:
非语法没有指定任何特定的方式来降低语法节点定义的规则。它取决于生成器将规则与目标语言构造进行模式匹配:JAVA将使用继承、RUST枚举和TypeScript - 联合类型。生成器只能接受所有可能规则的子集。限制的一个例子可能是:“只允许在顶层使用替换(|)。备选方案必须是其他节点“。有了这个限制,备选方案可以降低为具有多个子类的接口定义。
Ungramar2json二进制代码将非语法语法转换成等价的JSON。有关生成器的示例,请查看rst分析器中的gen_SYNTANGLING。(非语法生成器提供了Rust API,如果您的代码生成器是在Rust中实现的,请使用它。或者,ungramar2json二进制将非语法语法转换为等价的JSON。
Node和Token术语继承自Rowan,Ruust分析器的语法库,更好的选择是Tree和Token,因为节点包含其他节点并且是树。
始终使用单引号终端是一种很好的具体语法。我使用过的一些解析器生成器只需要引用一些终端,这在没有记住规则的情况下降低了可读性。同样,拼写加号而不是';+';也不是很好读。
“递归正则表达式”感觉像是一种方便的CFG语法,不会将右侧限制为备选方案的平面列表,而是使用()进行分组,并允许使用*和?主观上使生成的语法更具可读性。问题是需要联合类型和匿名记录来忠实地降低任意正则表达式表示的规则。将限制放在特定生成器而不是基础语言中,感觉更好地划分了责任。
通过引用终端,使用标点符号(:=()|*?)。对于语法和完全避免的关键字,非语法避免了产生式名称和非语法本身的语法之间的冲突。