Wing 语言容器类型 Lift 捕获实战:从 capture_containers 测试到 Hangar 快照验证机制

原创2026-10-15 09:53:112,274 阅读
文章标签:编程语言编译器云原生语言运行时

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\"");
    }
  }
  ...
}

这段代码揭示了三个关键事实:

  1. 闭包工厂函数接收被捕获容器的引用($arr、$mySet、$myMap 等),这是 lift 在代码层面的体现——preflight 变量被“注入”进 inflight 闭包;
  2. Array.at、Map.has、Map.size、Json.get 等操作被内联展开为 $macros.__Array_at、__Map_has 等宏调用,这是 Wing 编译器将 SDK 中的 @macro 声明就地展开的结果(详见下文第四节);
  3. 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 宏定义与编译产物三者对照阅读,就能完整掌握容器捕获的底层机制。

登录后查看全文
wing