2 个版本
0.1.2 | 2023 年 10 月 2 日 |
---|---|
0.1.1 | 2023 年 10 月 2 日 |
#220 在 解析工具
在 3 个crate中使用
24KB
294 行
目录
简介
带来您自己的语法,并为不同语言生成功能丰富的解析器。您可能想使用此工具的原因有:
- 简洁的输入语法格式和直观的算法:基于解析表达式语法生成递归自上而下的解析器
- 自动处理空白,使语法更简洁
- 通过失败
labels
使用自定义消息进行错误报告 - 部分支持声明错误恢复规则,允许增量解析,即使在多次解析错误的情况下也能返回输出树。
项目状态
- 我们还不是 1.0 版本,所以 API 不可稳定,这意味着数据结构形状可能会改变,并且/或者行为可能会发生剧烈变化,且没有太多的通知。
- 在没有提出问题和讨论您的想法之前不要提交拉取请求。我们将采取缓慢的方法,首先考虑伟大的设计,然后是稳定性,然后是功能丰富性。
当前支持的输出语言
- Rust¹
- Go Lang²
- Python
- Java Script
- 编写您自己的代码生成器
注意
-
Rust 支持基于虚拟机作为其运行时,而不是基于生成的解析器。这不太可能改变,因为我们的工具将用 Rust 构建,所以我们需要在“宿主实现”方面有更多的灵活性。如果我们提供任何价值,我们可能会提供一个选项来生成不依赖于任何库的 Rust 代码解析器。但这项工作目前尚未计划。
-
我们正在放弃Go实现,转而使用Rust代码生成Go解析器。原型设计一些特性,如导入系统和自动空格处理,非常有用,但一旦Rust实现的重构到位,Rust版本将会更好,因为它将使得为Rust和Go以外的语言生成解析器变得更加容易。
基本用法
如果您只是想试水,请将命令行工具指向语法,并选择一个起始规则。
cargo run --bin langlang run --grammar-file grammars/json.peg --start-rule JSON
这将带您进入一个交互式shell,您可以在其中尝试不同的输入表达式。
请查看仓库根目录下的grammars
目录中的其他示例。它包含常用输入格式的语法库。
输入语言
产生式和表达式
输入语法尽可能简单。它基于原始PEG格式,并谨慎地添加了其他特性。以下是一个示例输入
Production <- Expression
箭头左侧有一个标识符,右侧有一个表达式。这两个合在一起称为产生式或(解析)规则。让我们来看看如何组合它们。如果您曾经看到或使用过正则表达式,您已经领先一步。
终端
-
Any:匹配任何字符,只有在达到输入末尾时才会出错。例如:
.
-
Literal:任何引号(单引号和双引号相同)中的内容。例如:
'x'
-
Class and Range:类可以包含范围或单个字符。例如:
[0-9]
,[a-zA-Z]
,[a-f0-9_]
。最后一个示例包含两个范围(a-f
和0-9
)和一个单个字符(_
)。这意味着匹配以下任一项。例如:[a-cA-C]
被翻译为'a' / 'b' / 'c' / 'A' / 'B' / 'C'
。
非终端
此类语法与正则表达式相比的最大增光是能够定义和递归调用产生式。以下是一个解析数字的语法片段
Signed <- ('-' / '+') Signed / Decimal
Decimal <- ([1-9][0-9]*) / '0'
最顶层的产生式Signed
调用自身或产生式Decimal
。它允许递归地解析有符号和无符号数字。(例如:+-+--1
等等将被接受)。
表达式组合
以下运算符可以在括号表达式之上对终结符和非终结符使用
运算符 | 示例 | 注释 |
---|---|---|
有序选择 | e1/e2 |
|
非谓词 | !e |
|
谓词 | &e |
!!e 的简化 |
零次或多次 | e* |
|
一次或多次 | e+ |
用于 ee* 的语法糖 |
可选的 | e? |
用于 &ee / !e 的语法糖 |
词法化 | #e |
|
标签 | e^标签 |
用于 e/throw(label) 的语法糖 |
有序选择
该运算符逐个尝试表达式,从左到右,并在第一个成功的表达式处停止。如果没有替代方案工作,则报错。例如:
SomeDigits <- '0' / '1' / '2' / '3' / '4'
将 6
传递给上述表达式将生成错误。
谓词(非/与)
谓词是允许无限前瞻的机制,因为它们不会消耗任何输入。例如:
BracketString <- "[" (!"]" .)* "]"
在上面的例子中,如果解析器找到闭合的方括号,则不会评估 any 表达式。
and 谓词 (&
) 只是 !!
的语法糖。
重复(零个或多个)
-
零次或多次:它永远不会失败,因为它至少可以匹配其表达式零次。
-
一次或多次 是调用表达式一次,然后对该表达式应用零次或多次的语法糖。它可以在第一次匹配表达式时失败。
-
可选 它将匹配表达式零次或一次。
词法化
默认情况下,生成的解析器在生成代码时,会在每个序列项之前自动消耗空白,这些项被认为是非语法的。如果所有表达式都是语法的,则生成是语法的。如果表达式输出的树只由终端匹配组成,则认为它是语法的。如果有任何路径到非终端匹配,则整个表达式和生成都是非语法的。例如:
NotSyntactic <- Syntactic "!"
Syntactic <- "a" "b" "c"
在上面的例子中,由于所有项都是终端,因此在序列表达式 "a" "b" "c"
的项之前没有注入自动空间消耗。并且,NotSyntactic
生成包含非终端调用,这使得它成为非语法的。因此,将自动空间处理启用为 NotSyntactic
,并禁用为 Syntactic
为了 禁用 表达式的自动空间处理,请使用词法化运算符 #
前缀。例如:
Ordinal <- Decimal #('st' / 'nd' / 'rd' / 'th')^ord
Decimal <- ([1-9][0-9]*) / '0'
在上面的表达式中,Decimal
被认为是语法的,这禁用了自动空间处理。因为 Ordinal
调用另一个具有非终端的生成,所以 Ordinal
不是语法的。因此,将启用该生成的自动空间处理。但是,在非终端和具有终端的选项之间,空间处理被禁用。这就是预期的结果
输入 | 结果 |
---|---|
"3rd" | 成功 |
"50th" | 成功 |
"2 0th" | 失败 |
"2 th" | 失败 |
第一个输入成功,因为自动空间消耗被添加到非终端 Decimal
的调用左侧,因为 Ordinal
不是语法的。但是,因为跟随非终端的表达式带有词法化运算符,所以不会在非终端的调用和有序选择(带有语法的后缀 st
、nd
、rd
、th
)之间注入自动空间处理。
下面是可能需要的最经典的词法化例子:非语法的字符串字面量。它使用贪婪前瞻,并且空白是有意义的。例如:
SyntacticStringLiteral <- '"' (!'"' .) '"'
NonSyntacticStringLiteral <- DQ #((!DQ .) DQ)
如果不使用规则 NonSyntacticStringLiteral
上的词法化运算符,它将消耗第一个引号后的空白,这对于字符串字段可能是不可取的。
规则 SyntacticStringLiteral
不需要词法化运算符,因为它的所有子表达式都是终结符,因此该规则是语法的,并且默认不会产生空间消耗。
当然,还有更多关于词法化运算符的使用场景,这里只列出了常见的几种。
带有标签的错误报告
导入系统
一种语法的生成式可以从另一种语法中导入。这允许重用规则,并最终生成更统一的语法文件和更强大的解析器。
// file player.peg
@import AddrSpec from "./rfc5322.peg"
Player <- "Name:" Name "," "Score:" Number "," "Email:" AddrSpec
Name <- [a-zA-Z ]+
Number <- [0-9]+
// ... elided for simplicity
// file rfc5322.peg
// https://datatracker.ietf.org/doc/html/rfc5322#section-3.4.1
// ... elided for simplicity
AddrSpec <- LocalPart "@" Domain
LocalPart <- DotAtom / QuotedString / ObsLocalPart
Domain <- DotAtom / DomainLiteral / ObsDomain
// ... elided for simplicity
上述示例说明,一个相当完整的电子邮件解析器可以通过导入在其它语法中使用。在幕后,AddrSpec
规则及其所有依赖已经合并到了 player.peg
语法中。
生成器选项
Go
Go 代码生成器为命令行提供以下额外的旋钮:
-
--go-package
:允许自定义每个 Go 文件开始的package
指令中的内容。 -
--go-prefix
:允许自定义生成的结构体前缀。这在需要在一个包中解析两个语法时特别有用。至少需要一个前缀,以便通用的Parser
名称不发生冲突。例如:-go-prefix Tiny
会生成一个TinyParser
结构体,一个NewTinyParser
构造函数等。