gocc 实战:手把手用 50 行 BNF 写出一个四则运算计算器
【免费下载链接】goccParser / Scanner Generator项目地址: https://gitcode.com/gh_mirrors/go/gocc
想要快速入门 gocc 这个 Go 语言写的 Parser / Scanner Generator(解析器生成器)吗?本文将从零开始,手把手带你用一份约 50 行的 BNF 语法文件,生成一个支持加减乘除和括号的四则运算计算器。不需要手写词法分析器,也不用手写递归下降解析器,gocc 会替你自动生成完整可运行的 Go 代码,让你 10 分钟就能跑通第一个属于自己的编译器前端。下面我们就从 gocc 的安装开始,一步步完成这个实战项目。
gocc 是什么?为什么值得用它
gocc 是一个完全用 Go 编写的编译器工具包(compiler kit),它的核心能力是从一个 BNF 语法文件生成词法分析器(Lexer/Scanner)和语法分析器(Parser)。
- 词法分析器本质是 DFA(确定性有限自动机),支持 UTF-8 输入,负责把字符串切成一个个 token;
- 语法分析器本质是 PDA(下推自动机),识别 LR(1) 文法,并支持自动解决 shift/reduce 和 reduce/reduce 冲突。
你只需要把精力放在定义语法规则和编写语义动作上,剩下的琐碎工作全部交给 gocc。这也让它成为学习编译原理、快速验证语法的绝佳工具。
gocc 一键安装方法
在动手之前,先确认你已经装好了 Go 环境(1.16 以上即可)。然后克隆源码并编译安装:
git clone https://gitcode.com/gh_mirrors/go/gocc cd gocc go install .安装完成后,确保gocc命令在你的 PATH 中,输入gocc -h能打印帮助信息即表示成功。
三步搭建计算器项目结构
我们采用和官方 calc 示例一致的目录结构,创建以下三个文件即可:
calc.bnf:语法文件,唯一需要你手写的核心;calc_test.go:测试文件,验证计算器是否正确;Makefile:一键重新生成代码的快捷方式。
参考官方示例文件:calc.bnf、calc_test.go、Makefile。
手写 50 行 BNF 语法文件:核心实战
下面是整个项目的灵魂——calc.bnf,它分为词法部分和语法部分:
/* Lexical part */ _digit : '0'-'9' ; int64 : '1'-'9' {_digit} ; !whitespace : ' ' | '\t' | '\n' | '\r' ; /* Syntax part */ << import ( "github.com/goccmack/gocc/example/calc/token" "github.com/goccmack/gocc/example/calc/util" ) >> 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) >> ;别被吓到,拆开看其实非常简单:
词法规则怎么写:数字与空白
_digit : '0'-'9' ;定义了一个辅助符号(下划线开头),表示单个数字;int64 : '1'-'9' {_digit} ;定义整数 token:首位不能是 0,后面可以跟任意多个数字,{...}表示重复;!whitespace : ... ;以!开头表示忽略该符号,空白字符不会进入 token 流,这样我们就能写1+2而不是1 + 2。
语法规则与语义动作怎么写
<< ... >>之间的内容就是语义动作(Action Expression),语法上等价于 Go 的表达式列表,要求返回(Attrib, error):
$0、$1、$2分别代表产生式右侧第 0、1、2 个符号的返回值;Expr "+" Term << $0.(int64) + $2.(int64), nil >>表示加法:把左操作数(Expr)和右操作数(Term)相加返回;Factor : "(" Expr ")" << $1, nil >>表示括号:$1就是括号内的 Expr 值;int64 << util.IntValue($0.(*token.Token).Lit) >>表示把数字 token 的文本转成 int64,IntValue是 gocc 自动生成的工具函数,见 litconv.go。
注意这里把加法和乘法分成了Expr和Term两层,这正是处理运算优先级的经典手法:乘法先结合、优先级更高,所以1 + 2 * 3的结果是7而不是9。
一键生成词法分析器与解析器代码
文件写好后,执行:
gocc calc.bnfgocc 会自动在当前目录下生成lexer、parser、token、util、errors五个子包。以后改了语法想重新生成,直接make regenerate即可(见 Makefile)。如果生成过程中出现 LR(1) 冲突,可以用gocc -a calc.bnf让 gocc 自动解析冲突。
写测试验证计算器:两分钟跑通
最后用一段测试代码验证计算结果是否正确,参考官方 calc_test.go:
func Test1(t *testing.T) { p := parser.NewParser() for _, ts := range testData { s := lexer.NewLexer([]byte(ts.src)) sum, err := p.Parse(s) if err != nil { t.Error(err) } if sum != ts.expect { t.Errorf("Error: %s = %d. Expected %d\n", ts.src, sum, ts.expect) } } }核心用法只有三步:
lexer.NewLexer([]byte(输入字符串))创建词法分析器;parser.NewParser()创建解析器;p.Parse(s)解析并返回结果。
运行go test -v,如果输出PASS,恭喜你,你的第一个 gocc 四则运算计算器已经成功诞生了!
进阶:从计算器到真正的编译器
这个小计算器虽然简单,却包含了编译器前端的完整骨架。想进一步深入,官方仓库里还有更多值得参考的实战示例:
- bools/example.bnf:带 AST 构建的布尔表达式示例;
- errorrecovery/er.bnf:错误恢复机制演示;
- sr/sr.bnf 与 rr/rr.bnf:shift/reduce 与 reduce/reduce 冲突处理示例;
- usercontext/example.bnf:自定义解析上下文的进阶玩法。
这些示例都用相同的目录结构组织,看懂一个就能看懂全部。而 gocc 的完整语法规范定义在 spec/gocc2.ebnf,即"用 BNF 描述 BNF",非常有趣。
总结
从安装 gocc、编写 50 行 BNF,到一键生成代码并测试通过,你已经完整体验了 gocc 这个 Parser / Scanner Generator 的整个工作流程。用 gocc 生成解析器的最大好处是:语法规则即代码,改一行 BNF 就能重新生成新的解析器,让词法分析与语法分析的开发效率提升一个量级。现在就去动手试试吧,把上面的 BNF 改成支持减法、除法和更多运算符,你会发现 gocc 的世界比想象中更有趣。
【免费下载链接】goccParser / Scanner Generator项目地址: https://gitcode.com/gh_mirrors/go/gocc
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考