CLI 使用说明¶
这篇文档为什么要写得比“命令列表”更深¶
很多项目的 CLI 文档只会告诉你:
- 这个参数怎么写
- 那个参数会输出什么
但对编译器项目来说,这种写法不太够,因为你最后记住的只会是一串参数名,还是不知道自己到底在看哪一层。
因为这里的命令行工具不是单纯的“程序启动器”,而是整条编译流水线的观察窗口。
如果只是记住:
--emit-tokens会输出 Token--emit-ast会输出 AST--emit-ir会输出 IR
你还是可能不知道:
- 为什么要先看 Token,而不是先看最终输出
- AST 看错了说明的是语法问题还是语义问题
- IR 输出为空或走 fallback 到底意味着什么
--emit-c看到的内容是在验证前端,还是在验证后端
所以这篇文档不只是在说“参数怎么敲”,更想把下面这些事说清楚:
- CLI 在编译原理里对应什么
- 每个输出模式分别在观察哪一层
- 如何利用 CLI 逐层定位问题
CLI 在整个编译器里的位置¶
从代码结构上看,CLI 不是编译器本体,它更像前台窗口。你敲进去一条命令,它决定这次是带你看后台哪一层,还是直接一路走到底把程序编出来。
它的职责大致是:
- 读取输入源码文件
- 根据命令行参数决定停在哪一层
- 调用统一的编译流水线
- 输出某个中间结果,或者继续生成最终可执行文件
在当前项目中,这件事主要由 src/c_core_compiler/cli.py 和 src/c_core_compiler/pipeline.py 配合完成。
你把它想成下面这个分工就行:
pipeline.py管真正的流水线,负责一步一步往下走cli.py管你这次想看什么,决定在哪一层停下来看
最基本的完整编译命令¶
最普通的使用方式是:
这条命令看起来就一行,但它做的事并不含糊:
- 读取
examples/hello.c - 走完整编译流程
- 生成后端 C
- 调用系统编译器
- 输出最终可执行文件到
build/hello
这里的 -o 很关键,因为它等于是在告诉 CLI:别停在中间给我看过程了,这次我是来拿最终产物的。
为什么 -o 是一个分界点¶
在这个项目里,CLI 有两种主要使用模式:
1. 观察模式¶
例如:
--emit-tokens--emit-ast--emit-ir--emit-ir-dot--emit-c--emit-asm
这类模式的目标是“停在某一层输出”,不继续生成二进制。
2. 构建模式¶
例如:
这类模式的目标是走完整条链路并生成最终程序。
所以 -o 真就是一条分界线。带上它,你是在让编译器把整条路走完;不带它,你更多是在把它当成一台能开盖观察的机器。
--emit-tokens 在原理上是在看什么¶
命令:
这一层看的就是:词法分析第一刀切得准不准。
它回答的问题不是“程序能不能运行”,而是:
- 关键字有没有被识别成关键字
- 标识符有没有被切对
- 整数字面量有没有被识别
- 注释有没有被跳过
- 双字符运算符有没有被错误拆分
- 字符和字符串字面量有没有被正确扫描
这是最前面的一层。如果 Token 已经切歪了,后面所有阶段都只能在歪地基上继续往上搭。
什么时候优先看 Token¶
如果你遇到这些问题,第一反应应该是 --emit-tokens:
- 明明写了
==,后面像是按两个=处理 - 字符串或字符字面量行为异常
- 注释像是“漏进了代码”
- 某个报错位置看起来特别早、特别奇怪
这一层不能回答什么¶
Token 正确,不代表语法正确。
例如:
- 括号是不是匹配
if的条件范围对不对- 表达式优先级是否正确
这些不是词法层能回答的问题,必须往下一层看 AST。
--emit-ast 在原理上是在看什么¶
命令:
这一层在看的是:Parser 有没有把 Token 串整理成一棵像样的树,而不是表面看着对、里面挂得乱七八糟。
它关注的是:
- 顶层声明怎么分
- 函数定义怎么挂接参数和函数体
if / while / for的层次是否正确- 表达式优先级是否正确
- 赋值、调用、下标访问这些结构有没有被正确识别
从编译原理上讲,这一步是在验证“语法树是否符合预期”。
为什么 AST 是核心观察点¶
因为从这一步开始,后面的阶段基本都不再直接依赖原始 Token 序列,而是依赖 AST。
也就是说,一旦 AST 歪了:
- 语义分析看到的是歪结构
- IR Builder 降的是歪结构
- 后端最终输出也会跟着歪
所以 AST 是整条链路里最关键的中间观察点之一。
什么时候应该优先看 AST¶
例如这些情况:
- 你怀疑优先级不对,比如
a + b * c else像是绑错了iffor的初始化、条件、更新看起来行为怪- 函数调用和数组下标像是被解析错了
这时候先看 AST,通常比你盯着最终错误信息瞎猜有效得多。
--emit-ir 在原理上是在看什么¶
命令:
这一层在看的是:高层语法有没有被顺利拆成更低层、更像执行步骤的东西。
这里不是再看“代码长什么样”,而是在看:
- 临时变量怎么分配
- 控制流标签怎么生成
- 条件分支怎么展开
- 循环怎么改写成跳转结构
- 函数调用和返回怎么显式化
说人话就是:你在看编译器有没有把高层代码拆成一份后端真能接得住的步骤表。
为什么 IR 这么重要¶
因为 AST 仍然是高层结构,而 IR 更接近后端真正想要的形式。
例如:
- AST 里的
if还是一个语法节点 - IR 里的
if已经变成条件跳转、标签和显式控制流
一旦这一步对了,后端就不必重新猜测高层结构语义。
什么时候 IR 最值得看¶
如果你怀疑问题出在 lowering 或控制流展开,例如:
- 循环跳转行为怪
- 短路逻辑
&& ||表现不对 return路径异常- 某段控制流像是被展开错了
那么 IR 是最关键的排查点。
--emit-ir-dot 在原理上是在看什么¶
命令:
这一层和 --emit-ir 看的是同一份东西,只不过它把线性的 IR 文本摊成了图,适合你专门盯分支和循环这些容易绕人的地方。
它的价值在于:
- 更适合看分支图
- 更适合看循环结构
- 更适合检查标签之间的跳转关系
如果线性 IR 让你不容易一眼看出控制流形状,那么 DOT 版本更适合。
--emit-c 在原理上是在看什么¶
命令:
这一层看的就是:项目自己最后到底生成了一份什么代码,准备交给系统工具链去编。
它回答的问题包括:
- 后端是不是把 IR 正确翻译成了规范化 C
- 当前程序到底走的是 IR Backend 还是 AST Backend
- 输出结构是否足够规则、稳定、可编译
这一步离最终可执行文件已经很近了,但还没到系统编译器接手之后,所以它特别适合拿来区分“是我们自己生成错了”,还是“外部工具链后面才出事”。
为什么 --emit-c 特别关键¶
因为它刚好是项目内部逻辑和外部系统工具链之间的边界。
如果最终构建失败,你通常会问两个问题:
- 是项目前面的编译阶段就已经生成错了?
- 还是系统编译器在处理生成 C 时失败了?
--emit-c 和中间 .generated.c 文件正是用来回答这个问题的。
--emit-asm 为什么现在等价于 --emit-c¶
从名字看,很多人会以为:
--emit-c输出 C--emit-asm输出汇编
但当前第一代实现里,--emit-asm 被保留成兼容入口,实际输出的仍然是后端生成的规范化 C。
这样做不是故意把概念搅乱,而是先把调试入口固定住:
- 现在先输出 C
- 以后如果后端切换到原生汇编,仍然可以沿用同一个调试习惯
--target 在原理和工程上是什么意思¶
命令行里还有一个参数:
它的意义不是“切换前端语法”,而是告诉后端和工具链:你想按哪个目标平台配置来生成结果。
当前项目主要提供的是目标平台相关的后端元信息和组织方式,而不是完整的原生机器码后端切换。
所以这里更适合理解成:
- 后端目标配置选择
- 面向不同平台的生成与组织准备
而不是“像工业编译器那样完整切换到另一套机器代码后端”。
CLI 和两条编译路径之间的关系¶
这件事别轻轻带过,它会直接影响你后面怎么读输出。
当前项目不是所有程序都走统一路径:
- 基础
int子集:AST -> IR -> 优化 -> Backend C - 扩展能力如
char、数组、指针、字符串:AST -> AST Backend
CLI 本身并不强迫你选路径,而是通过程序内容自然触发不同路径。
它带来的实际影响很现实:
--emit-ir对某些高级特性程序,看到的不会是普通完整 IR 路径结果--emit-c更能反映当前程序最终到底走了哪条后端路线
所以 CLI 不只是“显示结果”,它还是理解编译路径差异的窗口。
一套最实用的排错顺序¶
如果你遇到“编译结果不对”或“行为异常”,非常推荐按这个顺序排查:
- 先看
--emit-tokens - 再看
--emit-ast - 如果是基础
int子集,再看--emit-ir - 必要时看
--emit-ir-dot - 最后看
--emit-c
这套顺序背后的想法其实很土,但非常好用:
- 从最靠前、最基础的表示开始排
- 每次只验证一层的职责
- 不把词法、语法、语义、后端问题混在一起
编译器调试说到底就是这个套路:别一头扑到最后一层去哭,先一层一层把范围缩小,很多问题其实很快就能看出来。
如果你是为了学习编译原理,CLI 最该怎么用¶
很多人会把 CLI 当成“跑一下示例”的工具,但对学习来说,更好的方式是:
- 选一个很小的示例
- 先看 Token
- 再看 AST
- 再看 IR
- 再看后端 C
- 最后再运行二进制
这样你会真的看到一段源码是怎么一步步变样的,而不是只知道“它最后编出来了”。
这比只看最终结果更能真正理解编译器。
一句话总结¶
这个项目的 CLI 不是单纯的“命令入口”,而是把编译流水线每一层暴露给使用者的观察接口。真正会用它的人,不只是会运行命令,而是知道每个参数在验证哪一层原理。