Node.js

[Node] 随遇而安 TypeScript(二):符号表

2020-02-07  本文已影响0人  何幻

1. 回顾

上一篇我们探索了 TypeScript 的符号查找过程,在判断一个符号 x 是否已定义时,
TypeScript 会沿着 ast 当前节点,通过 parent 属性往上找,
途中某些祖先节点,可能具有 locals 值,它保存了当前作用域中定义的所有变量。

如此这样,我们可以找到所有局部定义的词法变量。
全局变量也在 locals 中,位于 ast 的根节点中。
只不过,除了用户定义的全局变量之外,还包含 TypeScript 语言内置的一些全局变量。

然而,上一篇篇尾也提到,
通过 ts.createProgram 创建的 program,是没有 locals 属性的,
各 ast 节点也没有 parent 属性相连,必须执行一次,program.getGlobalDiagnostics();

program.getGlobalDiagnostics 到底做了哪些工作呢?
本文我们就探索一下其中的秘密。

2. 直击本质

在开始之前,请先按 github: debug-typescript 准备好调试环境。

为了简单起见,我们写入待编译的 debug/index.ts 的内容如下,

let _x;

然后在 src/compiler/binder.ts#L454 declareSymbol 函数中,

function declareSymbol(...) {
  ...
  if (name === undefined) {
    ...
  }
  else {
    ...
    if (!symbol) {
      symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name));
      ...
    }
    ...
  }
  ...
}

打一个条件断点,

name === '_x'

启动调试,


symbolTable.set(name, symbol = createSymbol(SymbolFlags.None, name));

这一步就是在符号表 symbolTable 中写入映射关系,key 为 name,值是一个 symbol

在当前这个例子中,symbolTable 就是 ast 根节点 SourceFilelocals 值。
我们来看看 declareSymbol 是怎么被调用的,symbolTable 是怎么传进来的,


这个 blockScopeContainer 正是 ast 的根节点 SourceFile

想必是 TypeScript 从根节点开始,往下搜索子节点,
遇到变量定义之后,就在表示变量生效范围的 container 上,添加一条符号映射关系。

let _x;

我们定义的是全局变量,
这个变量在整个 SourceFile 内部生效,或者说,作用域为 SourceFile
因此,会在 SourceFile.locals 中添加一条符号映射关系。

2. 链路

(1)全局变量

那么 SourceFile.locals 是什么时候创建的呢?
TypeScript 又是怎样向下搜索子节点的呢?

这得从调用链路说起。


我们仔细看左侧的调用栈,会发现 bind 函数重复出现了多次,它被间接的递归调用了。
bind 的作用就是处理当前节点,可是在处理的过程中,bindEachChild 又调用了 bind 自身。
这其实是一个 ast 的遍历过程。

bind 会遍历 ast 的所有节点,一旦发现变量定义,就在它的 container 中写入符号映射关系。
第一个 bind 调用了 bindContainer,正是在这里 TypeScript 为 container 添加了 locals 属性。

初始时它是一个空的符号表,createSymbolTablesrc/compiler/utilities.ts#L46

export function createSymbolTable(symbols?: readonly Symbol[]): SymbolTable {
  const result = createMap<Symbol>() as SymbolTable;
  if (symbols) {
    for (const symbol of symbols) {
      result.set(symbol.escapedName, symbol);
    }
  }
  return result;
}

我们示例中的 _x 是全局变量,因此写到了 SourceFile 中,
下面我们来看一下局部变量的 bind 链路。

(2)局部变量

修改 debug/index.ts 的内容如下,

function f() {
  function g() {
    let _x;
  }
}

保持 declareSymbol 函数中 src/compiler/binder.ts#L454 位置的条件断点不变,

name === '_x'

再次启动调试,调用链路明显边长了。


blockScopeContainer 不再是 SourceFile 节点了,而是 FunctionDeclaration 了。
节点范围如下,pos: 14end: 47

我们可以调用 blockScopeContainer.__debugGetText() 获取节点的内容,

或者,使用 slice(14, 47) 截取一下源代码,

> `function f() {
  function g() {
    let _x;
  }
}`.slice(14, 47)

=== "
  function g() {
    let _x;
  }"

这正是 _x 是词法作用域范围,它在函数 g 中可见,而在函数 f 中( g 外的地方)不可见。
所以,与 _x 相关的符号映射关系,放在了 glocals 中。

3. 后记

我们知道原始的 ast 节点,除了没有 locals 信息之外,也没有 parent 属性,
实际上 parent 属性正是在 bind 函数开头 src/compiler/binder.ts#L2235 中设置的。

function bind(node: Node | undefined): void {
  if (!node) {
    return;
  }
  node.parent = parent;
  ...
  ...
  if (...) {
    ...
    if (...) {
      bindChildren(node);
    }
    else {
      bindContainer(node, containerFlags);
    }
    ...
  }
  else if (...) {
    ...
  }
  ...
}
node.parent = parent;

其中,后面那个 parent 外层函数 createBinder 中的变量,这个函数有 3053 行,
src/compiler/binder.ts#L183-L3235

参考

github: debug-typescript
TypeScipt v3.7.3

上一篇 下一篇

猜你喜欢

热点阅读