HIT-编译原理 | 我从一串 C-- 代码里追出了词法、语法和中间代码
编译原理实验给我的第一个感觉,是“明明只是让程序理解一门小语言,为什么每一步都像在和自己较劲”。C-- 的语法并不算庞大,但从一串字符走到可执行的中间表示,中间要经过词法、语法、抽象语法树、符号表、类型检查和代码生成。任何一层没有把信息保存好,下一层都会接着出问题。
这组仓库里的三个实验,刚好记录了我怎样从“读懂一串 token”,慢慢走到“让编译器生成线性 IR,并尝试做一些优化”。它不是一个完整的工业编译器,却是我第一次真正摸到编译器内部的一段经历。
仓库地址:HIT-Compiler-Labs。
实验一:词法分析,先把字符认成有意义的东西
程序交给编译器的最初形态,只是一串字符。变量名、数字、关键字、运算符和括号混在一起,编译器首先要做的是把它们切成 token,并区分每个 token 属于什么类别。
刚开始看词法规则时,我很容易觉得这只是一些正则表达式:匹配到数字就返回数字,匹配到关键字就返回关键字。真正加入错误处理以后,才发现词法分析还要决定哪些字符是合法的、错误应该在哪里报告、换行和位置信息怎样保留。
语法分析进一步把 token 组成表达式、声明和语句。一个括号少了、运算符位置不对、函数参数结构不符合规则,都会让语法树无法建立。这个过程有点像把一堆散落的积木拼成一棵树,积木本身都认识还不够,拼接方式也必须符合规则。

这张 AST 图对我帮助很大。它让我第一次直观看到,源代码里的嵌套关系并没有消失,而是被转换成一棵结构化的树。后面的语义检查和中间代码生成,都要沿着这棵树继续走。
实验二:语义分析,代码语法正确不代表它有意义
有了 AST 以后,编译器还不能放心生成代码。因为“长得像一句合法语句”和“这句话有意义”是两回事。
比如变量有没有声明,函数调用的参数数量是否匹配,类型能不能参与某个运算,数组下标和返回值是否合理,这些都属于语义层面的问题。语法分析只负责确认结构,语义分析才开始追问这棵树表达的事情到底能不能成立。
这次实验的核心是符号表和语义遍历。每进入一个新的作用域,编译器就要知道哪些名字可见;离开作用域以后,内部声明又不能泄露到外面。名字相同但位于不同作用域的变量,不能简单地当成同一个对象。

符号表哈希链的图看起来有点像一串节点,但它背后解决的是一个很实际的问题:编译器怎样快速找到一个名字当前对应的声明。查找、插入、作用域进入和退出,都要保持一致,否则错误会在后面以更奇怪的形式出现。
我在这个实验里最明显的感受是,很多错误并不是“程序跑崩了”,而是编译器应该在更早的地方拒绝它。把错误尽量挡在语义分析阶段,后面生成的代码才有可信度。
实验三:从 AST 到线性 IR,编译器开始真正“写下下一步”
第三个实验进入中间代码生成。AST 适合表达嵌套结构,但要继续做优化和后续处理,通常需要一种更线性的表示。于是,表达式、赋值、跳转、标签和地址计算开始变成一条条更接近执行顺序的 IR。
这个阶段最容易让我迷路。源代码里一个数组访问,看起来只是一行表达式;到了中间代码里,却要拆成基址、下标、元素大小和偏移计算。一个 if 语句也不再只是“如果条件成立就执行大括号”,而是要生成条件跳转、标签和后续的控制流。

看到图里的符号和地址关系时,我第一次感觉自己不是在写一个语法处理器,而是在搭建一台小型的程序转换机器。输入是 C-- 源代码,输出还不是最终机器码,但已经是一个更容易被分析和优化的中间世界。
优化:删掉多余的临时量,也删掉没有意义的标签
实验里做的优化范围并不大,却很适合说明编译器为什么不能只负责“翻译”。有些赋值语句里的临时变量并不需要单独保存,右值可以直接把左值信息带过去;没有跳转目标的标签可以删掉;连续出现的标签可以合并。
还有常量表达式计算。如果一个表达式只依赖常量,那么编译器可以在编译期把它算出来,而不是留到程序运行时再算。这个想法看起来简单,但它要求编译器真的知道某个值是不是只由常量决定,同时不能误伤带有副作用的表达式。
我以前理解优化时,总觉得它是在做某种“聪明的替换”。这次才看到,优化其实是不断确认:这段代码保留吗?这个临时值真的需要吗?这个标签真的有人跳过来吗?编译器要做的不是随便改写,而是在不改变语义的前提下减少多余工作。
三个实验连起来,才看见编译器的流水线
词法分析负责把字符变成 token;语法分析把 token 组织成 AST;语义分析检查名字、作用域和类型;中间代码生成把树转换成线性结构;优化则在保证含义不变的情况下清理和改写。
如果只看某一层,很容易把编译器理解成某个孤立的算法。把三个实验连起来以后,我才发现它更像一条流水线:前一层保存的信息,会决定后一层能不能做得可靠。词法阶段没有位置,错误信息就不清楚;AST 结构不稳定,语义遍历就会混乱;符号表不完整,中间代码里的变量和地址就没有依据。
现在回头看,最难的是不要急着跳到下一层
当时我很想尽快看到 IR,觉得前面的词法和语法只是准备工作。后来发现,真正影响调试效率的,往往就是这些“准备工作”。如果 token、树结构和符号信息都能打印出来,后面的错误会有迹可循;如果前面只留下一个“解析失败”,后面只能靠猜。
这组实验让我开始喜欢那些中间结果:一棵 AST、一段符号表、一串线性 IR。它们不是最终产品,却把程序转换过程中最关键的判断暴露出来。理解编译器,很多时候不是直接看最后生成了什么,而是看它每一步为什么这样生成。
仓库地址:HIT-Compiler-Labs。
说明:本文根据个人历史课程实验和公开仓库整理,部分文字经过 AI 辅助润色;报告图片和结果属于课程实验记录,当前工具链可能需要按环境调整。课程资料仅用于学习回顾和个人作品整理,不用于当前课程作业或考试。

浙公网安备 33010602011771号