gocc 动作表达式实战:用 << >> 语义动作实现语法导向翻译(SDT)

📅 2026/8/21 13:50:19
gocc 动作表达式实战:用 << >> 语义动作实现语法导向翻译(SDT)
gocc 动作表达式实战用 语义动作实现语法导向翻译SDT【免费下载链接】goccParser / Scanner Generator项目地址: https://gitcode.com/gh_mirrors/go/goccgocc 是一款用 Go 编写的解析器生成器Parser / Scanner Generator它可以从 BNF 文法文件一键生成词法分析器和 LALR(1) 语法分析器。而在 gocc 所有特性中最让新手心动的是它的动作表达式Action Expression直接在文法里用 写 Go 代码让语法规则在归约的瞬间执行计算这就是经典的语法导向翻译SDT。本文带你从零上手用真实的 gocc 示例文件一步步跑通 SDT看完你也能自己写一个迷你计算器或表达式求值器。什么是 gocc 动作表达式传统编译器的做法是先解析出语法树再写一整套遍历代码去求值。而 gocc 提供了一条更短的路径——把语义动作直接嵌进 BNF 文法。每条产生式的后面可以跟一个 ... 代码块当解析器识别并归约出这条产生式时就立刻执行其中的 Go 表达式。一个最朴素的例子来自官方示例 calc.bnfFactor : ( Expr ) $1, nil | int64 util.IntValue($0.(*token.Token).Lit) ; $1, nil 的意思是把括号里 Expr 的属性原样返回 util.IntValue(...) 则是把数字 token 的文本转换成 int64。解析和求值在同一趟完成这正是 SDT 的核心思想。gocc 动作表达式的核心语法属性引用与返回值想用好动作表达式只需记住两组符号$0、$1、$2…按顺序引用产生式右侧各符号的属性。例如Term : Term * Factor $0.(int64) * $2.(int64), nil 这里的$0是左操作数、$2是右操作数*本身用$1引用它是个 token。$T0、$T1…当属性确定是*token.Token时可以用这个简写直接取$T0.Lit拿到原始文本例如 astx 示例 中的 ast.NewStmt($T0) 。动作表达式必须返回(interface{}, error)这个二元组error 为nil时返回值作为该产生式的属性压回解析栈继续参与上层计算error 非nil时解析立即终止并把错误抛给调用方。如果某条产生式没有写动作gocc 默认把$0当作属性行为很像透传。实战一用 gocc 语义动作实现四则运算最经典的 SDT我们直接拆解官方示例 calc.bnf它完整演示了如何在文法里完成解析即求值Expr : Expr Term $0.(int64) $2.(int64), nil | Term ; Term : Term * Factor $0.(int64) * $2.(int64), nil | Factor ;这个文法用左递归处理了运算符的优先级与结合性Expr管加减、Term管乘除。解析2 3 * 4时3 * 4先被归约成 12 放到栈上再由Expr算出2 12 14。整个过程没有生成任何中间语法树每个动作的返回值就是属性层层向上传递——这就是教科书里后缀语义postfix SDT的活教材。文法开头还需要一个文件头file header来导入动作里用到的包 import ( github.com/goccmack/gocc/example/calc/token github.com/goccmack/gocc/example/calc/util ) 它会被原样写入生成的 productionstable.go 顶部相当于给所有动作代码预留的公共 import 区。实战二用动作表达式构建 AST为复杂项目铺路当程序逻辑复杂时边解析边求值会显得拥挤此时更常见的做法是用动作表达式构造抽象语法树AST。官方示例 astx/ast.bnf 演示了这种模式StmtList : Stmt ast.NewStmtList($0) | StmtList Stmt ast.AppendStmt($0, $1) ;对应的 ast/ast.go 里定义了StmtList类型和NewStmtList、AppendStmt两个构造函数。动作表达式把属性直接喂给这些构造函数解析完成后你得到的不是一个字符串结果而是一棵结构化的 AST后续的类型检查、代码生成都在这棵树上进行。注意动作里用到的ast包也需要在文件头里import。进阶技巧用 $Context 在 gocc 动作中访问解析上下文如果你希望动作能感知当前解析到哪里比如记录符号名、检查重复定义gocc 提供了特殊的$Context标记对应解析器对象上的Context字段。官方示例 usercontext/example.bnf 里是这样用的Words : lowercase ast.NewIdentifier($0, $Context) | Words lowercase ast.NewIdentifier($1, $Context) ;此时 ast/main.go 中的NewIdentifier就能拿到一个自定义的ParserContext结构体在里面维护符号表、回调函数等状态。这套机制让 gocc 的语义动作从纯函数升级为有状态的翻译器非常适合写语言前端。新手常踩的 4 个坑类型断言别写错动作参数的类型都是interface{}使用前必须断言如$0.(int64)token 属性的Lit字段是[]byte记得转成string。返回值二元组不能缺(值, error)少一个gocc 编译文法时会直接报错。忽略空属性引用像Calc : Expr;这种单符号产生式不用写动作gocc 会自动传递$0写多了反而容易引错位置。冲突产生式注意优先级同一个非终结符的多个候选alternative动作要保证返回类型一致否则上层动作的类型断言会 panic。总结从calc.bnf的即时求值到astx的 AST 构建gocc 的动作表达式把语义和语法写进了同一份 BNF 文件让语法导向翻译SDT变得格外直观。动手建议把仓库克隆到本地后直接打开 example/calc/calc.bnf 和 example/astx/ast.bnf 对照阅读再试着给Term增加一个除法规则你会立刻感受到 gocc 语义动作的威力。【免费下载链接】goccParser / Scanner Generator项目地址: https://gitcode.com/gh_mirrors/go/gocc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考