#解析生成器 #语法 #表达式 #递归 #语言 #错误 #空白符

app langlang

langlang 是一种基于解析表达式语法的解析生成器

3 个版本

0.1.2 2023年10月2日
0.1.1 2023年10月2日
0.1.0 2022年12月29日

#52 in 解析工具

GPL-3.0-or-later

155KB
3K SLoC

目录

  1. 简介
    1. 项目状态
    2. 当前支持输出语言
      1. 注意
    3. 基本用法
  2. 输入语言
    1. 产生式和表达式
    2. 终端
    3. 非终端
    4. 表达式组合
      1. 有序选择
      2. 谓词(非/与)
      3. 重复(零次或多次)
      4. 词法化
      5. 使用标签的错误报告
      6. 导入系统
  3. 生成器选项
    1. Go
  4. 路线图

简介

带上你的语法,为不同语言生成功能丰富的解析器。你可能会想使用这个工具的原因有很多

  • 简洁的输入语法格式和直观的算法:根据解析表达式语法生成递归自上而下的解析器
  • 自动处理空白符,使语法更加清晰
  • 通过失败 labels 使用自定义消息进行错误报告
  • 部分支持声明错误恢复规则,允许增量解析,即使在多次解析错误的情况下也能返回输出树。

项目状态

  • 我们还没有到 1.0 版本,所以 API 不稳定,这意味着数据结构形状可能会改变,行为也可能会有很大的变化,而且不会提前通知。
  • 在没有打开问题并讨论你的想法之前不要提交拉取请求。我们将采取缓慢的方法,首先追求伟大的设计,然后是稳定性,最后是功能丰富性。

当前支持输出语言

  • Rust¹
  • Go Lang²
  • Python
  • JavaScript
  • 编写你自己的代码生成器

注意

  1. Rust 支持基于虚拟机作为其运行时,而不是基于生成的解析器。这一点不太可能改变,因为我们的工具将用 Rust 编写,因此我们需要在“宿主实现”上有更多的灵活性。如果我们认为这具有任何价值,我们可能会提供一个选项来生成不依赖于任何库的 Rust 代码解析器。但这项工作目前没有计划。

  2. 我们正在中间阶段放弃 Go 实现,转而使用 Rust 代码生成 Go 解析器。原型设计了一些功能,如导入系统和自动空白符处理,非常有用,但一旦 Rust 实现的重构到位,Rust 版本将会更好,因为它将更容易为 Rust 和 Go 之外的其它语言生成解析器。

基本用法

如果您只是想试试水,将命令行实用程序指向一个语法,并选择一个起始规则

cargo run --bin langlang run --grammar-file grammars/json.peg --start-rule JSON

这会将您放入一个交互式外壳,允许您尝试不同的输入表达式。

请查看仓库根目录下的目录 grammars 中的其他示例。它包含常用输入格式的语法库。

输入语言

产生式和表达式

输入语法尽可能简单。它基于原始PEG格式,并保守地添加了其他功能。以下是一个示例输入

Production <- Expression

箭头左侧有一个标识符,右侧有一个表达式。这两个一起称为生成式或(解析)规则。让我们来了解一下如何组合它们。如果您曾经见过或使用过正则表达式,您已经领先一步。

终端

  • Any:匹配任何字符,只有在达到输入末尾时才会报错。例如:.

  • Literal:引号内的任何内容(单引号和双引号相同)。例如:'x'

  • Class and Range:类可以包含范围或单个字符。例如:[0-9][a-zA-Z][a-f0-9_]。最后一个示例包含两个范围(a-f0-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 调用了另一个包含非终结符的生成,所以 Ordinal 不是语法的。因此,为该生成启用了自动空间处理。但是,在非终结符和具有终结符的选择之间,空间处理被禁用。这是预期的

输入 结果
"3rd" 成功
"50th" 成功
"2 0th" 失败
"2 th" 失败

第一个输入成功,因为自动在非终结符调用 Decimal 的左侧添加了空间消耗,因为 Ordinal 不是语法的。但是,因为跟随非终结符的表达式被标记为词法化操作符,所以不会在非终结符的调用和具有语法后缀的有序选择(stndrdth)之间注入自动空间处理。

这是需要词法化的最经典示例之一:非语法的字符串字面量。它使用贪婪向前查看,并且空格是有意义的。例如:

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构造函数等。

路线图

  • MID:gengo用Rust重写的Go生成器
  • MID:生成器接口,由所有目标共享
  • SML:gengo将结果缓存以保证O(1)的解析时间
  • SML:gengo在竞技场中分配输出节点
  • MID:genpy Python代码生成器:从头开始
  • MID:genjs Java脚本代码生成器
  • MID:gengo探索生成Go汇编代码而不是文本
  • MID:显示调用图,用于调试目的
  • BIG:从手写的解析器启动,这样语法编写者可以利用解析器生成器内构建的功能

依赖关系

~4–14MB
~150K SLoC