Chapter 1 — step_1 编译器最小驱动与文件 I/O

对应实践Lab01 - step_1 最小驱动 主要修改文件miniCompiler_lab/labs/lab01-step1/framework/student.c

很多人一开始学编译器,脑子里最先冒出来的都是“文法”“AST”“代码生成”。但真正落到一个可运行项目时,第一步反而没有那么学术。你得先解决一个更朴素的问题:这个程序到底怎样从命令行拿到输入文件,怎样把源文件读进内存,又怎样把第一份汇编文件写出去。

这不是绕远路。恰恰相反,这是编译器项目最早的生存线。如果连这一层都没有,后面的 lexer、parser、semantic pass 就只能停在函数级玩具里,永远没有机会被串到一次真实的“读源文件 -> 产出目标代码”的工作流里。

1.1 为什么正式课程要从这里开始

第一章的编译器还没有真的理解 C 语言。它甚至不会分析 token,更不会构建 AST。它做的事情非常克制:处理命令行参数、读取输入文件,然后输出一份固定的 RISC-V 汇编模板。

表面上看,这很“简陋”;但从课程设计角度看,它刚好建立了后面整门课的工作地面:

  1. 你第一次得到一个真正的 minicc input.c -o out.s
  2. 你第一次看到“编译器输出的不是可执行文件,而是汇编”
  3. 你第一次把 C 源程序和后续 RISC-V 工具链串起来

等这条最短链路打通以后,后面每一章就不再是孤立的算法练习,而是在同一个编译器外壳里不断替换和增强内部能力。

1.2 这一章结束后你应当会什么

做完这一章,你至少应当能回答下面几个问题:

  1. 为什么一个编译器项目在最早阶段就需要命令行参数和文件 I/O
  2. input.c -> out.s 这条链路里,编译器此刻负责哪一段,交叉工具链又负责哪一段
  3. -o-v 这种命令行选项为什么值得在第一章就实现
  4. 为什么即使输出的是固定汇编模板,这一步仍然是“真实编译器工作流”的一部分

然后在实践里,你会把这四个认识压缩成三件具体事情:

  • 解析命令行
  • 读取源文件
  • 写出最小汇编骨架

1.3 先看本章数据流

先别急着写代码,先把这一章的数据流看清楚:

命令行参数 -> 输入文件路径 -> 读取源码文本 -> 写出固定汇编模板

进入 lab 之后,你会在 framework/student.c 里补完三个函数。不要试图一上来把所有细节都写满,而要先带着三个问题去读 TASK.md 和验证程序:

  1. 参数解析怎样记录输入文件、输出文件和详细模式
  2. 文件读取怎样把整份源文件读成一块连续内存
  3. 汇编输出函数怎样往输出文件里写一份最小的 RISC-V 程序

在这一章里,这三件事比“它生成的汇编到底聪不聪明”更重要。因为此刻我们要建立的是编译器作为一个程序怎样活起来,而不是立刻建立“它怎样理解语言”。

1.4 固定汇编输出为什么不是假动作

第一次看到固定输出时,很多同学会下意识问一句:既然它还没分析源代码,这不就是“假装在编译”吗?

这句话只对了一半。

它当然还没有进入真正的语言理解阶段,但它已经完成了三件非常真实的工程动作:

  1. 接受一个源文件作为输入
  2. 产生一个汇编文件作为输出
  3. 把输出交给后续工具链继续汇编、链接和运行

这一步的意义,不在于它生成的汇编多聪明,而在于它第一次把编译器前端程序目标平台工具链接到同一条流水线上。

只要这条线已经连上,后面你给它换成真正的 token 流、AST、语义检查、代码生成,整个项目依然站得住。

1.5 这一章的 practice 边界

现在进入本章实践:

  • lab 路径:miniCompiler_lab/labs/lab01-step1/
  • 主要修改:framework/student.c
  • 验证命令:
cd labs/lab01-step1
make clean && make test

这一章的 lab 没有让你重写整份 minicc.c,而是把最关键的三段抽了出来:

  1. student_parse_args
  2. student_read_file
  3. student_emit_stub_program

这样的拆法是刻意的。它把“最小驱动”压成了三个可以单独验证的动作。只要你先把这三步的意图看明白,后面的代码就不会显得杂乱。

1.6 你在写的不是“打印字符串”,而是在搭编译器出口

student_emit_stub_program 看起来最容易被误解。因为它做的事情好像只是往文件里 fprintf 几行文本。

但这里真正值得你建立的直觉是:

对编译器来说,汇编文件不是日志,不是调试输出,而是它对外部世界交付的正式产物。

也就是说,从这一章开始,你就应该把“生成文本”理解成“生成目标代码”。哪怕此刻这份目标代码还很简单,它的角色已经确定了。

后面课程再往前走,你做的并不是把“输出文件”这个概念推翻重来,而只是把固定模板逐步替换成真正从 AST 推导出来的汇编。

1.7 做 lab 时最容易犯的三个误解

第一种误解是:觉得参数解析只是边角料。
不是。没有清晰的参数入口,后面你很难区分“编译器坏了”还是“输入命令不对”。

第二种误解是:觉得读文件只是模板代码。
也不是。后面 lexer 扫描的整条 token 流,起点就是这里读进来的字符缓冲区。

第三种误解是:觉得固定汇编输出没有学习价值。
还是不对。它第一次把“编译器输出是汇编”这件事 concretely 放到了你手上。

1.8 本章小结

这一章故意没有立刻进入词法分析。因为在你开始理解语言之前,先得有一个真正能接收输入、读文件、写出结果的编译器外壳。

只要这个外壳已经站住,后面的 lexer、parser、semantic、codegen 就不再是分裂的小题,而是不断填进同一个项目身体里的器官。

1.9 下一步

现在去做:

  1. Lab01 - step_1 最小驱动
  2. 跑通 make clean && make test
  3. 然后继续进入 Chapter 2 — 词法分析器