
1. 上下文无关文法基础从四元组到语言生成第一次接触上下文无关文法CFG时我被它简洁的数学定义惊艳到了——一个四元组G(T,N,P,S)就能描述编程语言的语法结构。这就像用乐高积木搭建城堡终结符号T是基础砖块非终结符号N是特殊连接件产生式P是拼装说明书而开始符号S就是城堡的大门。终结符号对应词法分析器生成的token比如数字123、运算符、标识符x。在Python中if、def这些关键字也是终结符号。非终结符号则代表语法成分比如表达式、语句等抽象概念。产生式规则A→α就像变形公式允许我们把非终结符号A替换为符号串α。举个实际例子JSON的语法可以用CFG这样定义JSON → object | array object → { pairs } pairs → pair | pair , pairs pair → STRING : value value → STRING | NUMBER | object | array | true | false | null array → [ elements ] elements → value | value , elements这个文法能生成所有合法的JSON字符串。当我在项目中需要解析自定义配置文件时就借鉴了这种结构先定义文法再实现解析器比直接写正则表达式要清晰得多。2. 文法设计实战从悬空else到表达式优先级新手设计文法时常会遇到两个经典陷阱悬空else问题和表达式二义性。有次我写一个脚本语言的解析器就掉进了if-else的坑里。原始文法如下stmt → if expr then stmt | if expr then stmt else stmt | OTHER当遇到if x then if y then A else B时else到底匹配哪个if这就是著名的悬空else歧义。通过引入matched_stmt和open_stmt概念可以解决stmt → matched_stmt | open_stmt matched_stmt → if expr then matched_stmt else matched_stmt | OTHER open_stmt → if expr then stmt | if expr then matched_stmt else open_stmt表达式优先级是另一个重灾区。最初我这样定义算术表达式expr → expr expr | expr * expr | ( expr ) | NUMBER结果发现12*3能生成两种语法树。后来采用分层设计将优先级编码到文法层级中expr → expr term | term term → term * factor | factor factor → ( expr ) | NUMBER这种设计确保乘法节点总是在加法节点的下层就像洋葱一样层层包裹。我在实现计算器时实测发现这种文法配合递归下降解析简直完美。3. 语法分析器构建LL(1)手写实践LL(1)分析器特别适合手工实现。记得第一次用Python写表达式解析器时我按照这个文法设计expr → term expr_tail expr_tail → term expr_tail | ε term → factor term_tail term_tail → * factor term_tail | ε factor → ( expr ) | NUMBER关键点是预测分析表的构建。计算FIRST和FOLLOW集时我总结了个小窍门FIRST(X)看产生式右侧第一个符号FOLLOW(X)看X后面可能出现的符号特别是A→αXβ时的FIRST(β)# 简化的LL(1)解析器框架 class Parser: def __init__(self, tokens): self.tokens tokens self.pos 0 def parse(self): return self.expr() def expr(self): left self.term() while self.current() : self.consume() right self.term() left (add, left, right) return left def term(self): left self.factor() while self.current() *: self.consume(*) right self.factor() left (mul, left, right) return left def factor(self): if self.current() (: self.consume(() val self.expr() self.consume()) return val return self.number()4. 二义性检测与消除技巧判断任意CFG是否二义性是不可判定问题但实践中有些实用技巧。我常用的二义性检查清单运算符优先级是否明确定义同个非终结符的产生式是否可重叠if-else是否有嵌套歧义最近在开发领域特定语言时就遇到了这种情况command → ID expr | ID ( args ) | command ; command当输入ab;c(d)时可以解释为(ab);c(d)或a(b;c(d))。通过引入显式分隔规则解决program → command_list command_list → command | command ; command_list command → assign | call assign → ID expr call → ID ( args )优先级爬升算法是另一种处理表达式二义性的优雅方案。不同于传统分层文法它通过优先级表动态指导解析def parse_expression(min_prec): lhs parse_primary() while True: tok current_token() prec get_precedence(tok) if prec min_prec: break next_token() rhs parse_expression(prec (1 if is_right_assoc(tok) else 0)) lhs make_ast_node(tok, lhs, rhs) return lhs5. 工具链应用Bison实战示例虽然手写解析器很有成就感但生产环境更常用工具如Bison。最近用Bison实现SQL子集的案例%token SELECT FROM WHERE AND OR ID NUMBER STRING %left OR %left AND %left %left - %left * / %% query : SELECT select_list FROM table_list where_clause ; where_clause : /* empty */ | WHERE condition ; condition : condition OR condition | condition AND condition | expr expr | expr expr | ( condition ) ;几个关键经验用%left声明优先级和结合性冲突处理Bison会报告shift/reduce冲突需要人工判断错误恢复定义error规则实现容错当处理复杂语言时我会先用Bison生成基础解析器再手动优化特殊case。比如为Python的缩进语法添加lexer hack在词法阶段记录缩进级别。6. 语法树构建与编译器前端完成语法分析后真正的乐趣才开始——构建抽象语法树AST。这是我的AST设计心得每个非终结符对应一个AST节点类型保留关键位置信息行号、列号简化不必要的文法细节比如对于lambda表达式AST可能这样设计class AstNode: pass class LambdaExpr(AstNode): def __init__(self, params, body): self.params params # [Identifier] self.body body # Expr class CallExpr(AstNode): def __init__(self, func, args): self.func func # Expr self.args args # [Expr]在类型检查阶段这些AST节点会被附加类型信息。我曾实现过一个类型推导器核心就是遍历AST并维护类型环境def infer_type(node, env): if isinstance(node, Number): return IntType elif isinstance(node, BinOp): left_type infer_type(node.left, env) right_type infer_type(node.right, env) return check_operands(node.op, left_type, right_type) elif isinstance(node, Variable): return env.lookup(node.name)从CFG定义到可运行的编译器前端这个过程就像把数学公式变成可触摸的机器。每次看到自己设计的文法能正确解析复杂代码时那种成就感无可比拟。