命令是怎么进入代码的
本节阅读量:上一节我们已经知道怎样运行:
|
|
这一节只看一眼 mini 的入口代码。先不用理解 parser、解释器、IR 和汇编生成的细节,只要知道:命令行参数进入 C++ 程序以后,是怎样被拆开、判断、再交给不同模块的。
这部分代码在:
|
|
后面每一章也都有自己的 src/cli/main.cpp。它们的形状基本一致:读输入文件,解析成 AST,然后按命令选择要做的事。
先把这一层的位置放进整体流程:
|
|
cli 是 command-line interface,也就是命令行接口。它负责把终端、文件和语言实现串起来,但不负责定义加法是什么意思,也不负责决定某条汇编指令怎样生成。
main 收到什么
C++ 程序从 main 开始运行。本课程里的入口长这样:
|
|
可以先把这两个参数读成:
|
|
程序自己的名字也算一个参数,所以这个例子里的 argc 是 3。
例如运行:
|
|
程序看到的参数大致是:
|
|
所以代码一开始会检查参数数量:
|
|
这里的意思是:如果连命令和输入文件都没有,就打印用法说明,然后用退出码 1 表示失败。
先拆出命令和文件名
接下来,入口代码会把最重要的两个参数取出来:
|
|
对于这条命令:
|
|
它们的值就是:
|
|
对于这条命令:
|
|
它们的值就是:
|
|
也就是说,mini 先取出要执行的模式和输入文件。只有进入 compile 分支后,它才继续处理 -o out.s 等编译选项。
读文件,得到 AST
拿到输入文件名以后,代码会读取文件内容:
|
|
如果 examples/add.lang 的内容是:
|
|
那么 source 里保存的就是这段文本。
然后这段文本会交给 parser:
|
|
这里的 expr 就是 AST,也就是语法树。第一章会正式讲 AST。现在可以先把它理解成:
|
|
后面的解释器和编译器都不直接处理原始文本,而是处理这棵 AST。
run 做什么
如果命令是 run:
|
|
这条路会调用解释器:
|
|
所以运行:
|
|
会直接打印:
|
|
这是一条“读懂程序,然后立刻算出结果”的路线。
ast 和 ir 做什么
ast 和 ir 是教学辅助命令。它们不是语言本身必须有的功能,而是为了让我们看见程序在不同阶段的样子。
如果命令是 ast:
|
|
它会打印 AST。
如果命令是 ir:
|
|
它会把 AST 转成 IR,再把 IR 打印出来。
这两个命令的作用是观察中间结果。后面读第一章时,如果某一步感觉抽象,可以先运行:
|
|
看见输出以后,再回头读代码会轻松很多。
compile 做什么
compile 比前面几个命令多一个输出文件参数:
|
|
所以入口代码会继续扫描后面的参数:
|
|
这里先看懂 -o 就够了:
|
|
如果忘了写 -o,代码会报错:
|
|
真正生成汇编的是最后两行:
|
|
这条路可以先读成:
|
|
注意,compile 到这里就结束了。它只生成汇编文本,不会调用系统工具链,也不会帮你链接出可执行文件。上一节里手动运行:
|
|
就是在完成 mini compile 之外的那一步。
第九章起,生成汇编会调用一个单独编译的 C++ GC runtime;那时手动链接会改用 c++ out.s gc_runtime.o -o out。CLI 的职责并没有扩大:compile 仍然只写 out.s,runtime object 由 make 构建,最终链接仍由读者显式完成。
报错会去哪里
整个 main 的主体包在 try 里,外面接着一个 catch:
|
|
这表示:如果读文件失败、编译选项写错或少了 -o,程序会把错误打印到标准错误输出,并返回失败退出码。标准错误输出也是终端中的一条输出通道,专门用来报告错误,不和正常结果混在一起。
例如:
|
|
因为少了 -o out.s,就会进入报错路径。
小结
第一次读 src/cli/main.cpp,不需要追进每个函数。先记住这一条线就够了:
|
|
cli 这一层不负责实现语言语义,也不负责决定每一行汇编怎样生成。它像一个总入口:先准备共同的输入,再把 AST 交给用户选择的处理路径。