- 编程语言
- 编译器
- 云原生
- 语言运行时
【免费下载链接】wing
A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡
导读
本文以 Wing 开源仓库中double_reference.test.w的tf-aws 编译快照(double_reference.test.w_compile_tf-aws.md)为解剖样本,完整展示一条 Wing 测试用例从源码出发,经过编译器 lift(提升)分析后产出的 inflight 闭包、Terraform 配置与 preflight 运行时代码。读者通过本文可以掌握:Wing 如何为“同一对象被直接引用与间接引用”的双重引用场景生成 lift 依赖、如何将cloud.Counter映射为 AWS DynamoDB 表,以及 hangar 快照测试工具链如何自动化验证编译结果。
一、测试用例背景:什么是 double_reference
快照对应的 Wing 源码位于 tests/valid/double_reference.test.w(该相对链接源自快照文档首行[double_reference.test.w](https://link.gitcode.com/i/638fa93017dad06a0fb3f76bf2d5b0f4),这里已按仓库根路径重写):
bring cloud; let initCount = new cloud.Counter(); class Foo { inflight new() { initCount.inc(); } pub inflight method() { } } class Bar { pub foo: Foo; new() { this.foo = new Foo(); } pub inflight callFoo() { this.foo.method(); } } let bar = new Bar(); test "hello" { bar.callFoo(); bar.foo.method(); // TODO: https://github.com/winglang/wing/issues/3244 assert(initCount.peek() == /*1*/ 2); }这段用例刻意构造了一个容易引发引用分析歧义的场景:
- 间接引用:测试体内调用
bar.callFoo(),而callFoo()内部又调用this.foo.method(); - 直接引用:紧接着测试体又直接访问
bar.foo.method(); - 共享对象:
Bar的构造器在 preflight 阶段创建了Foo实例并存入字段this.foo,因此bar与bar.foo两个 preflight 对象之间存在“拥有者—成员”关系; - 计数校验:
Foo的inflight new()每次实例化都会initCount.inc(),测试最终断言initCount.peek() == 2。
源码中的/*1*/ 2与 TODO 注释对应 winglang/wing#3244 这一历史问题:早期实现中bar.callFoo()与bar.foo.method()两条路径可能重复请求同一对象的 lift,导致initCount的初始化逻辑被重复触发。这个用例的目的,就是确保编译器对“双重引用同一对象”的情况能正确去重。
该文件位于
tests/valid/目录下,按 paths.ts 的定义,validTestDir = tests/valid,其中的*.w文件都会由 hangar 自动生成编译与运行测试,属于必须通过的合法用例集。
二、快照文档的结构:一份 tf-aws 编译产物全景
快照文件 double_reference.test.w_compile_tf-aws.md 的文件名遵循 hangar 的命名约定:<源码名>_<测试类型>_<目标平台>.md。其中compile表示编译测试,tf-aws表示目标平台。文档共收录 5 个编译产物,按文件名排序:
| 产物文件 | 语言 | 内容 |
|---|---|---|
inflight.$Closure1-1.cjs | CommonJS | 测试闭包test "hello"对应的 inflight 处理器 |
inflight.Bar-1.cjs | CommonJS | Bar类的 inflight 客户端 |
inflight.Foo-1.cjs | CommonJS | Foo类的 inflight 客户端 |
main.tf.json | JSON | 面向 AWS 的 Terraform 资源配置(DynamoDB) |
preflight.cjs | CommonJS | preflight 阶段生成的构造代码与 lift 元数据 |
这些文件的采集逻辑可以在 generated_test_targets.ts 中确认:编译完成后,hangar 遍历目标目录target/<name>.tfaws/.wing,按["preflight.", "inflight.", "extern/", "proc", ".Struct.js"]的前缀规则筛选产物,排除.mapsourcemap,最终由 createMarkdownSnapshot 将文件内容拼装为 Markdown 快照,并用 vitest 的toMatchFileSnapshot进行比对。
2.1 为什么会有三个 inflight 文件
Wing 编译器会把源码中每个“会在 inflight 阶段运行”的类或闭包单独序列化为一个.cjs模块。本例中:
inflight.Foo-1.cjs:Foo的method()与$inflight_init();inflight.Bar-1.cjs:Bar的callFoo();inflight.$Closure1-1.cjs:test "hello"闭包的handle()。
三个模块相互独立、通过 require 与参数注入建立联系,这正体现了 Wing“preflight 与 inflight 代码分离、依赖显式传递”的编译模型。
三、逐文件解读编译产物
3.1inflight.$Closure1-1.cjs:测试闭包的 inflight 实现
module.exports = function({ $bar, $bar_foo, $initCount }) { class $Closure1 { async handle() { (await $bar.callFoo()); (await $bar_foo.method()); $helpers.assert($helpers.eq((await $initCount.peek()), 2), "initCount.peek() == /*1*/ 2"); } } return $Closure1; }这个模块的入口参数{ $bar, $bar_foo, $initCount }是编译器的lift 清单:闭包在 inflight 阶段需要三个 preflight 对象——bar(调用callFoo)、bar.foo(直接调用method)以及initCount(调用peek)。三个$前缀变量在 inflight 运行时由宿主注入,所有 inflight 方法调用都显式await,保证异步语义。
3.2inflight.Bar-1.cjs与inflight.Foo-1.cjs:类的 inflight 客户端
Bar的客户端把字段foo保存为this.$this_foo:
module.exports = function({ }) { class Bar { constructor($args) { const { $this_foo } = $args; this.$this_foo = $this_foo; } async callFoo() { (await this.$this_foo.method()); } } return Bar; }Foo的客户端则体现了inflight new()的语义——被转译为隐式初始化方法$inflight_init():
module.exports = function({ $initCount }) { class Foo { async method() { } async $inflight_init() { (await $initCount.inc()); } } return Foo; }这里出现了一个关键名字:$inflight_init。它对应 lifting.ts 中定义的常量:
这是编译器在
_liftMap矩阵中使用的特殊操作名,表示某些传递依赖无论宿主请求什么操作都必然需要;对用户而言该操作是隐藏的,不会作为op传给onLift/onLiftType。
换言之,inflight new()是 Wing 为 inflight 类提供的“实例初始化钩子”,编译器在 lift 分析中把它建模为一个隐式依赖操作。
3.3main.tf.json:cloud.Counter的 AWS 落地形态
{ "provider": { "aws": [ {} ] }, "resource": { "aws_dynamodb_table": { "Counter": { "//": { "metadata": { "path": "root/Default/Default/Counter/Default", "uniqueId": "Counter" } }, "attribute": [ { "name": "id", "type": "S" } ], "billing_mode": "PAY_PER_REQUEST", "hash_key": "id", "name": "wing-counter-Counter-c824ef62" } } } }这份 Terraform JSON 是tf-aws 平台对cloud.Counter的具体映射结果。从源码看:
- cloud/counter.ts 定义 Counter 为“分布式原子计数器”(distributed atomic counter),支持
inc/dec/peek/set四个 inflight 操作,构造参数initial默认0; - shared-aws/counter.ts 声明哈希键
COUNTER_HASH_KEY = "id",因此在 Terraform 中hash_key为id、attribute为字符串类型的id; - 表名
wing-counter-Counter-c824ef62由构造路径(root/Default/Default/Counter/Default)与唯一 ID 组合而成,避免多栈冲突; billing_mode: "PAY_PER_REQUEST"表示按量计费,无需预置读写容量。
同时,shared-aws/counter.ts 的onLift实现会在 inflight 宿主上追加对应 DynamoDB 的 IAM 策略语句(通过calculateCounterPermissions按操作计算权限),并以环境变量DYNAMODB_TABLE_NAME_<addr>将表名注入宿主。也就是说,一个cloud.Counter在 AWS 上 = 一张 DynamoDB 表 + 一套按需生成的 IAM 权限 + 一个环境变量注入。
需要说明:快照中的
main.tf.json经过了 sanitize_json_paths 的清洗——绝对路径、source_hash、terraform 块与 cdktf 版本信息会被规范化或删除,以保证快照跨机器、跨平台稳定可比。
3.4preflight.cjs:lift 元数据的真正来源
preflight 代码是理解整个机制的核心,它定义了Foo、Bar与$Closure1三个 preflight 类,并暴露了两类 lift 元数据:
(1)_toInflightType():inflight 客户端实例化模板
class Bar extends $stdlib.std.Resource { static _toInflightType() { return ` require("${$helpers.normalPath(__dirname)}/inflight.Bar-1.cjs")({ }) `; } }Foo的模板中则注入了$initCount: ${$stdlib.core.liftObject(initCount)},$Closure1的模板同时注入了$bar、$bar_foo、$initCount三个对象——这与 3.1 节 inflight 闭包模块的入参一一对应。
(2)_liftMap:操作级依赖矩阵
以$Closure1为例:
get _liftMap() { return ({ "handle": [ [bar, ["callFoo"]], [bar.foo, ["method"]], [initCount, ["peek"]], ], "$inflight_init": [ [bar, []], [bar.foo, []], [initCount, []], ], }); }- 操作
handle(即test "hello"的执行体)依赖:bar的callFoo、bar.foo的method、initCount的peek; - 隐式操作
$inflight_init空依赖,但会触发闭包对三个对象的实例化准备。
Bar的_liftMap则展示了依赖的传递性:
get _liftMap() { return ({ "callFoo": [ [this.foo, ["method"]], ], "$inflight_init": [ [this.foo, []], ], }); }callFoo自身没有额外捕获,但会把请求转发给成员this.foo的method——这正是“双重引用”被正确展开的桥梁。
(3)_liftedState():字段级状态提升
_liftedState() { return { ...(super._liftedState?.() ?? {}), $this_foo: $stdlib.core.liftObject(this.foo), }; }Bar在 inflight 阶段需要的字段foo通过_liftedState序列化为$this_foo,最终在inflight.Bar-1.cjs的构造函数中被还原为this.$this_foo。
四、底层原理:collectLifts 与对象引用去重
快照中$Closure1的_liftMap同时列出[bar, ["callFoo"]]与[bar.foo, ["method"]],看起来是两条独立依赖。但 SDK 的 lift 引擎并不会“照单全收”地重复处理,而是通过 lifting.ts 中的collectLifts()与parseMatrix()做对象身份去重。
核心机制:
collectLifts(initialObj, initialOps)维护一个“已探索集合”(explored)与一个“待探索队列”(queue),以Map<对象, Set<操作>>的形式跟踪每个对象已被请求的操作集合;- 对每个对象,若已有同类操作被探索过,则跳过(lifting.ts);
parseMatrix()把编译器产出的[obj, [ops]]数组矩阵转换为Map<obj, Set<ops>>,并明确注释:编译器可能不知道 preflight 阶段obj1与obj2实际是同一对象,去重会把[obj1, ["op1"]] + [obj2, ["op2"]]合并为Map(obj1 → Set["op1", "op2"])(lifting.ts);- 遍历每个操作(连同隐式的
$inflight_init)时,把矩阵中的依赖对象继续入队,最终形成闭包式的传递依赖闭包; - 最后
Lifting.lift()对收集到的每个(对象, 操作集)调用onLift/onLiftType,保证每个对象的onLift只被调用一次,从而避免重复授权、重复初始化。
这正是 double_reference 用例想要守护的行为:虽然源码对bar.foo有两条引用路径(经bar.callFoo()间接引用、经bar.foo.method()直接引用),但去重机制保证Foo的初始化(initCount.inc())只执行一次,最终peek() == 2(一次new Foo()+ 一次断言前的显式计数)。这份编译快照之所以被固化,就是为了在后续编译器演进中防止回归——一旦 lift 分析出错,快照比对立即失败。
五、验证闭环:从编译快照到 sim 运行
hangar 为每个合法用例同时生成两类测试(见 generate_tests.ts):
test("wing compile -t tf-aws", async () => { await compileTest(sourceDir, filename, ...); }); test("wing test -t sim", async () => { await testTest(sourceDir, filename, ...); });- 编译测试:以
-t tf-aws运行wing compile,采集产物生成 Markdown 快照,即本文解析的文档; - 运行测试:以
-t sim运行wing test,采集 stdout/stderr 生成运行快照。
double_reference的 sim 运行快照位于 double_reference.test.w_test_sim.md,内容为:
pass ─ double_reference.test.wsim » root/Default/test:hello Tests 1 passed (1) Snapshots 1 skipped Test Files 1 passed (1) Duration <DURATION>pass表明测试闭包root/Default/test:hello在模拟器(sim)中真实执行并通过了initCount.peek() == 2的断言,与 tf-aws 编译快照形成“能编译 + 能运行”的双重验证。注意运行快照中的Duration <DURATION>是 sanitizeOutput 对不稳定输出(时长、端口、时间戳、绝对路径等)的归一化结果,保证快照内容可重复比对。
补充:
testTest只对入口文件(以.main.w/.test.w/main.w结尾)生成运行快照(generated_test_targets.ts),double_reference.test.w恰好满足.test.w后缀条件。
六、实操:如何查看与复现这份快照
如果你希望在本仓库中复现或扩展验证,可以按以下步骤操作(仓库只读,仅说明查看与运行方式,不做任何修改):
查看源码与快照
- 用例源码:tests/valid/double_reference.test.w
- tf-aws 编译快照:tools/hangar/snapshots/test_corpus/valid/double_reference.test.w_compile_tf-aws.md
- sim 运行快照:tools/hangar/snapshots/test_corpus/valid/double_reference.test.w_test_sim.md
理解生成链路
- 测试文件由 generate_tests.ts 从
tests/valid自动生成; - 快照由 generated_test_targets.ts 的
compileTest/testTest调用wingCLI 后采集,再由 utils.ts 的createMarkdownSnapshot写入__snapshots__/test_corpus/目录; - 路径约定见 paths.ts(
snapshotDir = tools/hangar/__snapshots__)。
- 测试文件由 generate_tests.ts 从
本地运行测试(需要先构建仓库并安装依赖)
# 在 tools/hangar 目录下执行 vitest,仅运行 double_reference 相关用例 pnpm vitest run --testNamePattern "double_reference"若编译器行为发生变化导致产物不一致,快照比对会直接失败并输出 diff,这正是 hangar 把编译产物“固化为文档”的意义。
七、总结
从 double_reference.test.w_compile_tf-aws.md 这一份快照文档,可以串联起 Wing 编译器与 SDK 的完整能力链条:
- 语言层:preflight 类(
Foo、Bar)与 inflight 闭包(test "hello")如何被拆分序列化; - lift 层:
_liftMap操作级依赖矩阵 +collectLifts/parseMatrix的对象引用去重,保证双重引用不产生重复初始化(对应 issue #3244); - 平台层:
cloud.Counter在 tf-aws 平台上被具体化为 DynamoDB 表、IAM 权限与环境变量注入; - 测试工程层:hangar 通过编译快照 + 运行快照的双通道,把这一切固化为可持续回归验证的基线。
快照文档看似只是“生成的代码陈列”,实则是 Wing 语言“同一份代码,preflight 编排、inflight 执行、平台差异化落地”这一核心设计的最直接、最可验证的证据。
- 编程语言
- 编译器
- 云原生
- 语言运行时
【免费下载链接】wing
A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡
相关推荐
Wing 语言 inflight class 定义与编译产物剖析:从测试用例到 tf-aws 快照
Wing 语言 inflight class 定义与编译产物剖析:从测试用例到 tf aws 快照 本文以 Wing 仓库中 inflight_class_de
编程语言编译器云原生语言运行时Wing 容器 Entrypoint 快照测试解析:从 sim.Container 参数到 tf-aws 编译产物
Wing 容器 Entrypoint 快照测试解析:从 sim.Container 参数到 tf aws 编译产物 导读 本文围绕 Wing 仓库中 hanga
编程语言编译器云原生语言运行时Wing 语言 math.atan 测试全解析:从 tf-aws 编译快照到 SDK 实现
Wing 语言 math.atan 测试全解析:从 tf aws 编译快照到 SDK 实现 本篇围绕 Wing 仓库中 atan.test.w 的 Hangar
编程语言编译器云原生语言运行时
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考