尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
gocc 解析器生成器源码逐行解读:action table 与 LR(1) 运行时工作原理
gocc 解析器生成器源码逐行解读action table 与 LR(1) 运行时工作原理【免费下载链接】goccParser / Scanner Generator项目地址: https://gitcode.com/gh_mirrors/go/goccgocc 是一款用 Go 语言实现的解析器生成器Parser / Scanner Generator你只要写好一份 .bnf 语法文件它就能自动生成词法分析器lexer与 LR(1) 语法分析器parser的完整 Go 源码。本文以项目自带的计算器示例为例逐行解读 gocc 生成的解析器源码重点讲透 action table动作表的数据结构与运行时工作原理帮助新手彻底看懂 parser.go、actiontable.go、gototable.go 这些机器生成的文件。什么是 gocc 解析器生成器从 BNF 到可运行源码的完整流程gocc 的用法非常直观写好一份 EBNF 风格的语法描述文件如example/calc/calc.bnf运行 gocc它就会在目标目录下自动生成 5 个子包lexer/词法分析器负责把字符流切成 token见example/calc/lexer/lexer.goparser/语法分析器核心是 LR(1) 查表驱动的 shift/reduce 引擎token/token 类型定义与 TokenMapexample/calc/token/token.goerrors/错误类型封装example/calc/errors/errors.goutil/字面量转换等工具example/calc/util/litconv.go以计算器示例为例语法文件example/calc/calc.bnf的核心部分长这样Calc : Expr; Expr : Expr Term $0.(int64) $2.(int64), nil | Term; Term : Term * Factor $0.(int64) * $2.(int64), nil | Factor; Factor : ( Expr ) $1, nil | int64 util.IntValue($0.(*token.Token).Lit) ;生成流程在源码里也有迹可循internal/parser/gen/gen.go是代码生成的总入口它依次调用GenAction、GenActionTable、GenGotoTable、GenParser、GenProductionsTableLR(1) 项目集的构建发生在internal/parser/lr1/items最终通过internal/parser/gen/golang下的模板文件渲染输出。gocc 生成的解析器源码清单parser 包里的文件各司其职在example/calc/parser/目录下gocc 生成了 6 个文件每个文件开头都带着 Code generated by gocc; DO NOT EDIT. 标记action.go定义动作接口与三类动作类型actiontable.go核心的 action table动作表二维查找表gototable.go归约后的状态跳转表goto tableproductionstable.go产生式表内含语义动作ReduceFuncparser.go解析器主体含栈结构与 Parse 主循环context.go用户上下文接口其中 action table 是整个 LR(1) 解析器的大脑其余文件都围绕它运转。action table 数据结构逐行解读action.go 里的三类动作打开example/calc/parser/action.gogocc 只定义了一个接口和三个极简类型type action interface { act(); String() } type ( accept bool // 接受语法分析成功 shift int // 移入值是下一个状态的编号 reduce int // 归约值是产生式编号 )在 LR 解析理论中每一时刻解析器面对当前状态 当前 token只需回答一个问题接下来做什么gocc 给出的答案只有三种shift(n)把当前 token 移入栈并跳转到状态 n继续读下一个 tokenreduce(n)用第 n 条产生式把栈顶若干符号归约成一个非终结符accept整个输入已被接受语法分析成功结束表格里出现nil的位置则代表非法动作即语法错误会触发错误处理逻辑。actiontable.go 源码逐行解读一张二维查找表如何驱动语法分析example/calc/parser/actiontable.go的核心数据结构极其简洁type ( actionTable [numStates]actionRow actionRow struct { canRecover bool actions [numSymbols]action } )也就是说整张 action table 就是一个状态数 × 符号数的二维数组。以计算器为例numStates 2323 个 LR 状态、numSymbols 1212 种符号含终结符与非终结符。查表方式就是actionTab[栈顶状态].actions[当前token类型]一次数组下标访问O(1) 完成决策。看 S0 这一行初始状态就能明白它的含义actionRow{ // S0 canRecover: false, actions: [numSymbols]action{ nil, // INVALID nil, // ␚ (EOF) nil, // nil, // * shift(5), // ( nil, // ) shift(6), // int64 }, },含义初始状态 S0 下如果读到(就移入并跳转状态 5读到int64就移入并跳转状态 6其余 token 都是nil报错。每一行注释里 gocc 都贴心标注了对应的 token 名称读起来一目了然。canRecover字段则用于错误恢复时判断某个状态能否作为恢复点。gototable.go 与 productionstable.go归约后的去向与语义动作goto tableexample/calc/parser/gototable.go负责回答归约后去哪个状态type gotoTable [numStates]gotoRow type gotoRow [numNTSymbols]int // -1 表示无跳转例如 S0 行Calc → 1、Expr → 2、Term → 3、Factor → 4。当栈顶归约出一个非终结符时解析器就用gotoTab[当前栈顶][产生式的NTType]找到下一个状态并压栈。产生式表example/calc/parser/productionstable.go则把语法规则和语义动作绑定在一起type ProdTabEntry struct { String string // 产生式的可读描述 Id string // 左部非终结符名 NTType int // 左部在 goto 表中的列号 Index int // 产生式编号 NumSymbols int // 右部符号个数决定弹栈数量 ReduceFunc func([]Attrib, interface{}) (Attrib, error) }注意第 2 条产生式Expr : Expr Term的ReduceFunc是X[0].(int64) X[2].(int64)——这正是你在 .bnf 里写的 $0.(int64) $2.(int64), nil 语义动作被 gocc 编译后的形态。也就是说你在语法文件里写的语义代码最终会成为这里的一个闭包函数。运行时工作原理Parse 主循环中的 shift/reduce 完整流程现在看example/calc/parser/parser.go中最重要的Parse方法。它维护一个双数组栈stack.state存状态编号stack.attrib存对应的属性值token 或归约结果。主循环非常紧凑for acc : false; !acc; { action : actionTab[p.stack.top()].actions[p.nextToken.Type] switch act : action.(type) { case accept: res p.stack.popN(1)[0] // 取出最终结果 acc true case shift: p.stack.push(int(act), p.nextToken) // 移入 token 并跳转 p.nextToken scanner.Scan() // 读下一个 token case reduce: prod : productionsTable[int(act)] attrib, _ : prod.ReduceFunc(p.stack.popN(prod.NumSymbols), p.Context) p.stack.push(gotoTab[p.stack.top()][prod.NTType], attrib) } }整个运行时工作原理可以概括成四步循环查表以栈顶状态和当前 token 为下标从actionTab取动作移入shifttoken 压栈、状态跳转、读取下一个 token归约reduce按NumSymbols弹出若干栈项执行ReduceFunc计算属性值再按 goto 表压回新状态接受accept弹出最终结果解析完成打开调试开关internal/parser/gen/golang/parser.go模板中的Debug分支后每一步都会打印形如S0 int64 shift:6的日志是学习 LR 解析原理的绝佳工具。实例跟踪解析 23*4 的完整过程用上面的表手动推演一遍token 序列int64(2) int64(3) * int64(4) EOF步骤栈状态当前 token动作说明1S0int64shift(6)2 入栈2S0,S6reduce(7)Factor→int64得 23S0,S4reduce(5)Term→Factor得 24S0,S3reduce(3)Expr→Term得 25S0,S2shift(7)运算符 入栈6S0,S2,S7int64shift(6)3 入栈7S0,S2,S7,S6*reduce(7)Factor→int64得 38S0,S2,S7,S4*reduce(5)Term→Factor得 39S0,S2,S7,S14*shift(8)运算符 * 入栈10S0,S2,S7,S14,S8int64shift(6)4 入栈11S0,S2,S7,S14,S8,S6EOFreduce(7)Factor→int64得 412S0,S2,S7,S14,S8,S15EOFreduce(4)Term→Term*Factor3×41213S0,S2,S7,S14EOFreduce(2)Expr→ExprTerm2121414S0,S2EOFreduce(1)Calc→Expr15S0,S1EOFaccept结果为 14注意第 12、13 步因为*的归约先于发生3*4先被算成 1221214得到正确结果——这正是 LR 语法分析自动实现运算符优先级的过程无需任何手工处理。完整推演与测试代码见example/calc/calc_test.go。如何快速上手用 gocc 生成你的第一个解析器想动手体验 gocc 解析器生成器的完整流程只需三步获取源码git clone https://gitcode.com/gh_mirrors/go/gocc然后按根目录Makefile或gen.sh构建出 gocc 可执行文件写语法文件仿照example/calc/calc.bnf编写你自己的 .bnf 文件生成并测试在example/calc这样的示例目录下执行makegocc 会自动重新生成所有源码并运行单元测试项目还自带丰富的进阶示例可对照学习example/astx生成 AST、example/errorrecovery错误恢复、example/usercontext用户上下文等。更详细的 API 说明可以翻阅用户手册doc/gocc_user_guide.pdf。常见问题action table 太大、冲突与调试技巧Q状态多、表很大的时候生成的 actiontable.go 会不会很占空间Agocc 内置了压缩方案。当配置开启 zip 模式时见internal/parser/gen/golang/actiontable.go中的GenCompActionTableaction table 会被序列化后用 gzip 压缩再在init()中解压还原显著减小生成的 Go 源码体积。Q语法有歧义生成时报冲突怎么办Agocc 会报告具体的冲突位置和行。项目提供了example/srshift/reduce 冲突和example/rrreduce/reduce 冲突两个专门示例展示了冲突出现的原因与处理思路值得逐一调试理解。Q解析出错时如何定位A每个 action row 都带canRecover标记配合parser.go中的Error、popNonRecoveryStates方法实现错误恢复开启 Debug 后还能看到每一步的 shift/reduce 日志配合栈内容打印stack.String()即可精准定位问题。写在最后gocc 生成的解析器源码看似机器味很重但拆开来看核心不过是一张 action table 加一个 while 循环查表、移入、归约、接受周而复始。理解了 action table 与 LR(1) 运行时的工作原理你不仅能自信地使用 gocc 生成解析器也为读懂任何表驱动的语法分析器打下了坚实基础。想深入了解从internal/parser/lr1/items的项目集构造算法开始你会看到 LR 理论的优雅全貌。【免费下载链接】goccParser / Scanner Generator项目地址: https://gitcode.com/gh_mirrors/go/gocc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
RELATED

相关推荐

手机号码归属地查询完整指南:输入号码,地图自动标出城市

手机号码归属地查询完整指南:输入号码,地图自动标出城市

手机号码归属地查询完整指南:输入号码,地图自动标出城市 【免费下载链接】location-to-phone-number This a project to search a location of a specified phone number, and locate the map to the phone number location. 项目地址: https://gitcod…

📅 2026/10/6 20:43:31
大语言模型输出多样性瓶颈:校准问题与采样策略的深层解析

大语言模型输出多样性瓶颈:校准问题与采样策略的深层解析

你有没有遇到过这种情况:同一个大语言模型,同一个问题,你让它生成十次答案,结果前三条看起来还行,后面七条要么车轱辘话来回说,要么质量断崖式下跌,甚至开始胡言乱语?你可能会怀疑&a…

📅 2026/10/2 16:06:08
ContextMenuManager:免费右键菜单管理工具,隐藏、新增、排序一次到位

ContextMenuManager:免费右键菜单管理工具,隐藏、新增、排序一次到位

ContextMenuManager:免费右键菜单管理工具,隐藏、新增、排序一次到位 【免费下载链接】ContextMenuManager 🖱️ 纯粹的Windows右键菜单管理程序 项目地址: https://gitcode.com/gh_mirrors/co/ContextMenuManager ContextMenuManager…

📅 2026/10/6 17:12:35
MORE NEWS

更多资讯

📰

K8s部署XXL-JOB实战:YAML、镜像与执行器避坑指南

简介:面向需要在 Kubernetes 集群中落地定时任务调度的运维与后端开发人群,这份 XXL-JOB 容器化部署 YAML 可作为直接应用的部署清单,重点解决集群环境下任务调度组件编排与快速验证的问题。压缩包内仅含 1 个 YAML 文件,类型为 y…

📰

MinIO公开Bucket的正确姿势:绕过90%人的权限认知误区

1. 为什么“把MinIO的Bucket设成公开”这件事,90%的人第一步就做错了在某次给某高校实验室搭建私有对象存储平台时,一位导师直接发来截图:“我点了几下控制台,明明勾选了‘Public’,但前端页面还是403,是不…

📰

连接跟踪与流量统计:从conntrack到iptables的运维实战

连接跟踪流量统计,这是Linux网络运维里一个特别容易被忽视,但关键时刻能救命的角落。很多人搞iptables、搞防火墙规则精通得很,一说到“你线上每秒多少新建连接?连接表占用多少?到底哪个IP在持续往外发包?”…

📰

POI操作Word指定位置插入图片:占位符与游标定位实战

做过Word模板导出功能开发的人,多半都被“插入图片”这件事卡过。客户的描述永远很具体:报告生成到第三页,这一段下面必须放质检照片,或者一个大表格里,某些单元格要贴图。你翻遍POI的文档,找到addPicture这…

📰

Tracking Master:多供应商埋点统一跟踪层与Provider抽象

简介:Tracking Master 是一款面向 Bludit 扁平文件 CMS 用户与插件开发者的开源网络分析跟踪插件,重点解决轻量站点在不依赖数据库的前提下统计访问量、页面浏览与用户来源的问题,并提供多供应商分析服务的接入思路。压缩包共 12 个文件&…

📰

C语言编程200例:从拆解源码到实战改造的练习指南

简介:这是一套面向C语言初学者的编程实例合集,源自《C语言精彩编程200例》,按01至06分主题编排,覆盖变量、运算符、流程控制、函数、数组、指针等基础语法,并延伸至结构体、联合体、位运算、文件操作以及链表、栈、队列…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬