scriptc Typed IR 设计解析泛型单态化与联合类型标签化如何实现完整指南【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptcscriptc是一款把 TypeScript / JavaScript 直接编译为原生的编译器它先经 TypeScript 编译器完成解析与类型检查再产出一套类型化的中间表示Typed IR最终生成可读的 C 代码、LLVM IR、汇编、原生可执行文件和 WebAssembly。本文将带你读懂 scriptc 的 Typed IR 设计中最关键的两大机制——泛型单态化与联合类型标签化理解脚本语言的两个老大难类型特性是如何在原生代码里落地的。为什么需要 Typed IRscriptc 的编译流水线scriptc 的编译路径可以概括为一句话源码 → tsc 类型检查 →Typed IR→ C / LLVM IR / 汇编 / 原生可执行文件Typed IR 是整个编译器的交汇点前端负责把 TypeScript 的类型世界翻译成 IR 里的精确类型后端则从同一份 IR 生成 C 和 LLVM 两套产物从而保证两种产物在语义上逐条对齐。IR 的核心定义都在 packages/compiler/src/ir/ir.ts序列化与校验分别位于 packages/compiler/src/ir/serialize.ts 和 packages/compiler/src/ir/validate.ts。用--emitir就能直接看到产物输出为.scriptc/hello.ir.json这是观察 IR 最直观的方式$ scriptc build hello.ts --emitir泛型单态化让泛型在原生代码里消失原生代码没有运行时泛型的概念而 scriptc 的选择是monomorphization单态化泛型模板只在编译期存在每个具体调用点都会生成一份填好类型的实体化版本。前端类型层的单态化入口单态化的决策主要发生在前端类型映射阶段。在 packages/compiler/src/frontend/type-mapper.ts 中可以看到核心策略泛型函数体在内部仍按泛型处理checker 继续做类型推理但在对外边界上函数成员会monomorphize per call site——即按每个调用点的具体类型参数实体化。类似的按流单态化monomorphization by flow也用于程序泛型类例如 type-mapper.ts 中对 GENERIC 程序类的注释。举个直观的例子function firstT(items: T[]): T { return items[0]; }如果代码里既调用了first([1, 2])又调用了first([a])IR 层面最终会得到两个具体的函数版本——操作number[]的和操作string[]的而不是一个携带未知元素类型的模板。单态化的两条配套约定单态数组T[]被映射为元素类型确定的单态数组且递归成立number[][]也是单态的见 type-mapper.ts。这意味着数组在 IR 中永远是定长类型布局的原生数组而不是需要装箱的动态容器。结构化 record 内联化对象字面量 / interface / 类型别名统一映射为结构化的 record 形状前端按结构做去重shapeId 相同 ⇔ 形状相同后端再为每种形状生成一个真正的 C struct见 ir.ts 附近的record类型定义。 单态化的代价是代码膨胀收益是所有数组和结构体都能得到确定的内存布局后端可以生成真正高效的标量代码完全不需要 GC 或类型标签分派。联合类型标签化把A | B变成可运行的值TypeScript 的联合类型是静态特性原生代码里却没有对应物。scriptc 的答案是标签盒tagged box——每个联合值在运行时都是一个带标签的不可变堆对象。标签盒的内存布局IR 中的union类型定义ir.ts说明了表示方式联合值 堆上分配 引用计数 不可变的标签盒盒内只有两个部件运行时 tag该分支在规范排序分支列表中的下标一个 payload 槽联合本身按结构内联unionId相同 ⇔ 分支集合相同因此类型比较可以只比较 id所有联合定义登记在模块级的unions表中按首次出现顺序编号u0、u1…分支按 typeKey 排序后其下标就是运行时 tagir.ts。排序保证A | B与B | A是同一个联合——这是标签化能成立的关键前提。包装与解包unionWrap / unionNarrow标签化通过一对 IR 表达式完成闭环unionWrap包装当某个分支类型B的值流入A | B槽位时前端自动插入unionWrap——把值装进新分配的标签盒tag 取该分支的规范下标ir.ts。payload 的引用计数所有权直接移入盒中盒一旦构造即不可变。unionNarrow解包当控制流已经通过typeof/instanceof等判断证明分支后前端发出免检查的解包读取——因为 tsc 的收窄已经完成了证明运行时无需再判一次。这套包装必发生、解包免检查的设计让典型的switch (typeof v)分支模式在运行时只剩一次 tag 比较。union-retag避免反复装箱联合值在传递中经常遇到宽度变化——例如string | number流入更大的string | number | boolean槽位。scriptc 的 lowering 层有专门的 retag 处理packages/compiler/src/frontend/lowering/union-retag.ts与收窄辅助packages/compiler/src/frontend/lowering/union-narrow.ts尽量减少不必要的重新装箱判别符推导逻辑集中在 packages/compiler/src/frontend/union-discriminants.ts。如何验证你的理解跑一跑测试用例scriptc 仓库自带大量语料测试是理解这两套机制最好的活文档。推荐按顺序读主题测试语料泛型函数值tests/corpus/1950-generic-fn-values.ts泛型类基础tests/corpus/1951-generic-classes-basics.ts泛型方法tests/corpus/2000-generic-methods-basic.ts联合类型 retagtests/corpus/1556-union-retag-width-arms.ts联合字段共享读取tests/corpus/1532-union-shared-field-read.ts三元表达式数组 retagtests/corpus/2035-ternary-array-retag.cjs配合--emitir观察.scriptc/下的*.ir.json再对照 IR 校验器 packages/compiler/src/ir/validate.ts 的规则就能看到从源码到类型化 IR 的完整链路。延伸阅读从 IR 到原生IR 遍历与数据流分析packages/compiler/src/ir/analysis.tsC 后端从 IR 生成可读 Cpackages/compiler/src/backend/c/c-emitter.tsLLVM 后端packages/compiler/src/backend/llvm/emitter.ts项目总览与构建方式README.md总结scriptc 的 Typed IR 用两个决策解决了脚本类型系统在原生编译中的两大难题泛型 → 单态化模板留在编译期按调用点实体化换来确定性的原生内存布局联合类型 → 标签盒tag 单 payload 槽的不可变盒包装自动插入、解包免检查把静态收窄结论固化为一次廉价的运行时 tag 比较。理解这两套机制就掌握了阅读 scriptc 编译器前端type-mapper / lowering与后端C / LLVM emitter之间的通用语言。✅【免费下载链接】scriptcTypeScript-to-Native Compiler项目地址: https://gitcode.com/GitHub_Trending/sc/scriptc创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考