)
Go 语言 PEG 解析器生成器 pigeon 完全指南从文法设计到生成代码附 OpenCloud KQL 实战案例【免费下载链接】opencloud️ OpenCloud is the open source platform for file management, sharing and collaboration. Simple and sovereign.项目地址: https://gitcode.com/GitHub_Trending/op/opencloud导读本文以 OpenCloud 仓库中随附的 vendor/github.com/mna/pigeon/README.md 为骨架系统讲解 pigeon —— 一个基于 Parsing Expression GrammarPEG解析表达式文法为 Go 生成递归下降解析器的命令行工具。读完本文你将掌握 pigeon 的安装、命令行选项、完整 PEG 语法含标签、谓词、状态存储、左递归、失败标签等高级特性、生成解析器的公开 API 与错误处理方式并通过 OpenCloud 中真实使用的 KQL 搜索查询解析器pkg/kql/dictionary.peg看到它的端到端工程实践。pigeon 是什么pigeon命令根据一份解析表达式文法PEG生成 Go 解析器。它的文法与语法深受 [PEG.js 项目]启发实现则大致参考了《Parsing Expression Grammar Support for C# 3.0》一文。pigeon 解析的是以 UTF-8 编码的 Unicode 文本。从源码结构看pigeon 本体由几个核心包组成全部位于 vendor/github.com/mna/pigeon 目录astPEG 文法的抽象语法树表示以及语法树优化ast_optimize.gobuilder把文法 AST 生成最终 Go 解析器源码的代码生成器builder.gomain.go命令行入口负责参数解析、文法解析与生成代码的格式化输出。pigeon 生成的解析器不依赖任何第三方库除非文法中的代码块code block自己引入了外部依赖同样pigeon 工具本身在安装与运行上也没有第三方依赖要求。生成代码默认会经过golang.org/x/tools/imports做格式化与 import 整理见 main.go。发布版本与维护状态v1.0.0是原始实现的正式 tag 版本目前处于维护模式仅修 bugv2.0.0的开发已经启动计划包含一些破坏性变更当前 master 分支上的 2.0 API 尚未稳定随时可能变化。作者 [mna] 于 2015 年 4 月创建了该项目[breml] 自 2017 年 5 月起担任维护者。Release 策略跟随 Go 安全策略自 2023 年 6 月起pigeon的向后兼容支持改为遵循官方的Go Security Policy。官方 README 给出了这一决策的三点理由golang.org/x/tools等关键依赖遵循官方安全策略而 pigeon 若不跟进将无法引入这些依赖的新版本也就无法获得关键 bug 修复社区对代码质量的要求在演进例如从interface{}迁移到any遵循良好实践的用户对 pigeon 生成的代码有更高期望过去几年遵循 Go 安全策略的体验顺畅定期升级 Go 版本对用户而言是合理的要求。因此使用 pigeon 的团队也应同步关注 Go 的版本更新节奏。安装 pigeon前提是正确安装 Go 并设置好$GOPATH与$GOBIN环境变量然后执行$ go get -u github.com/mna/pigeon该命令会安装或更新包pigeon命令将被安装到你的$GOBIN目录。在 OpenCloud 仓库中pigeon 以 vendor 形式随附在 vendor/github.com/mna/pigeon并由 pkg/kql/gen.go 通过go:generate指令直接调用//go:generate go run github.com/mna/pigeon -optimize-grammar -optimize-parser -o dictionary_gen.go dictionary.peg这是推荐的工程化用法文法改动后执行go generate ./...即可重新生成解析器无需全局安装。基本用法与命令行选项$ pigeon [options] [PEG_GRAMMAR_FILE]默认情况下文法从stdin读取生成的代码打印到stdout可以用-o参数把生成代码写入文件。若同时指定文法文件路径则从文件读取。完整选项参考以下选项定义可在 main.go 中逐一对应选项类型/默认值说明-cachebool默认 false缓存解析结果避免病态情况下指数级解析时间对典型场景可能更慢、更占内存-debugbool默认 false解析文法时输出调试信息-nolintbool默认 false为生成代码添加// nolint: ...注释抑制 gometalinter / golangci-lint 的告警-no-recoverbool默认 false不从 panic 中恢复。调试时便于拿到 panic 调用栈否则 panic 会被转换为 error-o FILEstring默认 stdout生成解析器的输出文件-optimize-basic-latinbool默认 false为每个字符类匹配器生成 Unicode 前 128 字符Basic Latin的查找表当待解析文本主要由该区间字符构成时可显著提速-optimize-grammarbool默认 false实验特性对文法做多轮性能优化重点降低文法深度。包括删除未被引用的规则、将无自引用的规则引用替换为规则副本、展开嵌套 choice、消解单分支 choice、展开嵌套 sequence、消解单元素 sequence、尽可能合并字符类匹配器与字面量匹配器。优化后文法通常更耗内存但解析更快-optimize-parserbool默认 false生成不含 Debug、Memoize、Statistics 代码的优化解析器若文法无状态变更表达式还会移除全局 state 相关代码或省去 action/predicate 后对 state 的恢复节省 CPU 周期-xbool默认 false只解析文法语法检查不生成解析器代码-receiver-name NAMEstring默认c生成方法来自文法代码块的接收者变量名-alternate-entrypoints RULE[,RULE...]逗号分隔规则名除文法首条规则外额外允许作为解析入口的规则名配合-optimize-parser使用某些规则可能被优化掉。传给Parse的Entrypoint选项使用-support-left-recursionbool默认 false实验特性支持左递归规则含间接左递归如expr expr * term / expr term若文法中的代码块符合 golint 与 go vet 规范则生成的代码同样符合这些规范。一个完整的计算器示例这是 README 给出的经典示例用一份文法生成能解析简单四则运算支持括号、负数、正负号优先级的解析器。文法文件头部的花括号块是初始化代码块initializer会被原样复制到生成文件顶部{ // part of the initializer code block omitted for brevity var ops map[string]func(int, int) int { : func(l, r int) int { return l r }, -: func(l, r int) int { return l - r }, *: func(l, r int) int { return l * r }, /: func(l, r int) int { return l / r }, } func toAnySlice(v any) []any { if v nil { return nil } return v.([]any) } func eval(first, rest any) int { l : first.(int) restSl : toAnySlice(rest) for _, v : range restSl { restExpr : toAnySlice(v) r : restExpr[3].(int) op : restExpr[1].(string) l opsop } return l } } Input - expr:Expr EOF { return expr, nil } Expr - _ first:Term rest:( _ AddOp _ Term )* _ { return eval(first, rest), nil } Term - first:Factor rest:( _ MulOp _ Factor )* { return eval(first, rest), nil } Factor - ( expr:Expr ) { return expr, nil } / integer:Integer { return integer, nil } AddOp - ( / - ) { return string(c.text), nil } MulOp - ( * / / ) { return string(c.text), nil } Integer - -? [0-9] { return strconv.Atoi(string(c.text)) } _ whitespace - [ \n\t\r]* EOF - !.生成解析器后即可解析如下算式注意-27 * (-18 / -3)中内层括号、负数与除法的优先级处理18 3 - 27 * (-18 / -3) -141这个例子同时展示了几个关键机制带标签的子表达式如expr:Expr标签expr在 action 代码块中作为变量可见action 代码块紧随表达式之后的{ ... }接收标签值作为参数必须返回(any, error)其中c是默认接收者名c.text是当前匹配的字节切片choice 表达式Factor规则中用/分隔的两个分支按顺序尝试取第一个匹配的分支not 谓词EOF - !.中.匹配任意单个字符!.即没有更多字符用于判定输入结束。PEG 文法语法详解pigeon 接受的文法语法在 doc.go 中有形式化定义。以下是其非正式但完整的说明。词法约定标识符、空白、注释与字面量遵循 Go 语言规范//单行注释、/* ... */多行注释、x单引号单字符字面量、...双引号字符串字面量、反引号原始字符串字面量、RuleName形式的合法标识符。文法必须是 UTF-8 编码的 Unicode 文本换行以\nU000A标识空格U0020、水平制表U0009与回车U000D视为空白仅用于分隔 token。规则Rules文法由一组规则构成标识符 [显示名] 规则定义符 表达式。可选的显示名是字符串字面量用于在错误消息中替代规则标识符RuleA friendly name a // RuleA 匹配一个或多个小写 a规则定义符可以是、-、←U2190或⟵U27F5。表达式Expressions规则体是一个表达式。表达式可用括号分组也可以引用其他规则。以下是全部表达式类型。Choice 表达式按定义顺序依次尝试的候选列表以/分隔取第一个匹配者ChoiceExpr A / B / C正因为首个匹配优先顺序至关重要。例如下面规则中永远不会被用到因为在前BadChoiceExpr / Sequence 表达式由空白分隔的表达式序列必须按顺序全部匹配SeqExpr A b c // 匹配 Abc不匹配 AcbLabeled 表达式带标签标识符 : 表达式引入一个可在同作用域代码块中引用的变量。变量类型是空接口any底层类型由表达式决定终结符字符/字符串字面量、字符类、任意匹配器值为[]byte如Rule label:a中label是[]byte谓词与!值恒为nil如Rule label:a序列值为[]any每个元素对应序列中一个表达式的值递归适用同样规则如Rule label:(a b)重复与*值为[]any每个元素是一次重复的值如Rule label:[a-z]choice值为实际匹配分支的值如Rule label:(a / b)中label是[]byte可选?值为nil或该表达式的值如Rule label:a?。一旦出现 action 代码块值类型可以被任意改变RuleA label:3 { return 3, nil } RuleB label:RuleA { // 此时 label 是 int // ... }And / Not 谓词表达式前缀是and谓词当后续表达式匹配时整个表达式匹配但不消费任何输入前缀!是not谓词当后续表达式不匹配时匹配同样不消费输入AndExpr A B // 匹配 A要求其后紧跟 B不消费 B NotExpr A !B // 匹配 A要求其后不是 B不消费 B与!之后也可以是代码块此时代码块必须返回(bool, error)在返回 true 时匹配!在返回 false 时匹配。代码块可访问其作用域内所有标签值CodeAndExpr value:[a-z] { // 可访问局部变量 value return true, nil }重复表达式后缀*零或多次、?零或一次、一次或多次。匹配是贪婪的会尽可能多地匹配ZeroOrMoreAs A*字面量匹配器尝试把输入与单个字符或字符串字面量匹配支持单引号单字符、双引号字符串、反引号原始字符串转义规则同 Go。字面量后跟小写i在结束引号之外表示大小写不敏感匹配LiteralMatch Awesome\ni // 匹配 awesome 加换行字符类匹配器方括号[...]内匹配一个字符类。括号内字符表示自身转义规则与字符串字面量相同但单双引号转义无效必须转义]才能使用。支持区间[a-z]Unicode 类[\pL]L 为单字母 Unicode 类或[\p{Class}]如[\p{Latin}]大小写不敏感后缀i[a-z]i取反以^开头表示反转[^a-z]i表示匹配不在该类的字符。Any 匹配器点.匹配除文件结束符之外的任意单个字符。因此!.表示文件结束AnyChar . // 匹配单个字符 EOF !.代码块Code Block代码块写在花括号{...}内共分三种另外还有一类特殊的状态变更块。初始化代码块initializer必须出现在文法最前面、任何规则之前。其内容去掉花括号被原样复制到生成解析器的顶部可包含函数、类型、变量等任意 Go 代码声明的符号对所有其他代码块可见。虽然文法上可选但通常需要它来生成合法的 Go 源文件如 package 子句{ package main func someHelper() { // ... } }Action 代码块声明在规则中某表达式之后被生成为*current类型上的方法。方法以任意标签表达式值为参数any必须返回两个值表达式值any与error。若返回非 nil error会被加入解析器最终返回的错误列表RuleA A { // 返回匹配到的字符串c 是 *current 接收者变量的默认名 return string(c.text), nil }Predicate 代码块紧跟在或!之后同样生成为*current方法必须返回(bool, error)RuleAB [ab]i { return true, nil }状态变更代码块State change code block以#开头的代码块#{...}。与 action/predicate 不同它允许修改全局 state 存储中的值返回类型是error。注意若返回非 nil error解析器不会回溯Rule [a] #{ c.state[a] if c.state[a] 5 { return fmt.Errorf(we have seen more than 5 as) // 解析器不会回溯 } return nil }*current类型的四个字段生成代码中*current类型为 action、predicate、state 代码块提供四个字段字段类型说明posposition结构体解析器在输入中的当前位置含line1 起始行号、col1 起始列号按 rune 计、offset0 起始字节偏移三个 int 字段text[]byte当前匹配的字节切片在 predicate 代码块中为空statemap[string]any全局存储支持回溯规则匹配失败时匹配过程中对 state 的全部修改都会被回滚。action/predicate 中的修改在块结束后即被还原更新 state 必须用#{...}块globalStoremap[string]any全局存储与 PEG 回溯机制完全独立回溯不会回滚其中的写入state的克隆与回滚机制值得特别注意为保证规则失败时正确回滚解析器在尝试规则前必须克隆 state。默认是浅拷贝指针、map、slice、channel 以及包含上述类型的结构体不会被正确复制。为此 pigeon 提供Cloner接口含单一方法Clone存入 state 的值若实现了该接口就会调用Clone获取正确副本需要通用深拷贝方案时可在Clone方法中引入外部深拷贝库。globalStore的初始化可通过生成解析器的GlobalStore(key, value)选项完成所有以_pigeon开头的 key 均为内部保留不得使用或修改。左递归Left recursion开启-support-left-recursion后pigeon 支持左递归规则含间接左递归实现基于 Warth 等人的左递归 PEG 论文expr expr * term间接递归同样可用A B / D B A / C失败标签、throw 与 recover错误恢复扩展pigeon 扩展了经典 PEG 语法支持 Maidl 等人论文《Error Reporting in Parsing Expression Grammars》提出的**失败标签failure labels**机制语法借鉴自 lpeglabel 实现。该机制可区分普通失败与错误普通失败通常是字符匹配失败会被有序 choice 捕获错误由 throw 操作符产生可由 recovery 操作符捕获。recover 表达式RegularExpr //{FailureLabel1, FailureLabel2} RecoveryExpr——先尝试普通表达式若以给定标签之一失败则尝试恢复表达式恢复也失败则错误继续传播throw 表达式%{FailureLabel1}——主动抛出带标签的错误。实现原理recover 表达式把每个失败标签的恢复表达式压入恢复栈后运行普通表达式throw 表达式按逆序检查恢复栈找到标签则运行对应恢复表达式成功则继续处理输入失败则整体解析失败并开始回溯。若 throw/recover 与全局 state 一起使用文法作者有责任在恢复操作中把 global state 重置为合法状态。使用生成的解析器公开 APIpigeon 生成的解析器导出以下符号可作为一个包被外部调用Parse(string, []byte, ...Option) (any, error) ParseFile(string, ...Option) (any, error) ParseReader(string, io.Reader, ...Option) (any, error)以及如下选项函数OptionAllowInvalidUTF8(bool) Option Debug(bool) Option Entrypoint(string) Option // 指定备用入口规则需配合 -alternate-entrypoints GlobalStore(string, any) Option MaxExpressions(uint64) Option Memoize(bool) Option Recover(bool) Option Statistics(*Stats) Option与文法一样输入文本必须是 UTF-8 编码的 Unicode。起始规则是文法中的第一条规则。Parse*返回文法在输入上执行后产生的值any与可选的 error。文法通常会构建某种抽象语法树AST但简单文法也可以立即求值如上面的计算器pigeon 对返回值不做任何约束。错误报告与 errList / parserError当解析器返回非 nil error 时其类型恒为errList[]error列表中的每个错误都是*parserError该结构体含Inner字段可访问原始错误。例如 action 中返回io.EOF_, err : ParseFile(some_file) if err ! nil { list : err.(errList) for _, err : range list { pe : err.(*parserError) if pe.Inner io.EOF { // ... } } }默认行为是出错后继续解析并累积全部错误。若文法到达不应继续的位置可在 action 中用panic终止解析——顶层Parse*会捕获 panic 并转换为*parserError照常返回errList。README 中的计算器文法正是利用这一点处理除零无需专门代码运行时 panic 被恢复并作为解析错误返回。良好的错误报告需要文法作者参与设计。pigeon 已提供文件名、位置、期望字面量与规则名等信息但更重要的是文法层面的设计。例如处理字符串字面量开引号找到后即期望闭引号找不到时不应回溯尝试其他分支而应记录错误并消费匹配StringLiteral ValidStringChar* { // 合法情况构建字符串字面量节点 // node ... return node, nil } / ValidStringChar* ! { // 非法情况构建替换节点或 BadNode // node ... return node, errors.New(string literal not terminated) }由于errList与parserError未导出当生成解析器作为库包被其他包使用如多个 CLI 工具共用同一解析器时需要额外处理一种可行的做法是基于接口定义导出错误类型并用caret 风格格式化失败位置pigeon 的 json 示例及其命令行工具提供了参考实现。API 稳定性承诺v1.0生成解析器把用户代码与 pigeon 代码混在同一个包中没有包边界阻止访问未导出符号——实现细节对用户代码同样可见但不应使用。pigeon 明确划定了 v1.0 API 的稳定范围类似 Go 1 兼容性承诺命令行 flag 与参数不会被移除语义保持不变生成解析器显式导出的 API上述Parse*函数与 OptionPEG 语法本身本文档描述的全部语法代码块除初始化块外总是生成在*current类型上且该类型保证有pos类型position与text类型[]byte字段其他字段与方法无保证position类型总是有line、col、offset三个 int 字段Parse*返回的 error非 nil 时类型总是errList[]error除实现 error 接口外无方法保证errList中的单个错误总是*parserError保证有Inner字段。上述承诺针对 v1.0.0已进入维护模式。master 分支面向 2.0为改进 pigeon 可能引入必要的破坏性变更。OpenCloud 实战用 pigeon 构建 KQL 搜索查询解析器OpenCloud 仓库在 pkg/kql 包中真实使用 pigeon为全文搜索构建了一个 KQLKeyword Query Language风格查询解析器——这是理解 pigeon 端到端工程价值的最佳案例。文法设计dictionary.peg文法文件以初始化代码块声明package kql随后定义了一组清晰分层的规则入口与结构AST - n:Nodes构建整棵 ASTNodes - (_ Node)表示任意多个由空白分隔的节点Node在分组、属性限制、布尔运算符、自由文本关键字之间按序选择分组GroupNode支持可选的 key、可选的冒号/等号运算符以及括号包裹的子表达式( v:Nodes )属性限制YesNoPropertyRestrictionNodekey:true/false、DateTimeRestrictionNode支持、、、、、:六种运算符以及today、yesterday、last 7 days等自然语言时间、NumberRestrictionNode、TextPropertyRestrictionNode区分精确匹配与:模糊匹配自由文本PhraseNode带引号的短语与WordNode[^ :()]单词布尔运算符AND/、NOT/-、OR时间FullDateYYYY-MM-DD、FullTime含可选小数秒与Z/时区偏移、DateTimeFullDate T FullTime、NaturalLanguageDateTime辅助规则Char[A-Za-z]、Key点分属性名Char (. Char)*、String双引号字符串、Number、Digit、空白_[ \t]*。文法中大量使用了带标签表达式与 action 代码块例如NumberRestrictionNodeNumberRestrictionNode - k:Key o:( OperatorGreaterOrEqualNode / OperatorLessOrEqualNode / OperatorGreaterNode / OperatorLessNode ) ? v:Number ? { return buildNumberNode(k, o, v, c.text, c.pos) }生成与使用gen.go 中的go:generate指令开启了-optimize-grammar -optimize-parser两个优化选项产物是 4400 余行的 dictionary_gen.go——文件头部有// Code generated by pigeon; DO NOT EDIT.标记内容由文法 AST 展开为grammar、rule、actionExpr、charClassMatcher、litMatcher等运行时结构对应 builder 的代码生成模板。factory.go 中的build*函数是 action 代码块与业务 AST 之间的桥梁它们把 pigeon 的positionpos.line/pos.col转换为 pkg/ast/ast.go 定义的带源码位置的 AST 节点Location、Base、StringNode、BooleanNode、DateTimeNode、NumberNode、OperatorNode、GroupNode等并实现运算符归一化→AND、-→NOT与自然语言时间到/双节点区间的展开。业务侧通过 kql.go 的Builder.Build调用生成的Parse(, []byte(q))并把返回的errList/*parserError用errors.As逐层解包优先透出parserError.Inner中的原始错误——这正是上文错误报告一节所述错误类型在真实项目中的典型处理方式。从这一案例可以看出 pigeon 的工程价值用一份可读的文法声明一次性得到解析、错误位置、回溯与优化能力业务代码只需专注于把解析结果转换成自己的 AST。贡献与许可证贡献指南见 vendor/github.com/mna/pigeon/CONTRIBUTING.md项目采用BSD 3-Clause 许可证全文见 vendor/github.com/mna/pigeon/LICENSE。总结pigeon 把 PEG 文法与 Go 代码生成无缝衔接文法即代码grammar as code一份文法同时定义了语法规则、AST 构建与错误恢复策略。对 OpenCloud 而言pkg/kql 证明了它在生产级搜索查询解析中的可靠性——从dictionary.peg的文法声明到go:generate触发的代码生成再到factory.go的 AST 构建整条链路清晰、可维护、可复现。如果你的项目也需要一个可自定义、零运行时依赖的解析器pigeon 是值得纳入工具箱的选择。【免费下载链接】opencloud️ OpenCloud is the open source platform for file management, sharing and collaboration. Simple and sovereign.项目地址: https://gitcode.com/GitHub_Trending/op/opencloud创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考