StaticScript 类与结构体编译原理:TypeScript class 如何变成 LLVM StructType?
StaticScript 是一个以 TypeScript 为前端、LLVM 为后端的开源编译器,它的目标是把 TypeScript 代码直接编译成原生二进制。今天我们用最通俗的方式,拆解它最核心的一环:TypeScript class 如何一步步变成 LLVM StructType。无论你是编译器新手,还是好奇"类到底怎么在底层表示"的开发者,这篇文章都能帮你快速建立认知。
如上图所示,StaticScript 的整体流程是:.ts 源码 → LLVM-IR → Bytecode → 目标文件 → 与运行时库链接,最终得到可执行文件。而 class 到 StructType 的转换,就发生在"生成 LLVM-IR"这个阶段。
核心思路:class 本质上是"结构体 + 构造函数"
在 LLVM 的世界里,并没有"类"这个概念。TypeScript 的 class 到了 LLVM 眼里,就是一个结构体(Struct):类的每个属性,就是结构体的一个字段;类的实例化,就是调用一个返回结构体指针的函数。
这个思路是整个转换过程的地基,记住它,后面的步骤就都顺理成章了。⚡️
第一步:用 TypeScript 类型检查器收集类的属性
编译流程的入口在 src/backend/llvm/index.ts:当遍历到 ClassDeclaration 语法节点时,会被转交给专门的 ClassDeclarationGenerator(位于 src/backend/llvm/code-generation/class-statement.ts)。
它做的第一件事,是借助 TypeScript 编译器 API 拿到类的"体检报告":
- 通过
ctx.typeChecker.getTypeAtLocation(node)获取 class 的类型对象; - 再用
getPropertiesOfType(classType)拿到该类型的所有属性(成员)列表。
这一步很聪明——它复用了 TypeScript 前端的类型分析能力,不需要自己重新解析属性,这正是"TypeScript 做前端"这句话的真正含义。
第二步:把 TS 类型映射成 LLVM 原生类型
拿到属性列表后,就要确定每个属性在 LLVM 中对应什么类型。这个映射工作由 NativeTypeResolver(源码在 src/backend/llvm/native-type-resolver.ts)完成,规则非常直观:
| TypeScript 类型 | LLVM 类型 | 说明 |
|---|---|---|
number | double(双精度浮点) | 数值默认按浮点处理 |
string | i8*(字符指针) | 字符串用指针表示 |
boolean | i1(1 位整数) | 布尔就是 0/1 |
int8 / int16 / int32 / int64 / int128 | 对应位宽的整数类型 | 显式整数类型 |
uint8 ... uint128 | 无符号整数 | 带符号位标记 |
float32 / float128 | 对应浮点类型 | 显式浮点类型 |
void | void | 常用于函数返回 |
有了这张"翻译表",每个 TypeScript 属性都能找到自己的 LLVM 原生类型。🧩
第三步:创建并填充 LLVM StructType
类型映射完成后,就该真正"造结构体"了。代码里只有短短几行,但信息量很大:
const struct = llvm.StructType.create(ctx.llvmContext, structName);
struct.setBody(properties.map((property) => {
const nativeType = NativeTypeResolver.getType(
ctx.typeChecker.getTypeOfSymbolAtLocation(property, node),
ctx
);
return nativeType.getType();
}));
过程分两步:
StructType.create先按类名创建一个"空"结构体类型(注意这里的名称就是 TypeScript class 的名字);setBody再把属性们映射好的 LLVM 类型按声明顺序填充进去,结构体就此定型。
也就是说,一个 class People { weight: number; length: number; } 最终会变成一个包含两个 double 字段的 LLVM 结构体,字段顺序与源码声明顺序完全一致。📦
第四步:把结构体注册进类的符号表
结构体建好后,不能就这么扔掉,后续 new 表达式还要用到它。所以最后一步是把它登记到作用域(Scope)的类表中:
ctx.scope.classes.set(structName, struct);
Scope(源码在 src/backend/llvm/scope.ts)里维护着一张 ClassesTable,本质是 Map<string, llvm.StructType>——类名到结构体类型的映射表。凡是编译过程中遇到过的 class,都能在这里查到对应的 LLVM StructType。
更有意思的是,Context(src/backend/llvm/context.ts)在初始化时就已经预注册了一批"内置类":Uint8Array、Int32Array、Float64Array 等 TypedArray 全部以结构体形式预先登记,甚至数组本身也是用结构体表示的,形如 array<type>,内部包含元素类型、size(大小)和 capacity(容量)三个字段。
new 表达式如何找到这个 StructType?
实例化类时,NewExpressionGenerator(src/backend/llvm/code-generation/new-expression.ts)会登场。它通过 buildFromIdentifier 在类的符号表中查找类名,命中后得到一个 ClassReference——它并不代表某个具体值,而是携带了对应的 StructType 引用,再配合构造签名生成构造函数调用,最终包装成 ObjectReference 使用。
这里还有一个细节:Value 体系(src/backend/llvm/value.ts)里区分了 ClassReference、ObjectReference、Primitive 等不同角色,类型信息随值一起流动,这也是编译器能够持续追踪"每个值是什么类型"的关键。
当前限制:这些 class 写法还不支持
作为教学价值极高的实验性编译器,StaticScript 的 class 支持目前是"精简版",了解这些限制能让你少踩坑:
- ❌ 泛型类:遇到
typeParameters会直接抛出 UnsupportedError; - ❌ 类方法:类里只允许属性声明,出现方法会被拒绝;
- ❌ 继承:测试用例
tests/snapshots/general/class.ts中带extends的代码目前处于注释状态,说明继承尚未实现; - ✅ 能用的:属性声明 +
new实例化 + 属性访问。
总结:一条清晰的四步流水线
回顾一下,TypeScript class 变成 LLVM StructType 的完整链路是:
- 🔍 TypeScript 类型检查器收集类的属性列表;
- 🧩
NativeTypeResolver把 TS 类型翻译成 LLVM 原生类型; - 🏗️
StructType.create+setBody按序填充字段,造出结构体; - 📋 把"类名 → 结构体"注册进
Scope.classes符号表,供new表达式查用。
StaticScript 用最直白的方式演示了"面向对象语法如何降级为结构化数据"这一经典编译思路。如果你想亲手验证,克隆 https://gitcode.com/gh_mirrors/sta/StaticScript 仓库,然后从 src/backend/llvm/code-generation/class-statement.ts 读起,沿着上述四步逐行对照,相信你会收获满满。🚀
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考




