3. 词法分析器实现:从字符流到Token流
好,咱们今天聊聊词法分析器。说白了,就是把你写的Verilog代码——那一串串字符——变成计算机能理解的Token流。我刚开始做编译器时,觉得这步挺简单的,不就是拆单词嘛。结果真动手才发现,坑多着呢。
3.1 词法分析器在干什么?
你想想看,Verilog代码本质上就是个长长的字符串。词法分析器的任务,就是把这个字符串切成有意义的片段。每个片段就是一个Token。
举个例子:
reg [3:0] data;
这段代码会被切成:
reg— 关键字[— 符号3— 数字:— 符号0— 数字]— 符号data— 标识符;— 符号
每个Token都带着类型和值。比如数字Token,你得知道它是十进制还是十六进制。
核心思路:词法分析器就是个状态机。读一个字符,判断当前状态,决定下一步怎么走。
3.2 标识符和关键字
Verilog的标识符规则其实挺简单:以字母或下划线开头,后面跟字母、数字、下划线或美元符号。关键字嘛,就是那些被语言保留的单词,比如module、always、wire。
我个人习惯的做法是:先识别出标识符,然后查关键字表。如果匹配上了,就当作关键字Token;否则就是普通标识符。
// 伪代码示例
Token recognizeIdentifier(char firstChar) {
string name;
name += firstChar;
while (isAlphaNumeric(nextChar()) || nextChar() == '_' || nextChar() == '$') {
name += consumeChar();
}
if (keywordTable.contains(name)) {
return Token(KEYWORD, name);
} else {
return Token(IDENTIFIER, name);
}
}
小技巧:关键字表用哈希表存,查找快。我见过有人用线性查找,代码量一大就卡得不行。
3.3 数字处理
Verilog的数字格式比较丰富。有十进制、十六进制、二进制、八进制,还能带位宽。比如8'hFF、4'b1010。
我曾经在项目里遇到过一个坑:有人写了'hFF,没指定位宽。按标准,这应该默认为32位。但我们的解析器没处理这种情况,结果仿真结果全错了。嗯,从那以后我特别注意默认位宽的处理。
数字的识别流程大致如下:
- 先看是不是以数字开头
- 如果是十进制,直接读完整串数字
- 如果后面跟着单引号,那就是带基数的数字
- 解析基数标识符(d、h、b、o)
- 然后读对应基数的数字部分
Token recognizeNumber() {
string value;
// 读十进制部分
while (isDigit(nextChar())) {
value += consumeChar();
}
// 检查是否有基数
if (nextChar() == '\'') {
consumeChar(); // 吃掉单引号
char base = consumeChar(); // d/h/b/o
string digits;
while (isHexDigit(nextChar()) || nextChar() == '_') {
if (nextChar() != '_') {
digits += consumeChar();
} else {
consumeChar(); // 忽略下划线
}
}
return Token(NUMBER, value + "'" + base + digits);
}
return Token(NUMBER, value);
}
注意:数字里的下划线是合法的,比如8'b1010_1100。解析时得跳过它们,但不能丢掉。
3.4 字符串处理
Verilog的字符串用双引号括起来。里面可以包含转义字符,比如\n、\t。
字符串的解析其实不复杂,但有个细节要注意:字符串里不能有未转义的双引号。我曾经调试过一个bug,就是字符串里漏了转义符,结果解析器把后面的代码都当成字符串了,报错一堆。
Token recognizeString() {
consumeChar(); // 吃掉开头的双引号
string content;
while (nextChar() != '"') {
if (nextChar() == '\\') {
consumeChar(); // 吃掉反斜杠
char escaped = consumeChar();
content += handleEscape(escaped);
} else {
content += consumeChar();
}
}
consumeChar(); // 吃掉结尾的双引号
return Token(STRING, content);
}
3.5 整体流程与状态机
词法分析器的核心就是一个循环:读字符,判断类型,生成Token。我习惯用一张状态图来理清逻辑。
这张图展示了词法分析器的核心逻辑。从开始状态出发,读一个字符,判断它的类型,然后进入对应的处理分支。处理完一个Token后,回到读字符状态,继续下一个。
3.6 避坑指南
做词法分析器,有几个地方特别容易出问题:
- 注释处理:Verilog有单行注释
//和多行注释/* */。多行注释里可能嵌套注释,得小心处理。 - 空白字符:空格、制表符、换行符都得跳过。但换行符在某些场景下有意义,比如宏定义。
- 文件结束:读到文件末尾时,得生成一个EOF Token。不然解析器不知道什么时候该停。
我曾经踩过的坑:多行注释没处理嵌套情况。结果人家写了/* /* */,解析器直接炸了。后来我加了个计数器,每遇到/*加1,遇到*/减1,归零才算结束。
3.7 代码骨架
最后,给一个词法分析器的基本骨架。你可以照着这个结构去填充细节。
class Lexer {
private:
std::string input;
size_t pos;
char peek() { return input[pos]; }
char consume() { return input[pos++]; }
public:
Lexer(const std::string& source) : input(source), pos(0) {}
Token nextToken() {
skipWhitespace();
if (pos >= input.length()) {
return Token(EOF_TOKEN, "");
}
char c = peek();
if (isAlpha(c) || c == '_' || c == '$') {
return recognizeIdentifier();
}
if (isDigit(c)) {
return recognizeNumber();
}
if (c == '"') {
return recognizeString();
}
return recognizeSymbol();
}
};
嗯,词法分析器这块就聊到这儿。记住,核心就是状态机加查表。把基础打牢了,后面的语法分析才能顺风顺水。
公众号:蓝海资料掘金营,微信deep3321