美文网首页Node.js
[Node] 淡如止水 TypeScript (三):词法分析

[Node] 淡如止水 TypeScript (三):词法分析

作者: 何幻 | 来源:发表于2020-01-01 12:02 被阅读0次

0. 回顾

上文我们介绍了 TypeScript 编译过程的宏观步骤:

(1)先从 lib/tsc 开始,处理命令行调用
(2)然后分两个大的步骤完成编译:源码解析、写文件
(3)源码解析过程,会创建 ProgramSourceFile 两个关键对象
(4)TypeScript 会为每个文件创建一个 SourceFile,通过调用 parser 来生成

下文我们来看一下,源码解析的过程到底是怎么完成的,SourceFile 到底是怎样生成的。
由于这一块内容会比较复杂,因此拆分成了两篇文章,
本文先介绍词法分析部分,下一篇介绍语法分析。

1. 词法分析器的状态:nextToken() & token()

TypeScript 的词法分析器,有两个方法用的特别多,nextToken()token()
词法分析器会在字符流中记住当前处理的位置,
token() 没有副作用,每次执行,都只会返回当前 token 的种类 SyntaxKind(对的)。

function token(): SyntaxKind {
  return currentToken;
}

nextToken() 的逻辑会比较长,包含了词法分析的所有细节,表示把词法分析器状态往后推移一个 token。

上一篇,代码执行到了 src/compiler/parser.ts 中,这是 parser 的入口,
createSourceFilesrc/compiler/parser.ts#L515

export function createSourceFile(...): SourceFile {
  ...
  if (languageVersion === ScriptTarget.JSON) {
    ...
  }
  else {
    result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind);
  }
  ...
  return result;
}

它会调用 Parser.parseSourceFilesrc/compiler/parser.ts#L692

export function parseSourceFile(...): SourceFile {
  ...
  const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes, scriptKind);
  ...
  return result;
}

接着执行 parseSourceFileWorkersrc/compiler/parser.ts#L843

function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {
  ...
  sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile);
  
  ...
  nextToken();
  
  ...
  sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);

  ...
  return sourceFile;

  ...
}

这里会调用 createSourceFile 创建 SourceFile 对象,但实际上只创建了 AST 的根节点。

接着调用的就是 nextToken() 了,词法分析器的状态会向后移,开始处理下一个 token,
parseList 是最顶层的解析函数,进行递归下降解析,sourceFile 就是完全解析后的结果。

2. 状态后移:nextToken()

2.1 扫描并积累字符

我们看 nextToken() 的执行过程,src/compiler/parser.ts#1098

function nextToken(): SyntaxKind {
  ...
  return nextTokenWithoutCheck();
}

调用了 nextTokenWithoutChecksrc/compiler/parser.ts#L1094

function nextTokenWithoutCheck() {
  return currentToken = scanner.scan();
}

这里调用了 scanner.scan(),获取 token 值,
scanner.scansrc/compiler/scanner.ts#L1490,这个函数比较长,有 443 行,
scanner 会在字符流中记住当前处理的位置 pos ,然后往后扫描 token。

function scan(): SyntaxKind {
  startPos = pos;
  ...
  while (true) {
    ...
    let ch = codePointAt(text, pos);

    ...
    switch (ch) {
      ...
      case CharacterCodes.plus:
      ...
      default:
        if (isIdentifierStart(ch, languageVersion)) {
          pos += charSize(ch);
          while (pos < end && isIdentifierPart(ch = codePointAt(text, pos), languageVersion)) pos += charSize(ch);
          tokenValue = text.substring(tokenPos, pos);
          ...
          return token = getIdentifierToken();
        }
        else if (isWhiteSpaceSingleLine(ch)) {
          ...
        }
        else if (isLineBreak(ch)) {
          ...
        }
        ...
    }
  }
}

以上 scan 函数,会从当前位置 pos 读取一个字符 ch
然后判断它的 CharacterCodes 类型,分别进行处理。

我们示例代码 debug/index.ts 中,第一字符是 c

const i: number = 1;

因此,scan 会跑到 switchdefault 分支。

if (isIdentifierStart(ch, languageVersion)) {
  pos += charSize(ch);
  while (pos < end && isIdentifierPart(ch = codePointAt(text, pos), languageVersion)) pos += charSize(ch);
  tokenValue = text.substring(tokenPos, pos);
  ...
  return token = getIdentifierToken();
}
else if (isWhiteSpaceSingleLine(ch)) {
  ...
}
else if (isLineBreak(ch)) {
  ...
}

这里的代码逻辑是,判断 ch 是否一个标识符的开始符号,这里 c 确实是这种情况,
然后开始往后积累字符,直到不构成标识符为止,这样就从字符流中读取出了一个完整的标识符了。

我们示例源代码中,c 开头的标识符是 const,因此,这里 tokenValue 就是 const 了。


2.2 返回 token 的种类,而不是 tokenValue

最后,scan 函数并没有返回 tokenValue,而是返回了一个 SyntaxKind 枚举,表示该 token 的种类。
这是在 getIdentifierToken 中完成的。

getIdentifierTokensrc/compiler/scanner.ts#L1414

function getIdentifierToken(): SyntaxKind.Identifier | KeywordSyntaxKind {
  // Reserved words are between 2 and 11 characters long and start with a lowercase letter
  const len = tokenValue.length;
  if (len >= 2 && len <= 11) {
    const ch = tokenValue.charCodeAt(0);
    if (ch >= CharacterCodes.a && ch <= CharacterCodes.z) {
      const keyword = textToKeyword.get(tokenValue);
      if (keyword !== undefined) {
        return token = keyword;
      }
    }
  }
  ...
}

这个函数里,区分了关键字 keyword 和普通的标识符。
在我们的例子中,const 是一个关键字,
因此,会根据 textToKeyword.get(tokenValue),获得 const 关键字对应的 SyntaxKind

映射关系位于 textToKeywordObj 里,src/compiler/scanner.ts#L66

const textToKeywordObj: MapLike<KeywordSyntaxKind> = {
  ...
  const: SyntaxKind.ConstKeyword,
  ...
};

这个枚举值 SyntaxKind.ConstKeyword80

这就是 parseSourceFileWorkersrc/compiler/parser.ts#L843 中,nextToken() 的执行结果,

function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {
  ...
  sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile);
  
  ...
  nextToken();
  
  ...
  sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);

  ...
  return sourceFile;

  ...
}

接下来就调用 parseList,就从第一个 token 开始解析了。
解析过程中,随时可以使用 token() 来获取当前 token。


总结

本文只是粗略探索了 TypeScript 词法分析器的冰山一角,
印象比较深刻的是,词法分析器内部保存了状态。

在进行词法分析时,TypeScript 会先根据下一个字符分情况处理,
在每一种情况中,都会不断的 “吃掉” 字符,直到不再满足条件的字符出现。

例如,const 关键字的扫描过程,词法分析器会先扫描到字符 c,判定这是一个标识符或者关键字,
然后往后读取字符 onst,都满足标识符的定义,
接着再读入的字符就是空格了,不再满足标识符的定义了,就返回 const,作为扫描结果。

其他 token 的扫描过程,大同小异,只是处理细节会非常繁琐。

参考

TypeScript v3.7.3

相关文章

网友评论

    本文标题:[Node] 淡如止水 TypeScript (三):词法分析

    本文链接:https://www.haomeiwen.com/subject/luwmoctx.html