将Go的Defer添加到TypeScript编译器中
我想看看将Go的defer语句添加到TypeScript编译器难度有多大,但等我完成时,我确信它可能不应该存在。在Go中,defer语句会延迟函数的执行,直到外部函数结束。它最常用于将资源获取和清理放在一起,比如获取一个信号量: ```go func withSemaphore(ctx context.Context, sem *semaphore.Weighted) error { if err := sem.Acquire(ctx, 1); err != nil { return err } defer sem.Release(1) // ... 受保护的工作 return nil } ``` TypeScript并没有一个严格等价的defer。您可能会使用try/finally,如下所示: ```typescript async function readFile(path: string) { await sema.acquire(); try { // ... 使用资源 } finally { sema.release(); } } ``` 但这有点丑。为了好玩,我们可以在TypeScript编译器中黑客般地添加一个defer语句,并获得Go一样的语义。由于defer无法映射到现有的JavaScript特性,我们需要输出能够在运行时工作与Go相同的JavaScript代码。所以目标是能够像这样编写TypeScript代码: ```typescript async function readFile(path: string) { await sema.acquire(); defer sema.release(); // 新! // ... 使用资源 } ``` ### TypeScript编译器 TypeScript编译器(tsc)主要是一个静态分析引擎。其复杂性在于检查一种根本动态语言的类型,并支持极度增量式的编译,以满足IDE中的延迟预期。幸运的是,我们在添加defer语句时不必过于担心类型或其他分析。tsc已经拥有“识别语法X,将其替换为等效的语法Y”的机制。例如,编译ES5时: ```typescript class Foo { x = 1; } ``` 可能会变成类似: ```javascript function Foo() { this.x = 1; } ``` 从概念上讲,添加defer意味着进行另一个树重写。tsc已经执行了许多AST到AST的转换(例如,可选链 ?. 变为条件表达式),因此我们不需要添加新的工具。虽然有一些复杂性需要深入研究,但从高层来看,我们将带有defer的AST: ```typescript function f() { defer cleanup(); work(); } ``` 转换为类似: ```javascript function f() { const __defers = []; try { __defers.push(() => cleanup()); work(); } finally { // 弹出并调用 } } ``` 首先,我们需要教tsc的解析器,defer是一个语句。有一个语法种类的列表,我们将DeferStatement添加到此列表中,并将其定义为接受一个单一的表达式操作数。我们需要执行一些检查,例如确保defer语句出现在函数体内,确保表达式是可调用的,以及确保tsc执行其常规递归检查: ```go func (c *Checker) checkDeferStatement(node *ast.Node) { c.checkGrammarStatementInAmbientContext(node) // defer与其包含的函数的生命周期相关 fn := ast.GetContainingFunction(node) if fn == nil || fn.Body() == nil || !ast.IsBlock(fn.Body()) { c.grammarErrorOnNode(node, diagnostics.Defer_statements_can_only_be_used_inside_function_bodies) } else if ast.GetFunctionFlags(fn) & ast.FunctionFlagsGenerator != 0 { c.grammarErrorOnNode(node, diagnostics.Defer_statements_cannot_be_used_in_generators) } // 仅支持调用,这保持捕获/降低不明确 expression := ast.SkipParentheses(node.Expression()) if !ast.IsCallExpression(expression) { c.grammarErrorOnNode(node.Expression(), diagnostics.The_operand_of_a_defer_statement_must_be_a_call_expression) c.checkExpression(node.Expression()) return } // 重用正常的调用检查(可调用的callee、参数类型等) c.checkExpression(expression) } ``` 实际的转换代码相当冗长,所以我在这里不复制,而是深入探讨我所做的设计决策,告诉您更多关于转换如何工作的内容。 ### 我认为defer应该如何工作 为了匹配Go的行为,callee、接收者和参数值会立即捕获: ```javascript let x = 1; defer console.log(x); x = 2; ``` 它必须打印1。我们需要生存的一个极端情况是可调用方法被重新定义,如下所示: ```javascript const logger = { log(message: string) { console.log("old:", message); }, }; defer logger.log("hello"); // defer之后一切都变了 logger.log = (message) => { console.log("new:", message); }; ``` 即使logger.log稍后被重新赋值,延迟调用仍然会调用原始方法。这与Go的语义匹配,其中函数值、接收者和参数都在执行到达defer语句时被评估。任何包含至少一个defer的函数都会获得一个小堆栈,每个达到的defer语句都会将一个闭包推入堆栈。当函数退出时,堆栈以逆序(后进先出)排出。注册在执行到达defer时发生,而不是在...
本站免费、广告极少。如果觉得有帮助,可以请我们喝杯咖啡 —— 任何金额都对持续运营有实际帮助。
☕请我喝杯咖啡