#语法 #解析器生成器 #表达式 #输入 #递归 #语言 #简洁

langlang_lib

langlang 是一个基于解析表达式语法的解析器生成器(库)

3 个版本

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

#420 in 编程语言


langlang 中使用

GPL-3.0-or-later

195KB
4K 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语言²
  • 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

这将会将您带到交互式shell,允许您尝试不同的输入表达式。

请参阅存储库根目录下的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(label)

有序选择

此运算符一次尝试一个表达式,从左到右,并在第一个成功时停止。如果没有替代方案工作,则引发错误。例如。

SomeDigits <- '0' / '1' / '2' / '3' / '4'

6 传递给上述表达式将生成一个错误。

谓词(非/与)

谓词是不消耗任何输入的机制,允许无限向前查看。例如。

BracketString <- "[" (!"]" .)* "]"

在上面的例子中,如果解析器找到闭合的方括号,则不会评估 任何 表达式。

谓词 (&) 只是 !! 的语法糖。

重复(零次或多次)

  • 零个或多个:它永远不会失败,因为它至少可以匹配其表达式零次。

  • 一个或多个 是调用表达式一次的语法糖,然后对该相同表达式应用零个或多个。它可以在第一次匹配表达式时失败。

  • 可选 它将匹配一个表达式零次或一次。

词法化

默认情况下,生成的解析器会在每个非语法的生成项序列之前自动消费空白。如果所有表达式都是语法的,则认为生成是语法的。如果表达式的输出树仅由终结符匹配组成,则认为表达式是语法的。如果存在通向非终结符匹配的任何路径,则整个表达式和生成都是非语法的。例如。

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 不是语法的。但是,因为跟随非终结符的表达式被标记为词法化运算符,所以不会在非终结符的调用和带有语法后缀的有序选择(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: [genall] 生成器接口,所有目标共享
  • SML: gengo 缓存结果以确保 O(1) 解析时间
  • SML: gengo 在竞技场中分配输出节点
  • MID: genpy Python 代码生成器:从头开始
  • MID: genjs Java Script 代码生成器
  • MID: gengo 探索生成 Go ASM 代码而不是文本
  • MID: 显示调用图以进行调试
  • BIG: 从手写的解析器启动,以便语法编写者可以利用解析器生成器内置的功能

依赖关系

~3–11MB
~109K SLoC