2个版本
0.1.2 | 2023年10月2日 |
---|---|
0.1.1 | 2023年10月2日 |
#162 in 解析器工具
在 2 个crate中使用(通过 langlang_lib)
83KB
1.5K SLoC
目录
简介
带来自己的语法,为不同的语言生成功能丰富的解析器。你可能会想要使用这个的原因
- 简洁的输入语法格式和直观的算法:基于解析表达式文法生成递归自顶向下的解析器
- 自动处理空白字符,使语法更简洁
- 通过失败
labels
发送自定义消息进行错误报告 - 部分支持声明错误恢复规则,允许增量解析,即使出现多个解析错误,也能返回输出树。
项目状态
- 我们还不是1.0版本,所以API不是稳定的,这意味着数据结构形状可能会改变,行为可能会发生巨大变化,而且不会有太多通知。
- 在没有打开问题并讨论你的想法之前不要提交拉取请求。我们将采取缓慢的方法,首先追求良好的设计,然后是稳定性,然后是功能。
当前支持输出语言
- Rust¹
- Go Lang²
- Python
- JavaScript
- 编写自己的代码生成器
注意
-
Rust支持基于虚拟机作为其运行时,而不是基于生成的解析器。这不太可能改变,因为我们的工具将用Rust编写,因此我们需要在“宿主实现”上具有更多灵活性。如果我们提供任何价值,我们可能会提供一个选项来生成不依赖于任何库的Rust代码解析器。但这项工作目前没有计划。
-
我们正在放弃Go实现,转而使用Rust编写的代码生成Go解析器。原型设计一些功能,如导入系统和自动空间处理,非常有用,但一旦Rust实现重构到位,Rust版本将会更好,因为它将更容易为Rust和Go以外的语言生成解析器。
基本用法
如果你只是想试探一下,将命令行实用程序指向语法并选择一个起始规则
cargo run --bin langlang run --grammar-file grammars/json.peg --start-rule JSON
这会带您进入一个交互式外壳,您可以在其中尝试不同的输入表达式。
请查看存储库根目录下的 grammars
目录中的其他示例。它包含常用输入格式的语法库。
输入语言
产生式和表达式
输入语法尽可能简单。它基于原始的 PEG 格式,并谨慎地添加了其他功能。以下是一个输入示例
Production <- Expression
箭头的左边是一个标识符,右边是一个表达式。这两个合起来称为产生式或(解析)规则。让我们来看看如何组合它们。如果您曾经见过或使用过正则表达式,那么您就有了一个优势。
终结符
-
任何:匹配任何字符,只有在达到输入的末尾时才会出错。例如:
.
-
文字:引号内的任何内容(单引号和双引号相同)。例如:
'x'
-
类和范围:类可以包含范围或单个字符。例如:
[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(标签) |
顺序选择
此运算符依次尝试表达式,从左到右,并在第一个成功时停止。如果没有替代方案有效,则引发错误。例如:
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
不是语法,因为它调用另一个包含非终端的生成式。因此,该生成式将启用自动空间处理。但是,在非终端和终端选择之间,空间处理被禁用。这是预期的
输入 | 结果 |
---|---|
"3rd" | 成功 |
"50th" | 成功 |
"2 0th" | 失败 |
"2 th" | 失败 |
第一个输入成功,因为自动空间消耗被添加到非终端 Decimal
调用的左侧,因为 Ordinal
不是语法。但是,由于随非终端之后的表达式带有词法化操作符,因此不会在非终端调用和带语法后缀的有序选择(st
、nd
、rd
和 th
)之间注入自动空间处理。
这是可能最经典的例子,需要使用词法化:非语法字符串字面量。它使用贪婪向前查看,并且空格是有意义的。例如:
SyntacticStringLiteral <- '"' (!'"' .) '"'
NonSyntacticStringLiteral <- DQ #((!DQ .) DQ)
如果没有在规则 NonSyntacticStringLiteral
上使用词法化操作符,它将消耗第一个引号后面的空格,这对于字符串字段可能是不可取的。
SyntacticStringLiteral
规则不需要词法化操作符,因为它的所有子表达式都是终端的,因此规则是语法的,默认情况下不会生成空间消耗。
lexification运算符的用例肯定不止这些,这里只列出了常见的用例。
带标签的错误报告
导入系统
一个语法的生成式可以导入另一个语法的生成式。这允许重用规则,生成更统一的语法文件和更强大的解析器。
// 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
构造函数等。