- 编程语言
- 编译器
- 云原生
- 语言运行时
【免费下载链接】wing
A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡
本文以 Wing 仓库中的 RFC《Explicit lift qualification》(docs/contributing/999-rfcs/2024-03-14-explicit-lift-qualification.md)为骨架,结合编译器源码与测试用例展开。你将理解 Wing 中"lift(提升)"与"lift qualification(提升限定)"的概念、自动限定在哪些场景会失败,以及
lift { ... } { ... }块语法如何显式限定 preflight 对象、关闭自动权限推导,并看到该提案在编译器中的完整实现路径与可运行测试。
背景:Wing 中的 Lift 与 Lift Qualification
在 Wing 语言(winglang)中,preflight(预飞行)代码负责定义基础设施,inflight(飞行中)代码负责运行时逻辑。为了让 inflight 代码能访问 preflight 对象,编译器引入了lift(提升)机制。RFC 将其拆分为两层含义:
- 捕获与客户端生成:把 preflight 对象捕获到 inflight 代码中,并为该对象生成一个 inflight 客户端。对原始类型(如
str、num)而言这仅仅是序列化;对 preflight 类而言,这个客户端就是该对象的 inflight API(如cloud.Bucket的put、get、list、delete等)。 - 宿主环境配置:配置承载该 inflight 闭包的主机(例如
cloud.Function),使其能够安全地使用上述客户端访问 preflight 对象——典型做法是为主机设置"最小权限"(least privilege permissions)。
RFC 明确指出,本文档讨论的是第二层:如何安全地配置宿主环境。
Lift qualification(提升限定)是编译过程中的一个阶段:编译器为每个被 lift 的对象标注其正在执行的操作。例如 inflight 代码bucket.put("key", "value")要求被 lift 的bucket对象在当前的 inflight 方法/闭包中限定为put操作。通过限定,编译器建立起一张映射表:记录所有 preflight 对象、以及它们在某个 inflight 方法/闭包中分别被如何使用。这张映射表随后被用来配置运行该 inflight 代码的主机,使其能够以最小权限安全访问这些 preflight 对象。
在 RFC 提交时,lift qualification 由编译器全自动完成:对所有 inflight 代码中出现的 preflight 表达式,编译器自动推导限定。自动化的局限在于——当编译器遇到一个类型为 preflight 类型、但具体是哪个 preflight 对象无法静态确定的 inflight 表达式时,它就无法完成限定,因为不知道这个表达式到底会引用哪些 preflight 对象,最终产生编译错误。RFC 提到,未来希望引入数据流分析(例如可达定义分析,reaching definitions analysis)来消解这类错误,但在那之前需要一种显式的补救手段。
现有方案的痛点:四个典型场景
RFC 归纳了四个当前自动限定无法处理、因而需要显式限定机制的典型场景(除最后一个外,当前都会产生编译错误):
场景 1:使用 inflight 表达式引用 preflight 对象。编译器无法静态判断 inflight 表达式b在运行时指向b1还是b2:
let b1 = new cloud.Bucket() as "b1"; let b2 = new cloud.Bucket() as "b2"; inflight () => { let var b = b1; if true { b = b2; } b.put("key", "value"); // 需要显式将 b1 和 b2 限定为 "put" }场景 2:在 inflight 代码中使用 preflight 对象集合。遍历集合时无法逐个限定每个对象:
for b in buckets { b.put("key", "value"); // 需要把整个 buckets 限定为 "put" }场景 3:业务逻辑能推导出更精确的限定。例如按索引奇偶性只访问部分 bucket,实际上只需要限定偶数索引的 bucket:
for i in buckets.len { if i % 2 == 0 { buckets.at(i).put("key", "value"); // 只需要把偶数 bucket 限定为 "put" } }场景 4:仅有方法名不足以生成最小权限。当 put 操作只针对特定 key 前缀时,显式限定应能表达更窄的授权范围:
for i in 0..5 { bucket.put("k{i}", "value"); }这里如果能把bucket显式限定为"put"操作并附带 key 模式"k*",将比编译器自动推导的权限更精确。而当前语法没有任何手段去关闭自动限定并定义这个更窄的限定。
需求与命名讨论
RFC 提出四个目标:
- 提供一种方式,显式限定被 inflight 方法/闭包使用的 preflight 对象;
- 在显式限定生效的范围内,关闭相应的编译错误;
- 在显式限定生效的范围内,关闭自动限定;
- 提供一个可扩展的框架,支持以编程方式定义和复用 lift qualifications。
关于命名,RFC 说明 "qualifications" 一词的含义是"限定对象被如何使用",并讨论了其他候选词:capabilities(能力,强调宿主需要哪些能力才能运行 inflight 代码)和operations(操作,强调 inflight 代码对 preflight 对象执行了哪些操作)。最终 RFC 采用 "qualifications",但表示欢迎讨论。
RFC 还回顾了先前的相关探索:winglang/wing 仓库的 issue #76("Mechanism for explicitly qualifying lifting")提出了该主题的讨论;PR #5935("feat(compiler): allow explicit lift qualifications of preflight objects")做过一次实现尝试,但存在四个问题:
- 语法看起来像 inflight 函数调用,容易引发误用;
- 没有限定"在哪个作用域内关闭编译错误";
- 没有提供关闭自动限定的机制;
- 没有机制(或清晰的路径)去编程式定义和复用 lift qualifications。
提案:lift 块语法
RFC 的核心提案是引入块级语句语法lift { 限定列表 } { 语句块 }。以场景 1 为例:
let b1 = new cloud.Bucket() as "b1"; let b2 = new cloud.Bucket() as "b2"; inflight () => { let var b = b1; if true { b = b2; } // 在下面的作用域中,关闭"未知 preflight 对象"导致的限定编译错误, // 同时也关闭自动限定。 // 我们显式将 b1 和 b2 限定为 "put",因为编译器无法替我们推导。 lift {b1: ["put"], b2: ["put"]} { b.put("key", "value"); } }内部表示与作用域语义
RFC 指出:lift块的参数在内部会创建一个std.LiftQualificationsContainer对象,并加入到该闭包的 lift qualifications 中。多个lift块只会为这个闭包创建更多的std.LiftQualificationsContainer;而实际的限定值(b1、"put")只是被追加到闭包/方法上,并不严格归属于某个块。
真正属于块的语义有两点:
- 在块内关闭限定错误:示例中的 inflight 表达式
b得以合法使用; - 在块内关闭自动限定:即使编译器技术上能够推导,也不会自动添加限定。
这一行为保证:用户可以创建自定义限定,而不必担心编译器用某种更宽泛的限定把它覆盖掉。这在场景 4(按 key 模式缩小权限)中尤为关键——块内的显式限定不会被自动限定覆盖。
块式语法的四个设计要点
- 粒度精细(granular):可以把限定作用域精确限定到需要的代码行,避免"远距离行为"(spooky action at a distance)。语法只对块内的代码行退出 Winglang 的自动权限生成——如果一个函数有几十行代码,不必把整个函数都退出自动限定系统。
- 意图清晰(clear):语法向阅读代码的人明确传达"这里发生了纯运行时/库代码无法实现的事情"。它不是函数调用,因此也不必遵循函数调用的规则。
- 可组合(compositional):当用户重构代码(例如把几行代码抽取为独立函数)时,可以把限定/能力块一起带走。因为限定紧邻相关代码,这种移动很自然。
- 嵌套无特殊效果:嵌套
lift块不会产生额外效果,只是把显式限定累加到方法上,并在最外层lift块关闭编译错误生成与自动限定。RFC 给出了等价示例:
// 下面两个 inflight 闭包在效果上是等价的: let nesting = inflight () => { let b = bucket; let q = queue; lift {bucket: ["get"]} { b.get("key"); lift {queue: ["push"]} { q.push("data"); } } } let no_nesting = inflight () => { let b = bucket; let q = queue; lift {bucket: ["get"], queue: ["push"]} { b.get("key"); q.push("data"); } }仓库中的实现落地
在本文档所在的仓库快照中,这一提案已经落地到编译器实现,并配有语法、正/反例测试与快照。下面给出各层级的实现证据。
词法与语法:tree-sitter 文法
lift语句是语言文法的一等公民,定义于 packages/@winglang/tree-sitter-wing/grammar.js:
lift_statement: ($) => seq( "lift", field("lift_qualifications", $.lift_qualifications), field("block", $.block) ), lift_qualifications: ($) => seq("{", field("qualification", commaSep1($.lift_qualification)), "}"), lift_qualification: ($) => seq( field("obj", $.expression), ":", choice( field("ops", $.identifier), field("ops", seq("[", commaSep1($.identifier), "]")) ) ),从文法可以看到两个重要细节:
obj是一个完整的expression,意味着限定对象不仅可以是标识符,也可以是任意表达式(测试中出现了maybe_bucket ?? bucket3这种可选链表达式);ops支持两种形式:单个方法名(lift {bucket3: put})或方法名数组(lift {bucket1: [delete, put, list]})。
AST:ExplicitLift 与 LiftQualification
语法树在 packages/@winglang/wingc/src/ast.rs 中表示为新的语句种类:
ExplicitLift(ExplicitLift), // StmtKind 的一个变体 pub struct ExplicitLift { pub qualifications: Vec<LiftQualification>, pub statements: Scope, } pub struct LiftQualification { pub obj: Expr, pub ops: Vec<Symbol>, }ExplicitLift携带两类信息:限定列表(每个限定 = 一个对象表达式 + 一组操作符号),以及块内的语句作用域statements。
解析:build_lift_statement
解析器在 packages/@winglang/wingc/src/parser.rs 中实现build_lift_statement。其中最关键的一条约束是:
// Lift statements are only legal in inflight if phase != Phase::Inflight { return self.with_error("Lift blocks are only allowed in inflight code blocks", statement_node); }也就是说,lift块只允许出现在 inflight 代码中——在 preflight 作用域使用会直接报错。随后解析器遍历lift_qualifications节点,为每个限定构建obj表达式和ops符号列表。
语义:LiftVisitor 中的处理
核心实现在 packages/@winglang/wingc/src/lifting.rs。LiftVisitor内部用计数器跟踪是否处于显式限定块中(lifting.rs#L26-L29):
// Used during visiting to track whether we're inside an explicit `lift` qualification block in_disable_lift_qual_err: usize,在visit_stmt中(lifting.rs#L445-L478),当遇到ExplicitLift语句时:
- 先
in_disable_lift_qual_err += 1,标记进入"忽略未知 preflight 对象"的状态; - 遍历
explicit_lift.qualifications,对每个限定对象调用jsify_expr生成 preflight 代码,并把操作方法列表格式化为["put"]这样的字符串,通过lifts.lift(...)注册到当前方法的限定中; - 继续访问块内语句;
- 离开时
in_disable_lift_qual_err -= 1,恢复自动限定与错误检查。
而"未知 preflight 对象"错误的判定在 lifting.rs#L240-L257:当 inflight 表达式求值结果为 preflight 对象类型、正在访问其属性、且不处于显式限定块内(v.in_disable_lift_qual_err == 0)时,编译器报告:
Expression of type "..." references an unknown preflight object, can't qualify its capabilities并附带提示:
Use a `lift` block to explicitly qualify the preflight object and disable this error注意:in_disable_lift_qual_err是一个计数器(usize)而非布尔值,这正对应 RFC 中"嵌套lift块无特殊效果、由最外层块统一关闭错误与自动限定"的语义——进出嵌套块时计数递增/递减,只要计数大于 0 就处于禁用状态。
数据结构:Lifts 与限定存储
编译器内部用 packages/@winglang/wingc/src/type_check/lifts.rs 中的Lifts结构收集限定结果:
/// All the lifts. Map from method to a map from preflight code to lift qualifications. pub lifts_qualifications: BTreeMap<String, BTreeMap<String, LiftQualification>>,其中LiftQualification的ops是IndexSet<String>——同一对象上的操作集合是去重的。lift()方法把"方法名 → preflight 代码 → 操作集合"逐层登记,同时还会为该类注册$inflight_init上的 lift,表示该类需要访问该 preflight 对象("this" 因已在作用域内而除外)。
编译产物:_liftMap 快照
编译到 tf-aws 平台的快照 tools/hangar/snapshots/test_corpus/valid/explicit_lift_qualification.test.w_compile_tf-aws.md 展示了显式限定最终进入类的_liftMap。例如Foo类的mehtod方法生成的限定映射:
get _liftMap() { return ({ "mehtod": [ [(maybe_bucket ?? bucket3), ["list"]], [bucket1, [].concat(["delete", "put", "list"], ["tryGet"])], [bucket2, [].concat(["put"], ["get"])], [bucket3, [].concat(["put"], ["get"])], [maybe_bucket, []], ], "$inflight_init": [ ... ], }); }可以看到:显式限定的操作(delete、put、list)与块外自动限定的操作(tryGet、get)通过concat合并在同一对象上,相互不覆盖——这与 RFC 中"限定值只是追加到闭包/方法,不严格属于块"的描述一致。
实战验证:正例与反例测试
正例:tests/valid/explicit_lift_qualification.test.w
仓库中的正式测试 tests/valid/explicit_lift_qualification.test.w 覆盖了 RFC 中提到的各种语法形态:
- 多对象、多方法限定:
lift { bucket1: [delete, put, list], bucket2: [put] },块内把bucket1赋给 inflight 变量b1后调用put/list/delete均不报错; - 单方法格式:
lift { bucket3: put }(不带方括号); - 表达式作为限定对象:
lift { maybe_bucket ?? bucket3: [list] }——用可选链表达式(unwrap-or)指定要 lift 的对象; - 闭包中的显式限定:
let inflight_closure = inflight () => { lift {bucket1: [put]} { ... } }; - preflight 接口类型的显式限定:
lift {bar: [method]},其中bar实现了带inflight method()的PreflightInterface。
对应的 sim 快照 tools/hangar/snapshots/test_corpus/valid/explicit_lift_qualification.test.w_test_sim.md 记录了三个测试(explicit method lift qualification、explicit closure lift qualification、explicit interface lift qualification)在模拟器中的运行结果,可作为端到端行为依据。
反例:tests/invalid/explicit_lift_qualification.test.w
反例测试 则从另一面锁定了语法边界,对应的错误快照位于 tools/hangar/snapshots/invalid.ts.snap:
- 在 preflight 中使用
lift:lift {bucket: [put]}{}出现在 preflight 方法或全局作用域 →Lift blocks are only allowed in inflight code blocks; - 限定 inflight 对象:
lift {b: [put]}{}(b是 inflight 变量)→ 错误; - 限定非类的 preflight 值:
lift {prelight_string: contains}{}→ 错误; - 限定未知成员:
lift {bucket: [shoot]}{}、lift {bucket: shoot}{}→ 错误; - 限定未知对象:
lift {not_bucket: put, not_bucket_again: get}{}→ 错误; - 限定 preflight 方法:
lift {bucket: addObject}{}(addObject是 preflight 方法)→ 错误; - 限定静态方法:
lift {preflight_class: static_method}{}→ 错误; - 限定生效范围之外:在
lift {bucket: put}{}块之外调用b2.put(...),即使此前已对bucket做过显式限定,仍会报错——这正验证了 RFC 中"错误禁用只作用于块内"的设计。
这些反例共同确认了 RFC 提出的关键边界:显式限定只针对 preflight 对象、只允许 inflight 操作(方法)名、只作用于lift块内部。
下一步展望:SDK API 与类型检查
编程式限定与复用:std.LiftQualification
RFC 规划了后续的 SDK 层 API:提供std.LiftQualification与std.LiftQualificationsContainer类,让用户以编程方式创建、组合、继承和复用限定对象,甚至可以表达"操作之外"的额外信息(例如 key 模式)。RFC 给出的示例:
// stdlib class cloud.BucketPutQual extends std.LiftQualification { this.keyPattern: str; new(b: cloud.Bucket) { super(b, ["put"]); this.keyPattern = "*"; } setKeyPattern(pattern: str) { this.keyPattern = pattern; } } // 创建一个限定容器 let quals = new std.LiftQualificationsContainer(); let b1 = new cloud.Bucket() as "b1"; let b2 = new cloud.Bucket() as "b1"; // 定义我们的显式限定:b1 在 put 时有特殊的 key 模式 let b1Quals = new cloud.BucketPutCap(b1); b1Quals.setKeyPattern("k*"); // b2 可以 put 任意 key let b2Quals = new std.LiftQualification(b2, ["put"]); // 把它们加入显式限定容器 quals.add(b1Quals); quals.add(b2Quals); let arr = [b1,b2]; // 为所有 bucket 定义 get 访问 let getBucketQuals = new std.LiftQualificationsContainer(); for b in arr { getQuals.add(new std.LiftQualification(b, ["get"])); } class A { inflight foo() { lift quals { // 使用 preflight 代码中编程式定义的限定对象 for b in arr { b.put("k99", "v"); // 这个 key 模式对两个 bucket 都应生效 } b2.put("hello", "world"); // b2 接受任意 key 模式 b1.put("k100", "v"); // 块内不产生自动限定,b1 的 key 模式限定不会被覆盖 } // 使用 getBucketQuals 为数组中的全部 bucket 提供 get 访问 lift getBucketQuals { for b in arr { b.get("k"); } } } inflight bar() { lift quals { // 在 A 的另一个方法中复用 `qual` let b = arr.at(randome(arr.len)); b.put("k0", "bar"); } } }这段示例体现了三个设计意图:组合(多个限定加入同一容器)、继承扩展(BucketPutQual在操作之外携带 key 模式)、复用(同一个quals容器被foo与bar两个方法引用)。lift quals { ... }与块内"不发生自动限定、限定不被覆盖"的行为,保证了编程式定制的权限在运行时不会被编译器推导的更宽泛权限冲掉。
类型检查限定方法名
RFC 还展望了限定方法名的类型检查:理想情况下lift {a: ["put"]}在a不存在名为put的 inflight 成员时不应编译通过。可能支持的语法是引用成员符号而非字符串:
let a = new A(); inflight () => { lift {a: [A.put]} // 校验 a 确实是 A 类型,且 A 实例存在 inflight put 方法 { } };需要注意:这一节属于 RFC 的"Next steps"规划,仓库当前实现的字符串形式(["put"])配合反例测试(未知成员、preflight 方法、static 方法均报错)已经提供了基本的方法名校验,但"成员符号引用 + 完整类型校验"的语法仍在设计中。
小结
显式 lift qualification 解决的是 Wing 编译器自动权限推导的"盲区":当 inflight 代码通过表达式、集合或动态逻辑间接引用 preflight 对象,或需要比自动推导更窄的权限时,lift { ... } { ... }块提供了一个既有清晰作用域、又关闭自动限定与相关编译错误的显式手段。在本文档所在的仓库快照中,该语法已完整落地:从 tree-sitter 文法、AST 节点、解析器 到 LiftVisitor 语义 与 Lifts 数据结构,并通过 正例 与 反例 测试锁定了行为边界。后续的 SDK 层std.LiftQualification/std.LiftQualificationsContainerAPI 与成员符号级类型检查,则在为"编程式定义、组合与复用限定"铺路。
- 编程语言
- 编译器
- 云原生
- 语言运行时
【免费下载链接】wing
A programming language for the cloud ☁️ A unified programming model, combining infrastructure and runtime code into one language ⚡
相关推荐
Wing 语言 TypeScript 指南:Inflight 闭包与资源提升(Lift)机制深入解析
Wing 语言 TypeScript 指南:Inflight 闭包与资源提升(Lift)机制深入解析 本文基于开源仓库 wing( README.md http
编程语言编译器云原生语言运行时tree-sitter-wing 解析器全解析:Wing 语言编译器背后的语法引擎
tree sitter wing 解析器全解析:Wing 语言编译器背后的语法引擎 Wing 是一门面向云原生的编程语言,它将基础设施定义与运行时业务代码统一到
编程语言编译器云原生语言运行时Wing 语言 while 循环实战:语法、break/continue 控制流与编译器实现
Wing 语言 while 循环实战:语法、break/continue 控制流与编译器实现 导读 Wing 是一种面向云的原生编程语言,将基础设施定义与运行时
编程语言编译器云原生语言运行时
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考