对应实践:Lab04 - step_4 语义分析 主要修改文件:
miniCompiler_lab/labs/lab04-step4/framework/student.c
到了这里,编译器已经能把源码变成 AST。但“能建树”不代表“程序合法”。一份语法完全正确的程序,仍然可能犯很多错误:使用了没定义的变量、同一作用域里重复定义变量、把不兼容的类型硬塞到一起。
这些问题已经不是词法问题,也不是语法问题。它们属于语义分析阶段。
4.1 为什么 AST 正确还不够
看下面这段代码:
int main() {
int x = 10;
int sum = x + undefined_var;
int x = 20;
return 0;
}
从 token 角度看,它很正常。
从 AST 角度看,它也完全能建出来。
但你一眼就知道它有问题:
undefined_var没有定义- 同一个作用域里
x被重复定义
这正是语义分析存在的理由。它要回答的问题不再是“这段代码像不像 C”,而是“这段代码在语言规则下是否合法”。
4.2 符号表到底解决什么问题
编译器要判断一个名字是否合法,最基本就得知道两件事:
- 这个名字有没有出现过
- 它在哪个作用域里出现过
符号表就是为这个问题准备的。
你可以把它理解成编译器在分析过程里维护的一本“登记册”。每当看到变量声明或函数定义,就把名字记进去;每当看到标识符使用,就去查这本册子。如果查不到,就是未定义;如果当前作用域里已经有同名条目,就是重复定义。
4.3 作用域为什么不能只靠一个平面表
如果程序永远没有代码块,那么一个平面链表也许勉强够用。但只要有 { ... }、函数体、条件分支里的局部声明,问题就变了。
例如:
int main() {
int x = 1;
{
int x = 2;
return x;
}
}
这里内层的 x 并不是非法重复定义,而是一个新的、遮蔽外层名字的局部变量。
这说明编译器必须能区分“当前作用域”和“父作用域”。
所以本章的符号表不能是单张平面表,而应该是带 parent 指针的作用域链。进入新块时进入新作用域,离开时再退回父作用域。这种结构会一路影响到后面的代码生成,因为变量栈偏移本身也依赖作用域中的符号信息。
4.4 语义分析器真正做的事
语义分析并不是对整棵 AST 做一次抽象的“检查”。它更像一次带上下文的遍历:
- 看到变量声明时,检查当前作用域能不能加这个名字
- 看到标识符时,查符号表看它有没有定义
- 看到赋值时,确认左边名字存在、右边表达式类型合理
- 进入 block 时切换作用域
- 离开 block 时恢复作用域
也就是说,语义分析的关键不在“遍历”本身,而在“遍历时手里一直带着符号表上下文”。
4.5 现在最值得看的错误输出
当语义分析发现错误时,你应当期待类似这样的输出:
语义错误 [7:19]: 未定义的变量 'undefined_var'
语义错误 [10:9]: 变量 'x' 重复定义
编译失败: 2 个语义错误
这组输出很重要,因为它第一次让你看到编译器不只是“生成东西”,还要“拒绝不合法输入”。对于真实编译器来说,这不是附加功能,而是核心职责之一。
4.6 本章 practice 边界
这一章的 practice 路径是:
miniCompiler_lab/labs/lab04-step4/
主要修改文件:
framework/student.c
验证命令:
cd labs/lab04-step4
make clean && make test
这一章 lab 会把语义分析压成三个核心动作:
student_symtab_addstudent_symtab_lookupstudent_semantic_analyze
这三个动作正好覆盖了语义阶段最关键的闭环:
- 记录名字
- 查找名字
- 带着符号上下文走完整棵树并统计错误
4.7 本章最容易误判的地方
这一章最常见的误判有两个。
第一种是把“父作用域里存在同名变量”也当成重复定义。
这是不对的。重复定义说的是当前作用域里重复,不是全局禁止重名。
第二种是查找变量时只查当前作用域。
这也不对。很多合法引用本来就应该向父作用域回溯。
如果这两个边界没想清楚,你的语义分析器很容易在简单样例上看起来能跑,但一遇到嵌套 block 就开始乱报错。
4.8 本章小结
从这一章开始,编译器第一次真正站到“语言规则裁判”的位置上。AST 只是结构,语义分析才决定这棵树到底能不能进入下一阶段。
后面的代码生成之所以值得信任,前提正是:输入给它的 AST 已经经过语义筛查,不再是“长得像程序”的任何东西。
4.9 下一步
现在去完成:
- Lab04 - step_4 语义分析
- 观察未定义变量和重复定义是否被正确报出
- 然后进入 Chapter 5 — RISC-V 代码生成