为什么 JS 源映射通常采用令牌粒度?



JavaScript 的源映射似乎通常不比令牌粒度更精细。 例如,标识映射使用令牌粒度。

我知道我见过其他例子,但不记得在哪里。

为什么我们不改用基于 AST 节点的粒度?也就是说,如果我们的源映射具有所有且仅启动 AST 节点的位置,那么缺点是什么?

在我的理解中,源映射用于崩溃堆栈解码和调试:永远不会有错误位置或有用的断点不在某个 AST 节点的开头,对吧?

更新 1

一些进一步的澄清:

  • 该问题涉及已经知道AST的情况。因此,"生成 AST 比生成代币数组更昂贵"不会回答这个问题。

  • 这个问题的实际影响是,如果我们可以在保留调试器和崩溃堆栈解码器的行为的同时降低源映射的粒度,那么源映射可能会小得多。主要优点是调试器的性能:开发工具可能需要很长时间来处理大型源文件,使调试变得很痛苦。

  • 下面是使用源映射库在令牌级别添加源映射位置的示例:

for (const token of tokens) {
generator.addMapping({
source: "source.js",
original: token.location(),
generated: generated.get(token).location(),
});
}

下面是在 AST 节点级别添加位置的示例:

for (const node of nodes) {
generator.addMapping({
source: "source.js",
original: node.location(),
generated: generated.get(node).location(),
});
}

更新 2

Q1:为什么预计 AST 节点的启动次数少于令牌的启动次数?

A1:因为如果 AST 节点的启动次数多于令牌的启动次数,则会出现一个从非令牌开始的 AST 节点。对于解析器的作者来说,这将是一项相当大的成就!为了具体化这一点,假设你有以下 JavaScript 语句:

const a = function *() { return a + ++ b }

以下是令牌开头的位置:

const a = function *() { return a + ++ b } /*
^     ^   ^        ^^^ ^ ^      ^ ^ ^  ^ ^
*/

以下是大多数解析器大致会说 AST 节点的开始位置。

const a = function *() { return a + ++ b } /*
^     ^   ^              ^      ^   ^  ^
*/

这减少了46%的源地图位置数量!


问题 2:为什么期望 AST 节点粒度源映射更小?

A2:见上文A1


Q3:您将使用什么格式来引用 AST 节点?

A3:无格式。请参阅上面的更新 1中的示例代码。我说的是为 AST 节点的启动添加源映射位置。该过程与为令牌开始添加源映射位置的过程几乎完全相同,只是您添加的位置更少。


问题 4:如何断言处理源映射的所有工具都使用相同的 AST 表示形式?

A4:假设我们控制整个管道,并在任何地方使用相同的解析器。


TypeScript

编译器实际上只在 AST 节点边界上发出源映射位置,但有一些例外,以提高与某些工具的兼容性,这些工具需要某些位置的映射,因此基于令牌的映射实际上并不是很通用。在您给出的示例中,TS 的源映射适用于这样的位置:

const a = function *() { return a + ++ b } /*
^     ^^  ^              ^      ^^  ^  ^^^
*/

这通常是每个标识符 AST 节点的开始结束(否则加上开始(。

映射标识符 AST 节点的开始结束位置的基本原理非常简单 - 重命名标识符时,您希望该重命名标识符上的选择范围能够映射回原始标识符,而不必依赖启发式方法。

可以使用 AST 粒度,但通常要构建 AST,无论如何都需要在标记代码之前。出于调试目的,AST 是一个不必要的步骤,因为语法分析器必须提供标记化数据才能工作。

关于主题的有趣资源

我还建议探索acornJS源代码,看看它是如何产生AST的

最新更新