Fooxygen
Bravely forward to pursue my dream.

Coming Soon

Xero Diary #3 | 从解释器到编译器的功能对齐

最后编辑于 2026-09-24
xerocompilerprogramming-languagec/cpp

Note

存储库与 Wiki:https://github.com/Fooxygen/Xero

欢迎提交 issue 告诉我你的想法,或报告 Bug,或者 Star 支持一下~

经过约 4 周的持续更新,Xero 整体已转移至编译器架构,完全废弃了解释器代码。由于改动的内容实在是太多,具体的特性请前往 Wiki 来查看。

整体结构

src/
├─ common/
│  ├─ defs/
│  └─ utils/
├─ lexer/
├─ parser/
├─ sema/
│  └─ defs/
└─ xcompiler/
   ├─ backend/
   ├─ defs/
   │  └─ types_init/
   ├─ ir/
   └─ optimizer/

核心变化

架构的核心变化基本集中于 Sema 和 Xcompiler。它们在设计上几乎是对称的结构,因为分别承载了一一对应的定义与实现。

例如实现新类型时,语义层 Sema 负责定义命名,设定基本类型/参数化类型等等,以及注册方法签名,这就构造了 Type。对应的在实现层 Xcompiler 负责找到 Type,与一个新的 TypeImpl 绑定,然后补全方法实现。完成这些操作的,也分别都有对应的 TypeTable 和 TypeImplTable。

处理入口

现在各个模块均抽出入口,提供给主入口调用。过去散落在 main 的各模块操作已经被收入模块相关文件,如 lexer 和 parser 的构造函数中,sema.hpp 和 xcompiler.hpp 中。

Sema

架构

  • ./defs/:类型、函数的定义,变量及作用域变量表的定义;
  • ./analyzer.cpp:对 Ast 的递归处理;
  • ./builtin.cpp:内置函数;

Type

除了先前的基本/参数化类型,现已扩展到 引用类型 ReferenceType和 绑定类型 BindingType,它们都是继承自 Type 基类。

类型定义部分在 ./defs/type_init.cpp 中。基本步骤是按名称生成类型,然后分类注册类型方法定义。

方法定义的注册需要的是方法签名,只用写出返回类型,调用方类型、形参类型,以及函数修饰符即可。写法上我们按 @cast,@copy,@release, @assign 和 Other 分开定义,其中特殊方法会在 Xcompiler 详细解释。

Fn

这是本次改动中期较为麻烦的一个模块,我们来快速理解函数体系:

  • Fn:函数,表示某一个具体的函数,除了名称,还携带了一个或若干个 FnSign;
  • FnSign:函数签名,记录返回类型,调用方类型、形参类型,以及函数修饰符;
  • FnModifier:函数修饰符,例如声明函数为转换方法 Cast;
  • FnTable:函数表,通用容器;

Fn 携带多个 FnSign,是函数重载的实现方式。因 Class 特性未实现,所以我们没法在 Xero 源代码中手动写出修饰符。

Xcompiler

架构

  • ./backend/:LLVM 后端;
  • ./defs/:类型、函数的实现,LLVM 变量及作用域变量表的定义;
  • ./ir/:LLVM IR 生成逻辑;
  • ./optimizer/:LLVM 优化器接入;
  • ./builtin.cpp:内置函数;

Type

类型实现部分在 defs/types_init/ 中,基本步骤是通过类型定义表 TypeTable 找到定义,然后与新创建的实现 TypeImpl 建立联系,存储在类型实现表 TypeImplTable 中。

类型实现层 TypeImpl 主要存储方法的实现,同时通过写出明确的函数签名,以再次找到先前定义的签名来匹配。

Fn

函数实现 FnImpl 分为原生函数实现 NativeFnImpl 和语言函数实现 LangFnImpl,它们都有各自的类型实现存储载体。

NativeFnImpl

原生函数实现,载体是 std::function。

对于某些函数或方法,其实现是预先被写入 Xero 中的,原因是实现需要与 LibC 关联,或为开发者提供必要的工具。

这主要被使用在类型方法实现时,通过 Lambda 函数写入,在调用函数时就直接执行该 C++ 回调。

LangFnImpl

语言函数实现,载体是 llvm::Function*。

在 Xero 源代码中定义的一切函数和方法,其实现由 LangFnImpl 管理。

在生成 IR 时,LLVM 会创建函数,并把函数逻辑的 IR 写入其中,此时就能拿到 llvm::Value。我们不展开讲 IR 的生成实现。

部分功能演进的特殊点

值类型与引用类型

基本和参数化类型属于前者,它们的值有独立的内存空间存储,而引用类型的值只是指向某个已存在值的地址。注意 Xero 中没有引入指针特性。

赋值

在赋值时,值类型会进行深拷贝,引用类型则是同样地创建一个别名。

函数参数传递

参数传递的难点在于值类型与引用类型语义不同。

实参记录

实参统一抽象为函数参数 Arg,类型的不同,存储和决策也不同。若为引用类型,则 val 是地址,反之是值;

调用方传入

调用方法的调用方 Caller 必须可作为引用写入。因此当 Caller 为临时值时(如 3.@plus(2)),需由 ValMaterialize 开辟临时内存、实体化为引用。这主要是为了统一执行路径,使得我们对 Caller 的操作都是基于引用来完成的。

值传递还是引用传递?

传递诸如数组这些需要开辟内存的类型值时,若不带引用则会完整进行拷贝,数据量大时则性能不佳。因此非常建议使用引用传递。

拷贝与释放

类型可实现 @copy 与 @release 方法。

声明与赋值时:

  • 左值调用 @release 释放原有内容;
  • 右值为变量则调用 @copy 拷贝,否则直接转移内存;
  • 对 stringview、arrayview 等视图,整体覆盖并不合适,改由 @assign 定义赋值逻辑;

工具函数

为了统一处理值与引用之间的地址与取值问题,我们抽取出一组工具函数:

  • IdResolve 取标识符地址(变量本身为引用时取其保存的地址);
  • ValMaterialize:为值开辟内存;
  • ExprLoad:从表达式取值(必要时解引用);
  • ArgRefMake/ArgLoad/ArgAddr:在值与引用间转换 Arg。

绑定类型

有一些类型很特殊,它们必须以参数化形式使用才有完整的能力,例如我们应使用 array[=i32=] 而不是 array。在这种类型的方法定义中,我们没法知道源代码中使用了什么样的参数,这就为函数签名的定义带来了困扰。

我们可以引入 绑定类型 BindingType,定义一个不插入至类型定义表中的绑定类型 T,它将被写入其归属类型的参数绑定列表中。

当源代码使用某归属类型的参数化形式时,该归属类型中所有含绑定类型的方法都会作为模板,各特化一份放入参数化类型的类型方法表中。

模板方法特化

以 array[=i32=] 为例,凡是含绑定类型 T 的签名,就用 i32 替换 T,生成实例签名。

参数化类型的参数不限于 1 个,实际可有任意数量,替换时按位置一一对应。假设有 type[=T, Q=],如果某参数化类型为 type[=i32, f64=],那么 i32 替换 T,f64 替换 Q。替换由 BindingTypeReplace() 完成。

替换方法接收三样东西:

  • 待处理类型:即签名中可能出现 T 的类型,如返回类型 array[=T=]、形参类型 T&;
  • 归属类型:绑定类型归属的类型,用于取出该归属类型中的绑定类型列表;
  • 替换类型列表:与绑定类型列表按位置一一对应的类型表;

替换方法分四种情况考虑待处理类型:

  • 绑定类型:在绑定类型列表里定位后,取替换类型列表中同位置的类型;
  • 引用类型内包含着绑定类型:递归替换被引用类型,再重新包成引用类型;
  • 参数化类型内包含绑定类型:递归替换参数,再重新包成参数化类型;
  • 不含绑定类型的类型原样返回;

得到的实例签名会记住原签名,实现层据此复用同一份实现。

总结

切换至 LLVM 框架后,表达指令方面上确实有更多复杂的实现,有时会觉得没有做解释器时大量创造新特性时那么快乐。

Xero 架构演变的还算快,同时我也越来越发觉代码的更新未能与 Wiki 或 Diary 的更新速度有好的匹配。必须承认的是我暂时没有太多精力字斟句酌地修改 Wiki,大量借助了 AI 来帮忙生成。Diary 还写的可以,但 Wiki 要写出较好的水平,我还得多琢磨琢磨。

接下来慢慢可以恢复新功能开发了,从较为枯燥的一些方面上抽离出来做更有意思的部分。

该文章的其它发布链接