冲泡 Cappuccino:在没有 LLVM IR 的情况下编写编译器

发布: (2025年12月22日 GMT+8 01:25)
5 分钟阅读
原文: Dev.to

Source: Dev.to

Introduction

编译器一直让我觉得像魔法。它们既复杂又简单——只是把代码从一种形式转换为另一种形式的程序。我花了无数小时研究 clang 生成的汇编,观察 C 是如何被转化为机器码的。于是我产生了好奇,想知道 如何 编写一个编译器。

So how did I do it?

我做的第一件事是 Google “how to make my own programming language”,这把我引到了这篇文章。它提供了编译流水线的粗略概念,但作者实现的是一个把代码转译成 C++ 的转译器。我对这种做法并不满意。

在翻阅了更多网站、文章和 GitHub 仓库(那时 ChatGPT 还不流行)后,我提炼出了任何编译器的核心组件:

  • Tokenizer – 读取源文件并生成 tokens,即最小的有意义词素。
  • Abstract Syntax Tree (AST) – 程序结构的树形表示。
  • Parser – 将 token 流转换为 AST。

常见的捷径是转译到 LLVM 的 IR,然后让 LLVM 生成二进制。我不喜欢这种方式,因为它把最有趣的部分——自己生成汇编——抽象掉了。虽然 LLVM 的 IR 很强大,但它把语言绑定到特定平台,对于“无外部依赖”的项目来说感觉像是作弊。

既然我已经对汇编比较熟悉,我决定自己动手实现后端。

The part where it got messy

我很快发现 tokenizer、parser 和 AST 都需要一个坚实且一致的结构;否则整个系统会崩溃。

我最初的 AST 设计大致参考了这篇文章。它在小例子上还能工作,但一旦我尝试生成汇编,代码就变得难以维护。我选择实现的语言是 C。我热爱 C,也写过不少 C 项目(例如anishell),但在 C 中处理动态数组和垃圾回收非常繁琐。经过反复重写 AST 并为每种节点类型创建结构体后,我最终改用 C++——这让人如释重负。

有了 C++ 后,我研究了正式的解析技术,最终决定使用 recursive‑descent parser,它提供了超出简单计算器示例的灵活性。

Actually Generating the Assembly

从概念上讲,生成汇编很直接:使用栈机器模型——从栈中弹出操作数,计算结果,再把结果压回栈。真正的挑战在于那些潜伏的细小 bug。变量顺序的一个不匹配就可能导致整个程序灾难性失败。

当我的 C 实现一直报错时,我卡住了,于是暂停了项目。后来改用 C++ 并借助 AI(Gemini)进行调试,我终于把问题逐一解决。随后我加入了一个小型标准库,并最终为项目取名:Cappuccino

What did I learn?

  • 我还有很多东西需要学习,应该在投入多年时间与糟糕设计搏斗之前先做足研究。
  • 项目名 Cappuccino 的灵感来源于 Java 的咖啡主题命名,以及我个人对咖啡的热爱。

完整源码已在 GitHub 上公开:https://github.com/AnirudhMathur12/cappuccino

感谢阅读我的第一篇博客。如果你觉得有趣,请考虑给仓库点个星。

Back to Blog

相关文章

阅读更多 »

数据架构师大师专业工作簿

概述:我构建了一个模块化、可审计的数据工程项目,并希望与社区分享。特性——干净的、生产级别的 Python——SQL pat...

为什么开源

介绍 本文档解释了我如何开始编写一个 open‑source 软件项目。原因众多且有趣。这是一份非技术性文档……