为什么 JS 源映射通常处于令牌粒度?
Why are JS sourcemaps typically at token granularity?
JavaScripts source maps 似乎通常不比 token 粒度更细。
例如,identity-map uses token granularity.
我知道我见过其他例子,但不记得在哪里了。
为什么我们不使用基于 AST 节点的粒度来代替?也就是说,如果我们的源映射具有所有 AST 节点的位置并且只有 AST 节点的开始,那么缺点是什么?
据我了解,源映射用于崩溃堆栈解码和调试:永远不会有错误位置或有用的断点不在某个 AST 节点的开头,对吧?
更新 1
一些进一步的说明:
问题涉及 AST 已知的情况。所以"it's more expensive to generate an AST than an array of tokens"不会回答这个问题。
这个问题的实际影响是,如果我们可以降低源映射的粒度,同时保留调试器和崩溃堆栈解码器的行为,那么源映射可能会小得多。主要优点是调试器的性能:开发工具可能需要很长时间来处理大型源文件,使调试变得很痛苦。
以下是使用 source-map 库在令牌级别添加源映射位置的示例:
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%!
Q2:为什么期望 AST-Node-granularity source maps 更小?
A2:见上文A1
Q3:您会使用什么格式来引用 AST 节点?
A3:无格式。请参阅上面 更新 1 中的示例代码。我说的是为 AST 节点的开始添加源映射位置。该过程几乎与为令牌开始添加源地图位置的过程完全相同,只是您添加的位置更少。
问题 4:您如何断言处理源映射的所有工具都使用相同的 AST 表示?
A4:假设我们控制整个管道并且在所有地方都使用相同的解析器。
可以使用 AST 粒度,但通常要构建 AST,无论如何都需要先标记代码。出于调试目的,AST 是不必要的步骤,因为语法分析器必须输入标记化数据才能工作。
一个有趣的资源on topic
我建议也探索一下 acornJS sourcecode and take a look how it produces AST
TypeScript
编译器实际上只在 AST 节点边界上发出 sourcemap 位置,除了一些例外,以提高与期望映射到特定位置的某些工具的兼容性,因此基于标记的映射实际上不是很通用。在您给出的示例中,TS 的源地图适用于这样的位置:
const a = function *() { return a + ++ b } /*
^ ^^ ^ ^ ^^ ^ ^^^
*/
通常是每个标识符 AST 节点的开始 和 结束(否则加上开始)。
映射标识符 AST 节点的开始 和 结束位置的基本原理非常简单 - 当您重命名标识符时,您希望重命名标识符的选择范围是能够映射回原始标识符,而不必依赖启发式方法。
JavaScripts source maps 似乎通常不比 token 粒度更细。 例如,identity-map uses token granularity.
我知道我见过其他例子,但不记得在哪里了。
为什么我们不使用基于 AST 节点的粒度来代替?也就是说,如果我们的源映射具有所有 AST 节点的位置并且只有 AST 节点的开始,那么缺点是什么?
据我了解,源映射用于崩溃堆栈解码和调试:永远不会有错误位置或有用的断点不在某个 AST 节点的开头,对吧?
更新 1
一些进一步的说明:
问题涉及 AST 已知的情况。所以"it's more expensive to generate an AST than an array of tokens"不会回答这个问题。
这个问题的实际影响是,如果我们可以降低源映射的粒度,同时保留调试器和崩溃堆栈解码器的行为,那么源映射可能会小得多。主要优点是调试器的性能:开发工具可能需要很长时间来处理大型源文件,使调试变得很痛苦。
以下是使用 source-map 库在令牌级别添加源映射位置的示例:
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%!
Q2:为什么期望 AST-Node-granularity source maps 更小?
A2:见上文A1
Q3:您会使用什么格式来引用 AST 节点?
A3:无格式。请参阅上面 更新 1 中的示例代码。我说的是为 AST 节点的开始添加源映射位置。该过程几乎与为令牌开始添加源地图位置的过程完全相同,只是您添加的位置更少。
问题 4:您如何断言处理源映射的所有工具都使用相同的 AST 表示?
A4:假设我们控制整个管道并且在所有地方都使用相同的解析器。
可以使用 AST 粒度,但通常要构建 AST,无论如何都需要先标记代码。出于调试目的,AST 是不必要的步骤,因为语法分析器必须输入标记化数据才能工作。
一个有趣的资源on topic
我建议也探索一下 acornJS sourcecode and take a look how it produces AST
TypeScript
编译器实际上只在 AST 节点边界上发出 sourcemap 位置,除了一些例外,以提高与期望映射到特定位置的某些工具的兼容性,因此基于标记的映射实际上不是很通用。在您给出的示例中,TS 的源地图适用于这样的位置:
const a = function *() { return a + ++ b } /*
^ ^^ ^ ^ ^^ ^ ^^^
*/
通常是每个标识符 AST 节点的开始 和 结束(否则加上开始)。
映射标识符 AST 节点的开始 和 结束位置的基本原理非常简单 - 当您重命名标识符时,您希望重命名标识符的选择范围是能够映射回原始标识符,而不必依赖启发式方法。