05 · 编译原理(下):动手实现
“看十遍不如写一遍。这一节我们把词法、语法、语义三个阶段串起来,做一个能跑的东西。“
动手一:完整的表达式解析器 + 求值器
这个小程序完整展示了编译器前三个阶段,能解析并执行一段类 C 的脚本:
package main
import (
"fmt"
"strconv"
)
// ============ AST 节点 ============
type Node interface {
Eval(env *Env) (int, error)
String() string
}
type IntLit struct{ Value int }
func (n *IntLit) Eval(env *Env) (int, error) { return n.Value, nil }
func (n *IntLit) String() string { return strconv.Itoa(n.Value) }
type VarRef struct{ Name string }
func (n *VarRef) Eval(env *Env) (int, error) {
if v, ok := env.Get(n.Name); ok {
return v, nil
}
return 0, fmt.Errorf("未定义的变量: %s", n.Name) // 语义检查在运行期
}
func (n *VarRef) String() string { return n.Name }
type BinaryExpr struct {
Op TokenType
Left Node
Right Node
}
func (n *BinaryExpr) Eval(env *Env) (int, error) {
l, err := n.Left.Eval(env)
if err != nil {
return 0, err
}
// 短路求值:&& 和 || 的右侧不一定执行
if n.Op == TokAndAnd && l == 0 {
return 0, nil
}
if n.Op == TokOrOr && l != 0 {
return 1, nil
}
r, err := n.Right.Eval(env)
if err != nil {
return 0, err
}
switch n.Op {
case TokPlus:
return l + r, nil
case TokMinus:
return l - r, nil
case TokStar:
return l * r, nil
case TokSlash:
if r == 0 {
return 0, fmt.Errorf("除零错误")
}
return l / r, nil
case TokLt:
return boolToInt(l < r), nil
case TokGt:
return boolToInt(l > r), nil
case TokLe:
return boolToInt(l <= r), nil
case TokGe:
return boolToInt(l >= r), nil
case TokEq:
return boolToInt(l == r), nil
case TokNe:
return boolToInt(l != r), nil
case TokAndAnd:
return boolToInt(l != 0 && r != 0), nil
case TokOrOr:
return boolToInt(l != 0 || r != 0), nil
}
return 0, fmt.Errorf("未知运算符")
}
func boolToInt(b bool) int {
if b {
return 1
}
return 0
}
type AssignExpr struct {
Name string
Value Node
}
func (n *AssignExpr) Eval(env *Env) (int, error) {
v, err := n.Value.Eval(env)
if err != nil {
return 0, err
}
env.Set(n.Name, v)
return v, nil
}
func (n *AssignExpr) String() string { return n.Name + " = " + n.Value.String() }
type IfStmt struct {
Cond Node
Then Node
Else Node
}
func (n *IfStmt) Eval(env *Env) (int, error) {
c, err := n.Cond.Eval(env)
if err != nil {
return 0, err
}
if c != 0 {
return n.Then.Eval(env)
}
if n.Else != nil {
return n.Else.Eval(env)
}
return 0, nil
}
type BlockStmt struct{ Stmts []Node }
func (n *BlockStmt) Eval(env *Env) (int, error) {
env = env.Enter() // 新作用域
result := 0
for _, s := range n.Stmts {
v, err := s.Eval(env)
if err != nil {
return 0, err
}
result = v
}
return result, nil
}
type WhileStmt struct {
Cond Node
Body Node
}
func (n *WhileStmt) Eval(env *Env) (int, error) {
result := 0
for {
c, err := n.Cond.Eval(env)
if err != nil {
return 0, err
}
if c == 0 {
break
}
result, err = n.Body.Eval(env)
if err != nil {
return 0, err
}
}
return result, nil
}
// ============ 运行时环境(符号表的运行期形式)============
type Env struct {
vars map[string]int
parent *Env
}
func NewEnv() *Env { return &Env{vars: make(map[string]int)} }
func (e *Env) Enter() *Env {
return &Env{vars: make(map[string]int), parent: e}
}
func (e *Env) Get(name string) (int, bool) {
if v, ok := e.vars[name]; ok {
return v, true
}
if e.parent != nil {
return e.parent.Get(name)
}
return 0, false
}
func (e *Env) Set(name string, val int) { e.vars[name] = val }
// ============ 解析器 ============
type Parser struct {
tokens []Token
pos int
}
func (p *Parser) peek() Token { return p.tokens[p.pos] }
func (p *Parser) next() Token { t := p.tokens[p.pos]; p.pos++; return t }
func (p *Parser) at(t TokenType) bool { return p.peek().Type == t }
func (p *Parser) expect(t TokenType) (Token, error) {
tok := p.peek()
if tok.Type != t {
return tok, fmt.Errorf("第 %d 行: 期望 %s,实际得到 %s",
tok.Line, tokName(t), tokName(tok.Type))
}
p.next()
return tok, nil
}
func (p *Parser) ParseProgram() (Node, error) {
stmts := []Node{}
for !p.at(TokEOF) {
s, err := p.parseStmt()
if err != nil {
return nil, err
}
stmts = append(stmts, s)
}
return &BlockStmt{Stmts: stmts}, nil
}
func (p *Parser) parseStmt() (Node, error) {
switch p.peek().Type {
case TokLBrace:
p.next()
stmts := []Node{}
for !p.at(TokRBrace) {
s, err := p.parseStmt()
if err != nil {
return nil, err
}
stmts = append(stmts, s)
}
if _, err := p.expect(TokRBrace); err != nil {
return nil, err
}
return &BlockStmt{Stmts: stmts}, nil
case TokIf:
p.next()
if _, err := p.expect(TokLParen); err != nil {
return nil, err
}
cond, err := p.parseExpr(0)
if err != nil {
return nil, err
}
if _, err := p.expect(TokRParen); err != nil {
return nil, err
}
then, err := p.parseStmt()
if err != nil {
return nil, err
}
var elseNode Node
if p.at(TokElse) {
p.next()
elseNode, err = p.parseStmt()
if err != nil {
return nil, err
}
}
return &IfStmt{Cond: cond, Then: then, Else: elseNode}, nil
case TokWhile:
p.next()
if _, err := p.expect(TokLParen); err != nil {
return nil, err
}
cond, err := p.parseExpr(0)
if err != nil {
return nil, err
}
if _, err := p.expect(TokRParen); err != nil {
return nil, err
}
body, err := p.parseStmt()
if err != nil {
return nil, err
}
return &WhileStmt{Cond: cond, Body: body}, nil
default:
expr, err := p.parseExpr(0)
if err != nil {
return nil, err
}
if _, err := p.expect(TokSemi); err != nil {
return nil, err
}
return expr, nil
}
}
// ★ 优先级爬升
func (p *Parser) parseExpr(minPrec int) (Node, error) {
left, err := p.parseUnary()
if err != nil {
return nil, err
}
// 赋值:右结合,优先级最低
if p.at(TokAssign) {
p.next()
vr, ok := left.(*VarRef)
if !ok {
return nil, fmt.Errorf("赋值左侧必须是变量")
}
rhs, err := p.parseExpr(1) // 右结合:传 1 而不是 2
if err != nil {
return nil, err
}
return &AssignExpr{Name: vr.Name, Value: rhs}, nil
}
for {
tok := p.peek()
prec, isBinary := precedence[tok.Type]
if !isBinary || prec < minPrec {
break
}
p.next()
right, err := p.parseExpr(prec + 1) // 左结合
if err != nil {
return nil, err
}
left = &BinaryExpr{Op: tok.Type, Left: left, Right: right}
}
return left, nil
}
func (p *Parser) parseUnary() (Node, error) {
switch p.peek().Type {
case TokMinus, TokNot:
op := p.next().Type
operand, err := p.parseUnary()
if err != nil {
return nil, err
}
return &UnaryExpr{Op: op, Operand: operand}, nil
}
return p.parsePrimary()
}
func (p *Parser) parsePrimary() (Node, error) {
tok := p.peek()
switch tok.Type {
case TokInt:
p.next()
v, _ := strconv.Atoi(tok.Value)
return &IntLit{Value: v}, nil
case TokIdent:
p.next()
return &VarRef{Name: tok.Value}, nil
case TokLParen:
p.next()
e, err := p.parseExpr(0)
if err != nil {
return nil, err
}
if _, err := p.expect(TokRParen); err != nil {
return nil, err
}
return e, nil
}
return nil, fmt.Errorf("第 %d 行: 意外的 token", tok.Line)
}
// ============ 演示 ============
func demoParser() {
src := `
x = 10;
y = 0;
i = 1;
while (i <= x) {
y = y + i;
i = i + 1;
}
result = y * 2 - 100;
`
tokens, err := Tokenize(src)
if err != nil {
fmt.Println("词法错误:", err)
return
}
ast, err := NewParser(tokens).ParseProgram()
if err != nil {
fmt.Println("语法错误:", err)
return
}
fmt.Println("=== AST ===")
printAST(ast, 0)
env := NewEnv()
val, err := ast.Eval(env)
if err != nil {
fmt.Println("运行时错误:", err)
return
}
fmt.Println("\n=== 执行结果 ===")
fmt.Printf("result = %d\n", val) // 55*2-100 = 10
}
func printAST(n Node, depth int) {
indent := ""
for i := 0; i < depth; i++ {
indent += " "
}
switch v := n.(type) {
case *BlockStmt:
fmt.Printf("%sBlock\n", indent)
for _, s := range v.Stmts {
printAST(s, depth+1)
}
case *WhileStmt:
fmt.Printf("%sWhile:\n%s cond:\n", indent, indent)
printAST(v.Cond, depth+2)
fmt.Printf("%s body:\n", indent)
printAST(v.Body, depth+2)
case *AssignExpr:
fmt.Printf("%sAssign(%s):\n", indent, v.Name)
printAST(v.Value, depth+1)
case *BinaryExpr:
fmt.Printf("%sBinary(%s):\n", indent, tokName(v.Op))
printAST(v.Left, depth+1)
printAST(v.Right, depth+1)
default:
fmt.Printf("%s%s\n", indent, n.String())
}
}运行输出:
=== AST ===
Block
Assign(x):
10
Assign(y):
0
Assign(i):
1
While:
cond:
Binary(<=):
i
10
body:
Block
Assign(y):
Binary(+):
y
i
Assign(i):
Binary(+):
i
1
Assign(result):
Binary(-):
Binary(*):
y
2
100
=== 执行结果 ===
result = 10
注意观察:y * 2 - 100 被解析成 Binary(-, Binary(*, y, 2), 100) —— * 在更深的层级,说明它结合得更紧。这就是优先级爬升正确工作的证据。
动手二:DSL 规则引擎(带友好错误提示)
真实场景:公司的规则引擎,业务方要写这样的规则:
user.age > 18 AND user.city IN ["北京","上海"] AND score >= 60
语法设计
expression = or_expr
or_expr = and_expr { "OR" and_expr }
and_expr = not_expr { "AND" not_expr }
not_expr = "NOT" not_expr | comparison
comparison = operand [ comp_op operand ]
comp_op = ">" | ">=" | "<" | "<=" | "==" | "!=" | "IN" | "CONTAINS"
operand = field_ref | literal | "(" expression ")"
field_ref = ident { "." ident }
literal = int | float | string | bool | array
array = "[" [ literal { "," literal } ] "]"四个设计决策及理由:
| 决策 | 理由 |
|---|---|
| 优先级用 BNF 层级直接表达 | or→and→not→cmp 的层级直接编码了 NOT > 比较 > AND > OR,符合 SQL 惯例 |
| 用关键字 AND/OR/NOT 而非 &&/||/! | 业务方不是程序员;避免 & vs && 拼写错误;词法更简单 |
| 比较运算符是可选中缀 | 允许 user.isVip(裸布尔字段),规则更自然 |
| 数组字面量支持 IN | city IN [...] 是最高频模式,作为一等公民 |
带友好错误的实现(核心部分)
// ParseError 带上下文和修复建议
type ParseError struct {
Line, Col int
Message string
Suggestion string
}
func (e *ParseError) Error() string {
var sb strings.Builder
fmt.Fprintf(&sb, "规则语法错误 (第 %d 行第 %d 列): %s\n", e.Line, e.Col, e.Message)
if e.Suggestion != "" {
sb.WriteString("\n💡 提示: " + e.Suggestion + "\n")
}
return sb.String()
}
type RuleParser struct {
tokens []Token
pos int
}
var compOps = map[string]bool{
">": true, ">=": true, "<": true, "<=": true,
"==": true, "!=": true, "IN": true,
"CONTAINS": true, "MATCHES": true,
}
var knownKeywords = []string{"AND", "OR", "NOT", "IN", "CONTAINS", "MATCHES", "true", "false"}
func (p *RuleParser) parseComparison() (Expr, error) {
// 括号:递归下降处理嵌套
if p.at(TokLParen) {
p.next()
inner, err := p.parseOr()
if err != nil {
return nil, err
}
if !p.at(TokRParen) {
return nil, p.errorf("括号未闭合,期望 ')'")
}
p.next()
return inner, nil
}
left, err := p.parseOperand()
if err != nil {
return nil, err
}
// 可选的比较运算符(没有就是裸布尔字段)
if p.at(TokIdent) && compOps[p.peek().Value] {
opTok := p.next()
right, err := p.parseOperand()
if err != nil {
return nil, err
}
return &Compare{Left: left, Op: opTok.Value, Right: right}, nil
}
return left, nil
}
func (p *RuleParser) errorf(format string, args ...any) error {
tok := p.peek()
suggestion := ""
if tok.Type == TokIdent {
if s := suggestKeyword(tok.Value); s != "" {
suggestion = fmt.Sprintf("你是不是想写 %q?", s)
}
}
return &ParseError{
Line: tok.Line, Col: tok.Col,
Message: fmt.Sprintf(format, args...),
Suggestion: suggestion,
}
}
// 编辑距离建议
func suggestKeyword(got string) string {
upper := strings.ToUpper(got)
best, bestDist := "", 1<<30
for _, kw := range knownKeywords {
d := levenshtein(upper, kw)
if d < bestDist && d <= 2 {
best, bestDist = kw, d
}
}
return best
}
func levenshtein(a, b string) int {
m, n := len(a), len(b)
dp := make([][]int, m+1)
for i := range dp {
dp[i] = make([]int, n+1)
}
for i := 0; i <= m; i++ {
dp[i][0] = i
}
for j := 0; j <= n; j++ {
dp[0][j] = j
}
for i := 1; i <= m; i++ {
for j := 1; j <= n; j++ {
cost := 1
if a[i-1] == b[j-1] {
cost = 0
}
dp[i][j] = min3(dp[i-1][j]+1, dp[i][j-1]+1, dp[i-1][j-1]+cost)
}
}
return dp[m][n]
}
func min3(a, b, c int) int {
m := a
if b < m {
m = b
}
if c < m {
m = c
}
return m
}效果:
输入: user.age > 18 ADN user.city == "北京"
输出: 规则语法错误 (第 1 行第 15 列): 表达式结束后还有多余的内容 "ADN"
💡 提示: 你是不是想写 "AND"?
扩展性验证
要加一个 STARTS_WITH 运算符,只需两处:
// 1. 加到 compOps
"STARTS_WITH": true,
// 2. 加到 compareValues
case "STARTS_WITH":
s, _ := l.(string)
prefix, _ := r.(string)
return strings.HasPrefix(s, prefix), nil词法层和语法层完全不用改——因为 STARTS_WITH 是 identifier,词法分析器已经能识别。
这就是”把运算符设计成关键字而非符号”的扩展性好处。
三个可改进点
- 类型检查应在解析期做(根据字段 schema),而不是运行期才发现
user.age > "abc" - 优先级硬编码在函数层级,若支持算术运算应改用优先级爬升
- 性能:规则会被反复执行,应把 AST 编译成闭包或字节码,而不是每次解释执行
思考题 2 ·【应用层】
上面的规则引擎有个设计问题:user.age > 18 中,如果 user.age 字段不存在,会在运行时才报错。请设计一个”编译期检查”方案:给定字段 schema,在解析阶段就验证所有字段存在且类型正确。说明你的方案如何处理这三个情况:① 嵌套字段 user.profile.city;② 动态字段(schema 里没有但允许);③ 字段类型与运算符不匹配。
参考答案
方案:Schema 驱动的语义分析
核心思路:把”验证”从运行期提前到解析期。 这正是编译器语义分析做的事——在运行前发现尽可能多的错误。
// ============ Schema 定义 ============
type FieldType int
const (
TypeInt FieldType = iota
TypeFloat
TypeString
TypeBool
TypeArray
TypeObject
)
type Schema struct {
Fields map[string]*FieldDef
// 是否允许 schema 之外的动态字段
AllowDynamic bool
}
type FieldDef struct {
Type FieldType
ElemType FieldType // TypeArray 时的元素类型
Children *Schema // TypeObject 时的子 schema
}
// 判断某个运算符支持哪些类型
var opSupportedTypes = map[string][]FieldType{
">": {TypeInt, TypeFloat},
">=": {TypeInt, TypeFloat},
"<": {TypeInt, TypeFloat},
"<=": {TypeInt, TypeFloat},
"==": {TypeInt, TypeFloat, TypeString, TypeBool},
"!=": {TypeInt, TypeFloat, TypeString, TypeBool},
"IN": {TypeInt, TypeFloat, TypeString},
"CONTAINS": {TypeString},
"STARTS_WITH": {TypeString},
}
// ============ 类型推导 ============
// inferType 推导表达式的类型,同时做检查
func inferType(e Expr, schema *Schema) (FieldType, []error) {
var errs []error
switch v := e.(type) {
case *Literal:
return literalType(v.Value), nil
case *FieldRef:
ft, err := resolveField(schema, v.Path)
if err != nil {
return TypeInt, []error{fmt.Errorf("第 %d 列: %v", v.Col, err)}
}
return ft, nil
case *Compare:
lt, e1 := inferType(v.Left, schema)
rt, e2 := inferType(v.Right, schema)
errs = append(errs, e1...)
errs = append(errs, e2...)
// 检查运算符是否支持这两个类型
supported := opSupportedTypes[v.Op]
if !containsType(supported, lt) {
errs = append(errs, fmt.Errorf(
"运算符 %s 不支持左操作数类型 %s(字段 %s)",
v.Op, typeName(lt), v.Left.String()))
}
// IN 的右侧必须是数组,且元素类型匹配
if v.Op == "IN" {
arr, ok := v.Right.(*ArrayLit)
if !ok {
errs = append(errs, fmt.Errorf("IN 的右侧必须是数组字面量"))
} else {
for _, elem := range arr.Elements {
et, _ := inferType(elem, schema)
if et != lt {
errs = append(errs, fmt.Errorf(
"IN 数组元素类型 %s 与左操作数类型 %s 不匹配",
typeName(et), typeName(lt)))
}
}
}
} else if !typesCompatible(lt, rt) {
errs = append(errs, fmt.Errorf(
"类型不匹配: %s %s %s", typeName(lt), v.Op, typeName(rt)))
}
return TypeBool, errs
case *Logical:
lt, e1 := inferType(v.Left, schema)
errs = append(errs, e1...)
if lt != TypeBool {
errs = append(errs, fmt.Errorf(
"逻辑运算符 %s 的操作数必须是布尔类型,实际是 %s",
v.Op, typeName(lt)))
}
if v.Right != nil {
rt, e2 := inferType(v.Right, schema)
errs = append(errs, e2...)
if rt != TypeBool {
errs = append(errs, fmt.Errorf(
"逻辑运算符 %s 的操作数必须是布尔类型,实际是 %s",
v.Op, typeName(rt)))
}
}
return TypeBool, errs
}
return TypeInt, nil
}
// ============ 三个特殊情况的处理 ============
func resolveField(schema *Schema, path []string) (FieldType, error) {
cur := schema
var lastDef *FieldDef
for i, p := range path {
def, ok := cur.Fields[p]
if !ok {
// ★ 情况 ②:动态字段
if cur.AllowDynamic && i == len(path)-1 {
// 允许,但类型未知 —— 返回特殊的"动态类型"
return TypeDynamic, nil
}
return TypeInt, fmt.Errorf("未定义的字段: %s%s",
strings.Join(path[:i+1], "."),
suggestField(p, cur))
}
lastDef = def
// ★ 情况 ①:嵌套字段
if i < len(path)-1 {
if def.Type != TypeObject || def.Children == nil {
return TypeInt, fmt.Errorf(
"字段 %s 不是对象,无法访问子字段 .%s",
strings.Join(path[:i+1], "."), path[i+1])
}
cur = def.Children // 下钻到子 schema
}
}
if lastDef == nil {
return TypeInt, fmt.Errorf("空字段路径")
}
return lastDef.Type, nil
}
// 字段名的编辑距离建议
func suggestField(got string, schema *Schema) string {
best, bestDist := "", 1<<30
for name := range schema.Fields {
d := levenshtein(strings.ToLower(got), strings.ToLower(name))
if d < bestDist && d <= 2 {
best, bestDist = name, d
}
}
if best != "" {
return fmt.Sprintf(",你是不是想写 %q?", best)
}
return ""
}三个情况的具体处理
情况 ①:嵌套字段 user.profile.city
方案:Schema 递归下钻。
userSchema := &Schema{Fields: map[string]*FieldDef{
"age": {Type: TypeInt},
"name": {Type: TypeString},
"profile": {
Type: TypeObject,
Children: &Schema{Fields: map[string]*FieldDef{
"city": {Type: TypeString},
"vip": {Type: TypeBool},
}},
},
}}解析 user.profile.city 时:
- 在根 schema 找
user→ 找到,类型是 Object - 下钻到
user.Children,找profile→ 找到,类型是 Object - 再下钻,找
city→ 找到,类型是 String
关键点:中间层如果不是 Object,要报明确的错误:“字段 user.age 不是对象,无法访问子字段 .city”
这个设计的额外好处:schema 本身是递归结构,天然支持任意深度嵌套。而且下钻的过程就是一次”作用域链查找”——跟编译器查符号表的逻辑完全同构。
情况 ②:动态字段
方案:引入 TypeDynamic 特殊类型 + AllowDynamic 开关。
const TypeDynamic FieldType = 99 // 类型未知
// 类型兼容性检查时,dynamic 与任何类型都兼容
func typesCompatible(a, b FieldType) bool {
if a == TypeDynamic || b == TypeDynamic {
return true // 放宽检查
}
return a == b || (isNumeric(a) && isNumeric(b))
}权衡:
- 允许动态字段 → 灵活,但失去类型检查的保护
- 禁止 → 安全,但业务方抱怨”我的字段明明有”
我的建议:分层处理。
- 核心字段(schema 里定义的)严格检查
- 动态字段允许,但标记为”未验证”,在规则管理界面上显示警告
- 运行时对动态字段做防御性处理(取不到值就返回 false,不 panic)
更进一步的方案:支持”部分动态”——schema 里声明”user 下的所有字段都是 string 类型”:
"user": {
Type: TypeObject,
Children: &Schema{
AllowDynamic: true,
DynamicType: TypeString, // 动态字段默认是 string
},
}这样既灵活又保留了一定检查能力。
情况 ③:类型与运算符不匹配
方案:运算符的类型表(白名单)。
var opSupportedTypes = map[string][]FieldType{
">": {TypeInt, TypeFloat},
"CONTAINS": {TypeString},
// ...
}user.age > "abc" 会报:“运算符 > 不支持左操作数类型 int 与右操作数类型 string 不匹配”
关键的额外检查:IN 的数组元素类型
user.age IN ["北京", "上海"] // ❌ int 字段配 string 数组这个错误很容易漏检——因为 IN 的右侧是数组,如果只检查”右侧是数组”就通过了。必须逐个元素检查类型。
三个容易忽略的细节
细节 1 · 错误要一次全报,不要遇错即停
// ❌ 遇到第一个错误就返回
func check(e Expr) error { ... return err }
// ✅ 收集所有错误
func check(e Expr) []error { ... return errs }业务方写规则时,往往有多个错误。一次全报出来能省好几轮修改。这就是上篇说的”错误恢复 + 多错误报告”。
细节 2 · 空值处理要有明确策略
user.profile.city == "北京"
// 如果 user 为 null 怎么办?profile 为 null 怎么办?三种策略:
- 三值逻辑(SQL 的做法):
NULL == "北京"结果是UNKNOWN,不是 true 也不是 false - 短路为 false:任何路径取不到值,整个表达式返回 false
- 报错
建议用策略 2(短路为 false)。理由:规则引擎的语义是”满足条件才执行动作”,取不到值就当不满足,最符合直觉,也不会因为数据缺失导致规则引擎崩溃。
但要在文档里明确写出来,否则业务方会困惑”为什么这条规则没触发”。
细节 3 · Schema 变更的兼容
字段 schema 会变(新增字段、改类型)。已经写好的规则怎么办?
方案:
- 规则保存时,同时保存编译后的 AST + 当时的 schema 版本
- schema 变更时,重新校验所有存量规则,把失效的规则标红
- 提供”规则健康检查”页面,列出所有有问题的规则
这本质上是一个”数据库 migration”问题——schema 演进时要考虑存量数据的兼容性。规则即数据。
一句话总结
这个设计的核心思想跟编译器完全一致:把错误从运行期提前到编译期。
代价是需要维护 schema,收益是:
- 业务方写完规则立即看到错误,不用等到运行时
- 规则引擎运行时不需要做类型检查,性能更好
- 可以做静态分析(比如检测出矛盾的规则
age > 18 AND age < 10)
这个模式(schema 驱动 + 提前验证)可以推广到很多场景:配置校验、API 参数校验、数据管道的质量检查。凡是”用户输入会被反复执行”的场景,都应该考虑提前验证。
思考题 3 ·【创造层】
Go 用递归下降,GCC/Clang 用 LALR/递归下降混合。假设你要设计一门新语言,怎么选分析策略?如果语言要支持”用户自定义运算符优先级”(如 Haskell),你的选择会变吗?
参考答案
决策框架:五个因素
① 语法复杂度(有歧义吗?需要几层前瞻?)
② 错误提示质量要求
③ 编译速度要求
④ 可维护性(团队能维护吗?)
⑤ 扩展性(语言还要演进吗?)
① 语法复杂度
- LL(k):必须在看到前 k 个 token 后确定用哪条产生式。两产生式前 k 个 token 相同就冲突
- LR(k):可”看完整条产生式再决定归约”,能处理更多文法
- 但 LL 可用”回溯”绕过大部分限制,代价是速度和错误提示
② 错误提示(最大差异点)
- 递归下降:只知道”我猜的这条路走不通”
- LR:状态机记录”当前所有可能的 token”,能给”expected one of X, Y, Z”
③ 编译速度
- 递归下降:函数调用,分支预测友好,指令缓存局部性好 → 快
- 表驱动 LR:每步查表,缓存不友好 → 慢 2~5 倍
④ 可维护性
- 递归下降:代码就是语法的直接映射,每个产生式一个函数,任何人都能读
- LR:状态表是生成的,几千个状态,人类无法阅读,改语法要重新生成,diff 无法审阅
这是 Go 选递归下降的关键理由——Go 团队极其在意编译器代码的可读性。
⑤ 扩展性
- 递归下降:加新语法 = 加新函数,局部改动
- LR:加新语法可能引入 shift-reduce 冲突,解决冲突可能要重构文法
我的选择(通用语言)
首选:手写递归下降 + 优先级爬升
理由:编译速度、可维护性、扩展性都最优;错误提示的劣势可通过”记录期望集合”缓解。
代价:文法必须避免左递归和歧义,语言设计受约束。
但我接受这个约束——而且我认为这是好事:如果你的语法连递归下降都处理不了,大概率它对人类来说也太复杂了。
这是 Go 的哲学:语言特性应该让编译器简单,也应该让人简单。这两个目标是一致的。
如果要支持自定义运算符,选择会大变
问题在哪
infixl 7 <*> -- Haskell:自定义优先级和结合性
infixr 5 ++
infix 4 ==// Scala:优先级由首字符决定,不可自定义
// (字母) < | ^ & = ! : + - * / % (其他)对解析器的冲击:
- 优先级表是动态的——解析到一半才知道某运算符的优先级
- 优先级可能重定义——
a + b * c中*的优先级取决于上下文定义 - 不同作用域可能有不同定义
静态递归下降无法处理——因为 parseExpr(minPrec) 里的 minPrec 比较依赖一个解析时才能确定的表。
三种解法
解法 A · 解析成平坦序列,语义阶段再建树(Haskell)
-- 第一步:不管优先级,全部解析成列表
-- a + b * c → [a, +, b, *, c]
-- 第二步:语义分析时查优先级表,用算符优先分析法建树
-- → a + (b * c)把”优先级解析”推迟到语义阶段——又是”信息推迟到能获取的时候再处理”的模式。
解法 B · 限制自定义能力(Scala)
优先级完全由运算符首字符决定,不可自定义。这样优先级表仍静态,递归下降可用。代价是表达力受限。
解法 C · 放弃中缀运算符(Lisp)
(+ a (* b c)) ; 前缀,优先级由括号显式给出语法分析极简单(就是括号匹配)。代价是读写体验。
Lisp 的 S 表达式之所以简单,正是因为这个选择——它的语法几乎是”无语法”。这也是 Lisp 宏系统强大的原因:语法树就是数据结构,可直接操作。
我的选择
解法 B(受限自定义)+ 解法 A(延迟解析)的混合:
- 默认运算符用静态优先级表(递归下降直接处理,保证 95% 场景的速度和错误提示)
- 自定义运算符的优先级由首字符决定(Scala 规则),不可任意指定
- 如果某作用域内确实重定义了优先级,回退到”平坦解析 + 延迟建树”
理由:这个混合方案让最常见的情况走快路径,同时保留了表达能力。这是”优化常见路径,为罕见情况保留退路”的经典思路。
更深的洞察:这三个解法对应三种语言哲学
| 解法 | 语言 | 哲学 |
|---|---|---|
| A · 延迟解析 | Haskell | 表达力优先。语法可以复杂,但编译器要能处理,哪怕慢一点 |
| B · 限制能力 | Scala | 实用性优先。牺牲一点表达力,换取编译器简单和错误提示好 |
| C · 放弃中缀 | Lisp | 极简优先。语法简单到极致,把复杂度推给用户(括号)和宏系统 |
没有对错,只有取舍。
但要看到一个共同点:三种解法都接受了”在某个维度上做出牺牲”。它们没有试图”全都要”。
试图全都要的语言,最后往往会得到一个谁都用不好的系统。 这是语言设计(其实也是所有系统设计)最重要的一条教训。
给你自己的检验标准
设计任何系统时,问自己:
- 我的核心约束是什么?(编译速度?错误提示?表达力?)
- 为了这个约束,我愿意牺牲什么?
- 这个牺牲,用户能接受吗?
答不上来第 2 个,说明你还没真正做设计,只是在堆特性。
小结:这一节你应该带走的东西
-
编译的五个阶段,每一层都在丢弃信息:词法丢格式、语法丢括号、IR 丢变量名、代码生成丢类型。后面阶段拿不到的信息,就没法做相应优化——这是现代编译器往”保留更多信息”演进的原因。
-
理论约束塑造工程实践:
- 正则无法计数 → 嵌套结构必须用栈 → 编程语言停在 2 型文法
- 通用 CFG 分析是 O(n³) → LL/LR 只处理子集才做到 O(n)
- 这解释了为什么编译要分阶段
-
递归下降 vs LR 的本质是信息量的差异:递归下降只知道”我猜错了”,LR 知道”所有可能的选项”。这直接决定了错误提示的质量。
-
优先级爬升是手写解析器的核心技巧:
parseExpr(minPrec),传prec+1得左结合,传prec得右结合。值得背下来。 -
纯粹的分层在真实系统里总被打破:C 的 typedef 问题、Go 泛型的
[歧义,都需要语义信息倒流回语法层。Go 的解法是改语言设计来避开歧义,这是”为了编译器简单而改变语言”的典范。 -
Go 编译快的三个原因:包级符号表、手写递归下降、扁平 IR。这是明确的取舍——用优化质量换迭代速度。
下一节
理论讲完,接下来是本章最硬的一个项目:从零实现一个能编译 C 子集(指针、结构体、函数、控制流、递归)的编译器,直接生成可执行的机器码。
老陈的预告:这个项目做完,你会真正理解”高级语言”和”机器指令”之间那道鸿沟是怎么被填平的。而且你会发现一个惊人的事实——编译器 90% 的复杂度不在算法,而在处理各种边界情况和 ABI 细节。