4. 语法分析基础:上下文无关文法、产生式、推导与归约
好,咱们进入语法分析这个环节。说实话,词法分析再复杂,也就是个“认字”的过程。但语法分析不一样,它要“懂句子”。你写了一大段 Verilog 代码,编译器怎么知道 always @(posedge clk) 后面该跟什么?怎么判断你漏了个 end?
嗯,这背后全靠一套严谨的数学工具——上下文无关文法。我个人觉得,搞懂这个,才算真正摸到了编译器的门槛。
4.1 为什么是“上下文无关”?
先说说这个名字。你想想看,自然语言里,“我吃苹果”和“苹果吃我”,词都一样,但意思完全反了。为什么?因为“苹果”不能当主语去“吃”人。这取决于上下文。
但 Verilog 不一样。一个 module 关键字,不管它出现在文件开头,还是嵌套在另一个模块里(虽然 Verilog 不允许嵌套),它的语法结构是固定的。它后面必须跟一个标识符,然后是一对括号,等等。它不需要知道“外面是谁”。
这就是“上下文无关”。每个语法结构的合法性,只取决于它自身的组成规则,不依赖外部环境。我在写解析器的时候,最爽的一点就是:我可以把每个语法单元独立地分析,不用担心“隔壁老王”影响了它。
4.2 产生式:语法的“配方”
文法怎么描述规则?靠产生式。说白了,就是一条条“替换规则”。
比如,一个简单的 Verilog 模块声明,可以写成这样:
module_declaration ::= "module" identifier "(" port_list ")" ";" module_item* "endmodule"
这个 ::= 读作“定义为”。左边是一个非终结符(可以继续展开),右边是它的定义,里面可以包含终结符(比如 "module" 这个关键字,不能再拆了)和其他非终结符。
我习惯把产生式想象成乐高积木的说明书。一个“汽车”积木,说明书上写着:它由“底盘”、“车身”、“轮子”组成。而“轮子”又由“轮胎”和“轮毂”组成。这就是一层层的产生式。
在 Verilog 里,我们会有成百上千条这样的产生式。比如:
always_construct ::= "always" statementstatement ::= blocking_assignment | nonblocking_assignment | if_statement | case_statement | ...if_statement ::= "if" "(" expression ")" statement ("else" statement)?
你看,statement 可以展开成很多种可能。这就是语法的“分支”。
4.3 推导:从“开始”到“句子”
有了产生式,我们怎么生成一段合法的代码?这个过程叫推导。
我们从文法的开始符号(比如 source_text)出发,不断用产生式的右边替换左边的非终结符,直到全部变成终结符(也就是具体的 token)。
举个例子,假设我们有这么几条极简的产生式:
E ::= E "+" E
E ::= "id"
我们要推导出 id + id。过程如下:
- 从
E开始。 - 使用第一条产生式:
E替换为E "+" E。 - 现在我们有
E "+" E。左边的E使用第二条产生式,替换为"id"。 - 得到
"id" "+" E。 - 右边的
E同样替换为"id"。 - 最终得到
"id" "+" "id"。
这个过程,就是最左推导(每次都替换最左边的非终结符)。你写代码的时候,其实就是在做推导——只不过你脑子里想的是“我要写一个加法表达式”,然后你写下了 a + b。
4.4 归约:推导的逆过程
推导是生成代码,那归约就是解析代码。它是推导的逆过程。
编译器拿到一串 token(比如 id, +, id),它要反向操作:把右边的符号组合起来,替换成左边的非终结符,直到归约回开始符号。
还是上面那个例子。解析器看到 id + id:
- 它看到第一个
id,发现可以归约为E(因为E ::= "id")。 - 现在栈里有
E,+,id。 - 它看到第二个
id,同样归约为E。 - 现在栈里有
E,+,E。 - 它发现这匹配
E ::= E "+" E,于是把这三个符号归约为一个E。 - 归约完成!
这就是一个移进-归约的过程。解析器一边读 token(移进),一边看能不能把栈顶的几个符号归约成一个非终结符。
id + id + id,是先把 id + id 归约成 E,还是先移进第三个 id?这涉及到运算符的结合性。如果不处理,解析器就会报错。解决办法是给产生式定义优先级和结合性。
4.5 知识体系总览
说了这么多,咱们用一张图来总结一下这四者的关系。我画了个 SVG,帮你理清脉络。
这张图你看懂了吗?上下文无关文法是总纲,它由一组产生式构成。我们既可以用产生式正向推导出合法的代码,也可以反向归约代码,并在这个过程中构建出一棵解析树。
嗯,说白了,语法分析就是“照着规则玩拼图”。规则(产生式)定好了,编译器就能判断你拼得对不对。
4.6 为什么这对 Verilog 解析器很重要?
你可能要问:我写 Verilog 代码,知道这些理论有什么用?
用处大了。Verilog 的语法其实挺复杂的。它有模块声明、端口列表、连续赋值、过程块(always)、各种语句(if、case、for)、表达式、运算符优先级……如果没有上下文无关文法,解析器根本没法写。
我记得有一次,我需要解析一个带参数的模块实例化。那个语法在 Verilog 标准里写得很绕。我直接照着标准里的产生式,一条条翻译成解析代码,一次就通过了。这就是理论的力量。
而且,当你理解了推导和归约,你就能明白为什么编译器会报“语法错误”。它其实就是归约到某一步,发现栈顶的符号没法匹配任何产生式的右边了。这时候,它就知道“出错了”。