
本文详解如何正确使用 Go 自带的 go tool yacc 编写符合语法规范的 Yacc 规则文件,修复常见语法错误(如非法首规则),并基于 calc.y 示例实现一个可运行的字符回显解析器。
本文详解如何正确使用 go 自带的 `go tool yacc` 编写符合语法规范的 yacc 规则文件,修复常见语法错误(如非法首规则),并基于 `calc.y` 示例实现一个可运行的字符回显解析器。
Go 标准工具链中的 go tool yacc 是对经典的 Berkeley Yacc 的兼容实现,用于生成 LALR(1) 语法分析器。但其输入格式严格遵循传统 Yacc 规范,不允许在 %% 后直接书写正则式动作(如 .|\n ECHO;)——这正是原问题中 bad syntax on first rule 错误的根本原因。
Yacc 文件必须严格按三段式结构组织:
%{ ... %} // Go 包声明与导入(可选)
%%
grammar rules // 语法规则:非终结符 : 右部 ; (冒号与分号不可省略)
%%
Go functions // 用户定义函数(如 main、Lex、Error)
✅ 正确的最小可运行示例(echo.y)
以下是一个经过验证、可直接编译运行的简化版 Yacc 文件,功能为逐字符读取输入并打印:
%{
package main
import (
"bufio"
"fmt"
"os"
"unicode"
)
// 声明 token 类型(需与 Lex 返回值匹配)
const CHARACTER = 256
%}
%union {
val string
}
%token <val> CHARACTER
%%
in : /* empty */
| in input '\n' { fmt.Printf("Read character: %s\n", $2) }
;
input : CHARACTER
| input CHARACTER { $$ = $1 + $2 }
;
%%
type InputLex struct {
s string // 输入字符串(含换行符)
pos int // 当前解析位置
}
func (l *InputLex) Lex(lval *yySymType) int {
if l.pos >= len(l.s) {
return 0 // EOF
}
c := rune(l.s[l.pos])
l.pos++
// 跳过空格(可选)
if c == ' ' || c == '\t' || c == '\r' {
return -1 // 忽略,继续扫描
}
// 仅识别小写字母和数字作为有效 token
if unicode.IsLower(c) || unicode.IsDigit(c) {
lval.val = string(c)
return CHARACTER
}
// 其他字符直接作为终结符返回(触发语法错误或自定义处理)
return int(c)
}
func (l *InputLex) Error(s string) {
fmt.Printf("syntax error: %s\n", s)
}
func main() {
fi := bufio.NewReader(os.Stdin)
line, err := fi.ReadString('\n')
if err != nil {
fmt.Fprintf(os.Stderr, "read error: %v\n", err)
return
}
// 初始化 lexer 并启动 parser
lex := &InputLex{s: line}
InputParse(lex)
}</val>
? 编译与运行
go tool yacc -o echo.go -p Input echo.y go run echo.go
⚠️ 注意事项:
- go tool yacc 不支持内联正则匹配(如 .|\n),所有词法分析逻辑必须在 Lex 方法中手动实现;
- %union 和 %token 声明用于类型安全传递语义值(yySymType 是自动生成的联合体);
- InputParse 函数名由 -p Input 参数决定(前缀 Input + Parse),需与 Lex 方法签名匹配(接收 *InputLex);
- 若未定义 %union,yySymType 默认为 int,无法传递字符串等复杂值。
? 总结
Go 的 yacc 工具并非“开箱即用”的现代解析器生成器,而是面向传统编译原理教学的轻量级实现。它要求开发者深入理解:
- Yacc 三段式语法结构(声明 / 规则 / 程序);
- Lex 方法需承担全部词法分析职责(无正则引擎支持);
- 语法规则必须以非终结符开头,且严格遵循 A : B C D ; 格式。
对于新项目,建议优先考虑更现代的替代方案(如 goyacc 增强版、peg、participle 或手写递归下降解析器)。但对于学习编译器前端原理,本例提供了一个精准、可控、可调试的起点。











