规则解析器(Rule Parser):规则语言基础与AST构建
规则解析器,说白了就是让计算机能读懂我们写的设计规则。你想想看,我们平时写规则,用的都是SKILL、Tcl这类脚本语言。但机器不懂这些,它只认0和1。所以我们需要一个翻译官——这就是规则解析器要做的事。
我个人习惯把规则解析器分成三个层次:词法分析、语法分析、AST构建。这三个层次就像剥洋葱,一层层把规则语言翻译成计算机能理解的数据结构。
规则语言基础:SKILL与Tcl
先聊聊规则语言。EDA领域最常用的就是SKILL和Tcl。SKILL是Cadence家的看家本领,Tcl则是Synopsys和Mentor的宠儿。
SKILL语言的特点:
- Lisp风格,括号满天飞
- 强类型,变量需要声明
- 内置大量EDA专用函数
Tcl语言的特点:
- 一切皆字符串
- 弱类型,用起来灵活
- 扩展性强,可以嵌入C/C++
我在项目中遇到过最头疼的事:团队里有人用SKILL写规则,有人用Tcl写。最后不得不写个统一的解析器,把两种语言都转成内部表示。嗯,那段时间真是加班加到怀疑人生。
来看个简单的规则例子:
// SKILL写法
procedure(checkNetWidth(net minWidth)
let((width)
width = getNetWidth(net)
if(width < minWidth then
error("Net %L width %f < %f" net width minWidth)
)
)
)
# Tcl写法
proc checkNetWidth {net minWidth} {
set width [getNetWidth $net]
if {$width < $minWidth} {
error "Net $net width $width < $minWidth"
}
}
两种语言都能表达同样的逻辑,但语法差异很大。解析器要做的就是把这些差异抹平,统一成内部表示。
词法分析:把字符串变成Token流
词法分析,说白了就是分词。把一整段规则代码,拆成一个个有意义的单词(Token)。
举个例子,对于这段代码:
if(width < 10) then error("太窄了")
词法分析器会把它拆成:
| Token类型 | Token值 |
|---|---|
| KEYWORD | if |
| LPAREN | ( |
| IDENTIFIER | width |
| OPERATOR | < |
| NUMBER | 10 |
| RPAREN | ) |
| KEYWORD | then |
| IDENTIFIER | error |
| LPAREN | ( |
| STRING | "太窄了" |
| RPAREN | ) |
词法分析器的核心是状态机。每个字符进来,根据当前状态决定下一步怎么走。我曾经踩过一个坑:字符串里的转义字符没处理好,导致规则解析总是报错。后来加了个转义状态才搞定。
小技巧:词法分析时,最好把注释和空白符直接过滤掉。这样语法分析器就不用处理这些无关信息了。
语法分析:从Token流到语法树
词法分析完了,我们得到了一堆Token。但这些Token之间是什么关系?这就需要语法分析器来搞定了。
语法分析器会按照语法规则,把Token流组织成一棵树。这棵树就是抽象语法树(AST)。
常见的语法分析方法有两种:
- 自顶向下:从根节点开始,递归下降
- 自底向上:从叶子节点开始,规约到根
我个人偏爱自顶向下的递归下降法。为什么?因为代码写起来直观,调试也方便。你想想看,每个语法规则对应一个函数,多清晰。
来看个简单的递归下降解析器伪代码:
// 解析if语句
parseIfStatement() {
matchToken(KEYWORD, "if")
matchToken(LPAREN)
condition = parseExpression() // 解析条件表达式
matchToken(RPAREN)
matchToken(KEYWORD, "then")
body = parseStatement() // 解析执行体
return new IfNode(condition, body)
}
// 解析表达式
parseExpression() {
left = parsePrimary() // 解析基本表达式
while (currentToken is OPERATOR) {
op = currentToken
advance()
right = parsePrimary()
left = new BinaryOpNode(left, op, right)
}
return left
}
注意:递归下降解析器容易陷入左递归。比如表达式解析时,如果写成 parseExpression -> parseExpression + parseTerm,就会无限递归。解决办法是改成循环或者用右递归。
构建抽象语法树(AST)
AST是规则解析器的最终产物。它把规则代码变成了一棵结构清晰的树,每个节点代表一个语法结构。
AST节点通常包含:
- 节点类型(如IfNode、BinaryOpNode)
- 子节点列表
- 属性信息(如操作符、变量名)
- 位置信息(行号、列号,方便报错)
下面我用SVG画一张AST的示意图,帮你理解它的结构:
这张图展示的是 if(width < 10) then error("太窄了") 的AST结构。根节点是IfNode,下面分条件分支和执行体分支。条件分支又拆成左操作数、操作符、右操作数。执行体分支则是一个函数调用。
AST建好了,后续的规则检查就简单了。你只需要遍历这棵树,对每个节点执行相应的检查逻辑就行。
我个人习惯在AST节点里加一个visit方法。这样遍历树的时候,每个节点自己知道该怎么处理。这就是访问者模式,用起来特别顺手。
避坑指南
做规则解析器,有几个坑我踩过,分享给你:
- 错误恢复:解析到语法错误时,别直接崩溃。最好能跳过错误,继续解析后面的内容,这样一次能报出多个错误。
- 位置信息:每个AST节点都要记录行号和列号。不然报错时,用户根本不知道哪里出了问题。
- 性能优化:规则文件可能很大,解析器要高效。我试过用缓存机制,把解析过的规则缓存起来,避免重复解析。
我曾经接手过一个项目,规则解析器没有错误恢复功能。用户写错一个括号,解析器就崩了,连错误位置都不报。后来我花了三天重写了解析器,加了错误恢复和位置信息。嗯,从那以后,团队里再没人抱怨规则解析器难用了。
好了,规则解析器的核心内容就这些。记住三个关键词:词法分析、语法分析、AST构建。把这三点吃透了,你就能写出自己的规则解析器。