相关问题 |
上下文
正式语言的规范核心,例如AsciiDoc,是一个正式语法。正式语法通过描述语言的句法,即哪些字符串的组合是有效的,以及与这些组合相关的任何语义来描述语言的句法。语法由一系列规则组成,这些规则描述了标记元素、它们之间的关系、以及必要的排序和优先级。语法由解析器补充,解析器将按照规则匹配的输入转换为节点,并表达语义规则。AsciiDoc的正式语法将解析的细节从特定于实现的代码中剥离出去,变成其他实现可以参考和构建的东西。
我们需要选择哪种语法形式来描述AsciiDoc语言在规范中的语法。这种形式也需要能够以规则动作的形式表达与这些规则相关的任何必需的语义。换句话说,我们必须选择一个提供完全语义解析的解决方案,其目标是生成一个抽象语法树(ASG),而不仅仅是词法解析。
接受的决定
我们已经决定在规范中使用解析表达式语法(PEG)作为正式语法来描述AsciiDoc语言。特别地,我们选择了PEG的[Peggy方言](https://peggyjs.org/documentation.html#grammar-syntax-and-semantics-parsing-expression-types)。Peggy提供了PEG解析器的全部表达能力。在原型制作期间,它的方言被证明是灵活、简洁和易于阅读的。
规范的规范部分将使用PEG语法和规则操作作为沟通AsciiDoc语言的语法规则、关系和预期行为的方式。
请注意,并不是一定要使用PEG解析器,甚至也不必重用规范中提供的语法和其规则。引用自 https://blog.reverberate.org/2013/09/ll-and-lr-in-context-why-parsing-tools.html:
语言规范通常用像BNF这样的形式语言来定义,但几乎从来没有真正的解析器能够直接从这种形式语言生成。
重要的是实现是否产生预期的ASG。然而,我们坚信规范中的语法将清楚地传达 AsciiDoc 语言的有效语法规则和解析所需的语义,并且将为编写实现提供一个良好的起点。
决策摘要
我们必须牢记我们正在为一种现有的语言撰写规范,因此需要一个正式的语法。AsciiDoc 的实现定义历史导致了该语言的某些方面与其解析方式紧密相连。规范的一个目标是解开这些语法的纠缠,使其可以在规范中被清晰地描述。
既然AsciiDoc语言是在有任何尝试对其形式化之前就已经建立的,其语法必将不得不去适应它固有的规则,而不是反过来。这严重限制了哪些语法形式适用于描述AsciiDoc语言。AsciiDoc往往是上下文敏感的,与许多语法形式要求的上下文无关性相反。AsciiDoc还本质上是递归的,但通常依赖于语义边界。因此,许多现有的语法形式可能并不适用于AsciiDoc语言。
我们发现,PEG(解析表达式语法)是在规范中使用正式语法描述AsciiDoc最合适的形式主义。我们专注于Peggy(一个基于JavaScript的PEG实现),因为它被证明是最适合这项任务的。
为什么选择PEG?
AsciiDoc不是一个上下文无关语言。此外,解析AsciiDoc不能有歧义,这意味着只有一个有效的解析树。这意味着解析AsciiDoc依赖于有序选择运算符,这在某些时候,必须借助断言和语义谓词来做出选择。断言会在不消耗任何字符的情况下,检查光标前方的邻接要求。谓词是一个任意复杂的表达式或动作,用于在不实际消耗输入字符串的情况下提前查看输入字符串。这已经明确地将我们置于PEG的功能集之内。
解析表达式文法通过强制所有的语法规则根据优先选择来定义来避免歧义。
那正好描述了 AsciiDoc 所要求的确定性解释。
AsciiDoc的另一个特点使我们得出结论,PEG是描述AsciiDoc的正确选择。任何一系列Unicode字符都被视为可解析的AsciiDoc文档,即使在某些情况下会发出警告。这是因为AsciiDoc首先是一种用于写作的语言,而不是编程语言。语言必须假设,如果没有匹配的语法规则,意味着没有找到保留的标记,则文本旨在成为读者眼中的内容。另一方面,如果字符序列,例如块分隔符线,匹配了一个语法规则,那么这个字符序列就被解释为具有语义意义,例如将内容包裹在侧边栏块中。PEG还支持正则表达式字符匹配,这对于支持所有书面语言(即,Unicode)至关重要。
在语言的许多方面,AsciiDoc 依赖于递归下降(章节层次、限定块、嵌套文本格式等)。PEG 作为一个递归下降解析器,使其成为一个自然的选择。
在解析表达式语法(PEG)中,可以将动作与规则关联起来,将解析结果转换为语义节点,这对于描述如何解释一个元素至关重要。对我们来说,仅仅记录AsciiDoc的有效语法是不够的;我们还需要记录所需的语义。PEG中的动作赋予了我们这种能力,而无需引入单独的形式主义。
基于这些需求,我们得出的结论是AsciiDoc非常适合使用PEG解析器。我们依靠PEG解析的强大功能来描述AsciiDoc语言,至少是为了编写规范。
我们不必过于担心PEG的低效,因为AsciiDoc的行和标记导向风格已经自然地设计得可以提前否决规则。在这方面,这种语言非常适合由PEG生成的解析器。
Wikipedia上的PEG页面宣称,并不是所有可以用解析表达式语法表示的语言都能被LL或LR解析器解析。我们认为这对AsciiDoc也是如此,尽管证明或反驳这一点仍然是一个未解决的问题。
多重语法
为了以一种实现合理向后兼容性的方式描述AsciiDoc,不可能像最初在 https://www.tweag.io/blog/2021-06-15-asciidoc-haskell-pandoc 中断言的那样使用单一语法。相反,必须使用此处列出的几种不同语法来描述该语言:
-
行预处理器(如果解析器支持的话,有可能集成到块语法中)
-
区块
-
属性列表
-
内联预处理器
-
内联
将语法分开的主要原因是块级元素和行内元素在解析上有根本的不同。块级元素大多是基于行的。因此,每个这样的规则都必须将行结束视为有意义的字符,往往是一个边界。行内元素将行结束看作是另一种类型的空白字符,并且可以舒适地跨越它们。另一个原因是块级名称决定了使用哪个根语法规则来解析行内语法(如果有的话)。
因此,由于语言的演变方式,解析的每个阶段之间都存在自然的重启点,需要切换到一个子解析器。试图用单一的语法来描述这种语言会允许设计上禁止的行为,比如一个段落延续到限定块的末尾,或者一个块属性值延续到行末。
行预处理器
没有疑问,行预处理器是用形式语法描述AsciiDoc的最大障碍。那是因为它与块结构交织在一起,这意味着它既依赖于它,也能够影响它。使用专用的语法可能可以描述这部分语法,但实现可能选择将其直接集成到块语法中,或者完全使用不同的策略。由于它将是最容易理解的,我们很可能会在规范中使用专门的PEG语法来描述行预处理器。将行预处理器集成到块语法中,依赖于PEG解析器允许输入在已解析的内容之前被修改。我们还在考虑是否可以收紧规则,以将行预处理器与块解析解耦(#26)。
断言和语义谓词
仅仅说AsciiDoc必须使用PEG来描述是不够的。这种语言有一些特性要求语法形式必须符合特定的要求。AsciiDoc需要使用PEG提供的全部特性集,尤其是断言和语义谓词。
一个需要断言的好例子是匹配受限的内联标记。一个受限的跨度不能允许在开头标记之后立即出现空格,并且不能由一个单词字符跟随。在PEG中表达这一要求唯一的方法是使用断言。我们还看到这一需求在块级语法中再次出现,当规则必须避免越过行边界或者断言在规则紧随其后的地方有一个行边界。
表达块级语法严重依赖于语义谓词。例如,必须支持可变长度的块定界符就是需要使用语义谓词的一个很好的案例。在AsciiDoc中,一个块定界符行开始和结束一个定界块。解析器将寻找开头的定界符,然后将输入作为该块的子元素进行解析,一直到闭合的定界符,不允许解析器消耗掉闭合的定界符。对于PEG来描述这部分还是足够简单的。除非定界符行的长度可以变化。因此,为了找到闭合的定界符行,语法无法仅仅匹配任何该类型块的定界符行,而必须匹配与它长度相同的定界符行。当匹配到一个定界符行时,语法规则必须使用语义谓词来检查解析状态,以确定定界符行是否完全吻合。
嵌套块也存在类似问题。不允许在封闭外层块之前就封闭内嵌的块。同样地,语法规则必须使用语义谓词来判断鉴于当前解析上下文,该规则是否适用。
我们在AsciiDoc中一次又一次地看到这种需求出现,包括章节解析、列表解析和表格解析。
AsciiDoc的块语法主要是一个向前的语法结构。一旦块级解析器匹配上启动块的规则,此时会保存状态,那个规则就应该成功匹配。换句话说,解析器不应该需要回过头来重新考虑那个规则。因此,必须仔细编写语法以确保解析上下文保持一致。理论上,如果我们禁止使用packrat解析(缓存),可以避免这个要求。然而,如果我们能够以一种在启用PEG解析器的这个功能时不会失败的方式编写语法,那将是最好的。
语义谓词需要用到的另一个地方是,当块属性,特别是块样式影响解析时。例如,如果一个标题上面有`[discrete]`,解析器应该将其视为独立标题,而不是节/节标题。在这种情况下,解析器需要能够咨询块属性以确定如何继续进行。为此,块属性必须存储在解析上下文中,使得语义谓词可以进行咨询。
在可能的情况下,我们会尽量避免在语法中依赖语义断言,以便更易于理解。然而,由于语言本身依赖于这些语义断言,因此完全避免它们是不可能的,它们将会出现在规范的语法规则中。
向后兼容
使用正式语法来描述AsciiDoc语言,特别是PEG,将对向后兼容性产生一些影响。但这种影响是积极的。这意味着AsciiDoc将被更准确和确定性地解析。
一个显著的变化是嵌套块将被语义地解析,而不是词法地解析。这意味着如果一个块嵌套在另一个块内部,父块不能被关闭直到子块被关闭。这几乎是所有其他递归语言的工作方式,也是AsciiDoc存在以来作者们期望的东西。在规范之前的AsciiDoc中不可能以这种方式实现块解析,因为解析不是基于正式的语法。但我们现在有机会纠正这一点。
内联解析将会有类似的好处。当内联标记嵌套时,嵌套的标记必须在父标记结束之前结束。同样,这是作者所期望的行为。缺乏这种能力已经导致了使用防御性的变通方法。这些变通方法在很大程度上仍然适用,但将变得不必要。
在块上使用`subs`属性会变得更加限制性。语法将能够考虑常见的排列组合,但不允许完全重新排列语法规则。我们相信在保持对现有文档的合理兼容性的同时,仍然能够适应这一功能最流行的用途。
属性条目将成为块语法的一部分。因此,将明确属性条目的有效位置。在某些情况下,这可能导致规则更加限制性(例如,不能与块属性行交错),而在其他情况下,它可能允许规则更加宽松(例如,可以放置在限定块的末尾)。
由正式语法带来的语法变更将使 AsciiDoc 的解析更加确定性,因此将受到作者的欢迎。我们认为这项变化适合保持与现有文档的合理兼容性。