Wing 语言容器类型 Lift 捕获实战:从 capture_containers 测试到 Hangar 快照验证机制
Wing 语言容器类型 Lift 捕获实战:从 capture_containers 测试到 Hangar 快照验证机制
导读
在 Wing 语言中,preflight 阶段(编译期)创建的容器数据(Array、Set、Map、Json 等)可以被 inflight 阶段(运行时)的闭包直接读取使用,这种跨阶段的数据传递称为 Lift 捕获。本文以仓库中的 capture_containers.test.w 测试及其 Hangar 快照文档(capture_containers.test.w_test_sim.md)为骨架,完整讲解容器类型捕获的语法、编译产物形态、SDK 宏实现原理,以及 Hangar 快照测试系统如何自动生成并校验这些结果。读完本文,你将掌握 Wing 中 inflight 闭包捕获各类容器的正确写法,并能读懂 Hangar 生成的编译快照与测试快照。
一、源测试:在 inflight 闭包中捕获四种容器
测试用例位于 tests/valid/capture_containers.test.w,全文仅 22 行,却覆盖了 Wing 中最常见的四种不可变容器类型在 inflight 阶段的读取操作:
bring cloud;
let arr = ["hello", "world"];
let mySet = Set<str>["my", "my", "set"];
let myMap = {"hello" => 123, "world" => 999};
let arrOfMap = [{"bang" => 123}];
let j = Json {a: "hello", b: "world"};
test "capture_containers" {
assert(arr.at(0) == "hello");
assert(arr.at(1) == "world");
assert(arr.length == 2);
assert(mySet.has("my"));
assert(mySet.size == 2);
assert(myMap.has("world"));
assert(myMap.size() == 2);
assert(arrOfMap.at(0).has("bang"));
assert(j.get("b") == "world");
}
1.1 顶层变量与隐式 inflight 上下文
- 五个容器均在文件顶层(preflight 作用域)声明。
test块在 Wing 中本身就是一段独立的 inflight 执行单元,其内部代码运行于运行时阶段,因此测试块对顶层变量的访问必然触发 lift 捕获机制。 bring cloud;用于引入云抽象库,虽然本测试未创建任何云资源,但这是所有使用test关键字程序的惯用引入(test由 SDK 的std.Test提供)。
1.2 各容器的关键语义
| 容器 | 字面量语法 | 测试断言 | 语义要点 |
|---|---|---|---|
Array<str> |
["hello", "world"] |
at(0)、at(1)、length |
不可变数组,at 越界会抛错,tryAt 则返回 nil |
Set<str> |
Set<str>["my", "my", "set"] |
has("my")、size == 2 |
集合自动去重,重复元素 "my" 只保留一份,故 size 为 2 |
Map<num> |
{"hello" => 123, ...} |
has("world")、size() |
映射表;注意 size 是方法调用(带括号),与 Array.length、Set.size 的属性访问不同 |
Array<Map> |
[{"bang" => 123}] |
at(0).has("bang") |
嵌套容器可链式调用,at(0) 返回内层 Map 后再调用 has |
Json |
Json {a: "hello", b: "world"} |
get("b") |
Json 类型通过 get 读取键值;该测试聚焦于 Json 作为被捕获值的场景 |
这里 Map 的 size() 是方法而非 getter,这一点在 SDK 源码中留有明确注释:"For now this has to be a method rather than a getter as macros only work on methods"(见 packages/@winglang/sdk/src/std/map.ts)。
二、快照文档结构解读:一行日志背后的完整验证链
2.1 sim 测试快照(本关联文档)
关联文档 capture_containers.test.w_test_sim.md 是 Hangar 对 wing test -t sim 运行结果的标准快照,其结构为:
# <a href="https://link.gitcode.com/i/4d67e5dfe119d3a15bc758e1db7e3cc8" target="_blank">capture_containers.test.w</a> | test | sim
## stdout.log
```log
pass ─ capture_containers.test.wsim » root/Default/test:capture_containers
Tests 1 passed (1)
Snapshots 1 skipped
Test Files 1 passed (1)
Duration <DURATION>
- **标题行**格式为 `[源文件名] | test | sim`,由快照生成器按“测试动作 + 目标平台”命名,`sim` 表示 Wing Simulator 平台;
- `root/Default/test:capture_containers` 是测试在应用树中的完整路径——`root` 为应用根,`Default` 是隐式默认作用域,`test:capture_containers` 是 `test` 块生成的 `std.Test` 节点 id(对应 preflight.cjs 中的 `new $stdlib.std.Test(...)`);
- `Duration <DURATION>` 中的占位符并非快照本身产生,而是 **Hangar 在写入快照前做了输出归一化**:将真实的耗时(如 `Duration 1m12.34s`)统一替换为 `<DURATION>`,从而保证快照在不同机器、不同负载下保持字节级稳定。
### 2.2 归一化逻辑的源码依据
归一化由 <a href="https://link.gitcode.com/i/22d54425a746d4bb9ca53f8ddb5b0022" target="_blank">tools/hangar/src/utils.ts</a> 的 `sanitizeOutput` 完成,它依次处理了:
- 换行符与 Windows 路径分隔符的归一化(`\r\n` → `\n`,`\` → `/`);
- Rust 源码行号的移除(`src/xxx.rs:LINE:COL`);
- 绝对堆栈路径、绝对路径的脱敏(`<ABSOLUTE>`);
- `.state` 随机状态文件、运行耗时(`Duration <DURATION>`)、动态端口、IP、时间戳的脱敏。
这些处理保证了同一测试在 CI、本地、不同操作系统上运行都能产出完全一致的快照文本,这正是快照对比测试(snapshot testing)可靠性的基础。
## 三、编译快照:容器 Lift 的真实产物形态
同源的编译快照 <a href="https://link.gitcode.com/i/c1b311b021796bc8c6ca99453df985eb" target="_blank">capture_containers.test.w_compile_tf-aws.md</a> 展示了 `wing compile -t tf-aws` 后生成的三类关键产物,是理解容器捕获底层原理的一手材料。
### 3.1 inflight 闭包代码(inflight.$Closure1-1.cjs)
```cjs
"use strict";
const $helpers = require("@winglang/sdk/lib/helpers");
const $macros = require("@winglang/sdk/lib/macros");
module.exports = function({ $arr, $arrOfMap, $j, $myMap, $mySet }) {
class $Closure1 {
...
async handle() {
$helpers.assert($helpers.eq($macros.__Array_at(false, $arr, 0), "hello"), "arr.at(0) == \"hello\"");
$helpers.assert($helpers.eq($macros.__Array_at(false, $arr, 1), "world"), "arr.at(1) == \"world\"");
$helpers.assert($helpers.eq($arr.length, 2), "arr.length == 2");
$helpers.assert((await $mySet.has("my")), "mySet.has(\"my\")");
$helpers.assert($helpers.eq($mySet.size, 2), "mySet.size == 2");
$helpers.assert($macros.__Map_has(false, $myMap, "world"), "myMap.has(\"world\")");
$helpers.assert($helpers.eq($macros.__Map_size(false, $myMap, ), 2), "myMap.size() == 2");
$helpers.assert($macros.__Map_has(false, $macros.__Array_at(false, $arrOfMap, 0), "bang"), "arrOfMap.at(0).has(\"bang\")");
$helpers.assert($helpers.eq($macros.__Json_get(false, $j, "b"), "world"), "j.get(\"b\") == \"world\"");
}
}
...
}
这段代码揭示了三个关键事实:
- 闭包工厂函数接收被捕获容器的引用(
$arr、$mySet、$myMap等),这是 lift 在代码层面的体现——preflight 变量被“注入”进 inflight 闭包; Array.at、Map.has、Map.size、Json.get等操作被内联展开为$macros.__Array_at、__Map_has等宏调用,这是 Wing 编译器将 SDK 中的@macro声明就地展开的结果(详见下文第四节);Set的has/size保持为await $mySet.has(...)的异步调用,说明 Set 在 inflight 侧经由 SDK 的 InflightClient 桥接,与纯本地内联的 Array/Map/Json 宏路径不同。
3.2 Lift 权限清单(preflight.cjs 中的 _liftMap)
get _liftMap() {
return ({
"handle": [
[arr, [].concat(["at"], ["length"])],
[arrOfMap, ["at"]],
[j, ["get"]],
[myMap, [].concat(["has"], ["size"])],
[mySet, [].concat(["has"], ["size"])],
],
"$inflight_init": [
[arr, []], [arrOfMap, []], [j, []], [myMap, []], [mySet, []],
],
});
}
_liftMap 是 Wing 编译器为每个 inflight 闭包生成的权限矩阵:它精确记录了闭包对每个被捕获对象允许调用哪些方法/属性。这是 Wing 最小权限 lift 模型(explicit lift qualification)的实现基础——编译器只授予源代码中实际使用到的操作权限,例如对 arr 只授予 at 与 length,对 j 只授予 get。
3.3 preflight 构造与 Terraform 输出
preflight.cjs 中,五个顶层变量被降级为 JS 常量(const arr = ["hello", "world"] 等),随后通过 _toInflightType() 以 $stdlib.core.liftObject(arr) 的形式序列化注入闭包;测试节点通过 $ClassFactory.new("@winglang/sdk.std.Test", ..., "test:capture_containers", ...) 注册。
而 main.tf.json 中只包含空壳的 aws provider、没有任何云资源定义——这恰好印证了一个事实:本测试只做纯计算断言,不创建云资源,因此 tf-aws 平台的 Terraform 产物为空,所有行为都发生在 sim 平台的内存运行时中。
四、SDK 宏实现:@macro 如何定义容器操作
$macros.__Array_at、__Map_has、__Json_get 这些名字来自 packages/@winglang/sdk 中各容器类的 @macro 注解。Wing SDK 用“宏 + 抽象方法”的模式定义纯本地(pure-local)操作:方法体在 TS 中只是 throw new Error("Macro") 的占位,真正的实现写在 JSDoc 的 @macro 里,由编译器在展开时替换。
典型示例(packages/@winglang/sdk/src/std/array.ts):
/**
* Get the value at the given index
* @macro ((arr, index) => { if (index < 0 || index >= arr.length) throw new Error("Index out of bounds"); return arr[index]; })($self$, $args$)
* @param index index of the value to get
* @returns the value at the given index
*/
public at(index: number): T1 {
index;
throw new Error("Macro");
}
同理:
- map.ts 中
size()的宏是Object.keys($self$).length;has()的宏是($args$ in ($self$));get()的宏在键不存在时抛出Map does not contain key: "<key>"; - json.ts 中的
Json.get、Json.keys(Object.keys($args$))等同样以宏形式提供。
这正是编译快照中容器操作被内联为 $macros.__* 调用的根本原因:宏是 Wing 实现“类型方法在编译期内联”的机制,它让 Array/Map/Json 这类基础容器在 inflight 阶段无需额外的序列化协议即可高效访问。
五、Hangar 快照系统:从 .w 文件到快照的自动化流水线
本快照文档是 Hangar(Wing 工具链端到端测试套件,见 tools/hangar/README.md)自动生成的。理解它的产生过程,就能读懂快照中的每一个字段。
5.1 测试用例自动生成
tools/hangar/src/generate_tests.ts 会递归扫描 tests/valid(以及 tests/sdk_tests、tests/doc_examples/valid、tests/valid/parameters)下所有 .w 文件,为每个文件生成一个 .test.ts,其中固定包含两个 vitest 用例:
test("wing compile -t tf-aws", async () => {
await compileTest(sourceDir, filename, env, includeJavaScriptInSnapshots, skipMarkdownSnapshot, args);
});
test("wing test -t sim", async () => {
await testTest(sourceDir, filename, env, skipMarkdownSnapshot);
});
即每个合法 .w 文件都要经过“tf-aws 编译”与“sim 运行测试”双重验证。这正是同源测试同时存在 _compile_tf-aws.md 与 _test_sim.md 两份快照的原因。
5.2 快照落盘流程
- 编译快照(generated_test_targets.ts 的
compileTest):执行wing compile -t tf-aws后,收集target/<file>.tfaws/main.tf.json(经sanitize_json_paths去除本地绝对路径、资产路径与 cdktf 版本号)以及.wing目录下的preflight.*、inflight.*、extern/、proc、*.Struct.js等文件内容; - 运行快照(
testTest,同上 L86-L122):执行wing test -t sim,捕获stdout.log/stderr.log,经sanitizeOutput归一化后写入; - 两者最终都由 utils.ts 的
createMarkdownSnapshot落盘到tools/hangar/__snapshots__/test_corpus/<目录>/<文件名>_<动作>_<平台>.md,并以expect(md).toMatchFileSnapshot(snapPath)与既有快照比对。
5.3 实际执行的 CLI 命令
从 utils.ts 的 runWingCommand 可以看到,快照背后真实执行的命令形态是:
wing --no-update-check --no-analytics test <源文件.w> -t sim
wing --no-update-check --no-analytics compile <源文件.w> -t tf-aws
其中 -t 指定目标平台,sim 测试还会设置 WING_HIDE_CONTAINER_LOGS=1 隐藏容器日志(generated_test_targets.ts)。你也可以在自己的环境中手动运行 wing test -t sim tests/valid/capture_containers.test.w 复现本快照的 stdout 输出。
六、拓展:容器捕获的更多形态
6.1 可变容器与复制
tests/valid/capture_mutables.test.w 展示了 MutArray、MutSet、MutMap 三种可变容器同样可以被 inflight 捕获,并且不可变容器可通过 copyMut() 转为可变副本后再捕获:
let a = MutArray<str>["hello"];
let s = MutSet<num>[12];
let m = MutMap<bool>{"hello" => true};
let aCloned = (Array<str>["hello"]).copyMut();
let handler = inflight () => {
assert(a.length == 1);
assert(s.size == 1);
assert(m.size() == 1);
assert(aCloned.length == 1);
};
test "main" {
handler();
}
6.2 基本类型捕获
tests/valid/capture_primitives.test.w 则验证了 str、num、bool、duration 等基本类型在 inflight 中的只读捕获,与容器捕获共同构成 Wing lift 语义的完整图景。
6.3 嵌套与云资源捕获
tests/valid/captures.test.w 展示了 cloud.Bucket、cloud.Queue 等云资源在 inflight 闭包中的方法级捕获(如 bucket1.put(...)、bucket1.list()),并支持通过 as "PublicBucket" 命名节点、以 env: 传环境变量等进阶用法。容器捕获与资源捕获遵循同一套 _liftMap 权限模型,只是容器的纯本地操作走宏内联、云资源走 SDK 客户端桥接。
七、小结
capture_containers.test.w 及其两份 Hangar 快照,是理解 Wing 语言“统一 preflight/inflight 编程模型”的一个高密度切片:
- 语言层:
test块天然是 inflight 上下文,顶层容器变量被自动 lift,Array/Set/Map/Json 的读取操作在 inflight 中可直接使用; - 编译层:lift 由
_liftMap按最小权限精确记录,容器本地操作经 SDK@macro内联为$macros.__*调用,云无关计算不会产生 Terraform 资源; - 验证层:Hangar 将每个
.w文件自动跑compile -t tf-aws与test -t sim,经路径/耗时/端口归一化后生成稳定的 Markdown 快照(如本关联文档所示),任何行为回归都会在快照 diff 中暴露。
对读者而言,这份快照既是测试通过的凭证,也是编译器行为的“示波器”——把 Wing 源码、SDK 宏定义与编译产物三者对照阅读,就能完整掌握容器捕获的底层机制。