[Node] 淡如止水 TypeScript (四):语法分析

0. 回顾

上一篇中,我们粗略的跟了一下 TypeScript 词法分析器的扫描过程,
调用 nextToken() 之后,词法分析器做了很多事情,
但给人的感觉是井然有序的,每种情况都硬编码,然后覆盖全面。

本文,我们开始研究语法分析相关的代码,
TypeScript 语法分析是一个递归下降的处理过程,由非常多的 parseXXX 函数的互相调用完成,
每一个 parseXXX 处理 AST 的一个子树,最后拼成一棵完整的 AST。

1. 最顶层的 parseList

上一篇,我们执行过了 nextToken()
位于 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;

  ...
}

我们来看随后 parseList 的执行过程。

parseListsrc/compiler/parser.ts#L1756
这里得记住,传入的第二个参数是 parseStatement,它会层层传递,最后以回调方式调用,

function parseList<T extends Node>(kind: ParsingContext, parseElement: () => T): NodeArray<T> {
  ...
  while (!isListTerminator(kind)) {
    if (isListElement(kind, /*inErrorRecovery*/ false)) {
      const element = parseListElement(kind, parseElement);
      list.push(element);

      continue;
    }

    ...
  }

  ...
  return createNodeArray(list, listPos);
}

parseList 调用了 parseListElementsrc/compiler/parser.ts#L1779
第二个参数 parseElement 透传,值为 parseStatement

然后 parseListElement 调用了参数 parseElement,它的值就是 parseStatement

function parseListElement<T extends Node>(parsingContext: ParsingContext, parseElement: () => T): T {
  ...
  return parseElement();
}

parseStatementsrc/compiler/parser.ts#L5512,是一个有非常多个 case 构成的函数。

function parseStatement(): Statement {
  switch (token()) {
    ...
    case SyntaxKind.ConstKeyword:
      if (isStartOfDeclaration()) {
        return parseDeclaration();
      }
      break;
  }
  ...
}

它先调用 token()src/compiler/parser.ts#L1910,获取当前 token 的种类,
注意是 token 的种类,而不是当前 token 的值。

function token(): SyntaxKind {
  return currentToken;
}

然后,通过对 token 种类 SyntaxKind 进行 switch,分情况处理。
示例中,我们的 token 是 const,种类为 SyntaxKind.ConstKeyword

TypeScript 会判断当前源码位置,是否一个变量声明,
如果是的话,就当做变量声明来解析。

if (isStartOfDeclaration()) {
  return parseDeclaration();
}
break;

2. 关键的 parseStatement

2.1 前瞻判断 isStartOfDeclaration

我们先来看 isStartOfDeclarationsrc/compiler/parser.ts#L5437

function isStartOfDeclaration(): boolean {
  return lookAhead(isDeclaration);
}

其中,isDeclarationsrc/compiler/parser.ts#L5359 是一个函数,
这里是当做回调来传入的,虽然尚未执行,但我们知道它会返回 true

function isDeclaration(): boolean {
  while (true) {
    switch (token()) {
      ...
      case SyntaxKind.ConstKeyword:
        return true;
      ...
    }
  }
}

下面我们来看 lookAheadsrc/compiler/parser.ts#L1176

function lookAhead<T>(callback: () => T): T {
  return speculationHelper(callback, /*isLookAhead*/ true);
}

然后,speculationHelpersrc/compiler/parser.ts#L1139
其中 isLookAheadtrue

function speculationHelper<T>(callback: () => T, isLookAhead: boolean): T {
  ...
  const result = isLookAhead
    ? scanner.lookAhead(callback)
    : ...;
  
  ...
  return result;
}

scanner.lookAheadsrc/compiler/scanner.ts#L2262
注意到这里的 lookAhead 跟上面那个是不同的,位于 scanner.ts 中。

function lookAhead<T>(callback: () => T): T {
  return speculationHelper(callback, /*isLookahead*/ true);
}

speculationHelpersrc/compiler/scanner.ts#L2217
这个 speculationHelper 跟上文的也不同,也位于 scanner.ts 中。

其中,isLookaheadtrue

function speculationHelper<T>(callback: () => T, isLookahead: boolean): T {
  const savePos = pos;
  const saveStartPos = startPos;
  const saveTokenPos = tokenPos;
  const saveToken = token;
  const saveTokenValue = tokenValue;
  const saveTokenFlags = tokenFlags;
  const result = callback();

  // If our callback returned something 'falsy' or we're just looking ahead,
  // then unconditionally restore us to where we were.
  if (!result || isLookahead) {
    pos = savePos;
    startPos = saveStartPos;
    tokenPos = saveTokenPos;
    token = saveToken;
    tokenValue = saveTokenValue;
    tokenFlags = saveTokenFlags;
  }
  return result;
}

该函数会先将当前 token 信息保存起来,防止 callback 中对当前 token 误操作,
这里 callback 的值是上层透传过来的,实际上正是 isDeclarationsrc/compiler/parser.ts#L5359
我们之前已经分析过了,它会返回 true

因此,speculationHelper 会返回 true

这就回到了最上层,isStartOfDeclarationsrc/compiler/parser.ts#L5437,会返回 true

function isStartOfDeclaration(): boolean {
  return lookAhead(isDeclaration);
}

2.2 子树的解析

parseStatementsrc/compiler/parser.ts#L5512,中判断了 isStartOfDeclaration 之后,
就要开始解析变量声明了,parseDeclaration

function parseStatement(): Statement {
  switch (token()) {
    ...
    case SyntaxKind.ConstKeyword:
      if (isStartOfDeclaration()) {
        return parseDeclaration();
      }
      break;
  }
  ...
}

parseDeclarationsrc/compiler/parser.ts#L5588

function parseDeclaration(): Statement {
  ...

  const node = <Statement>createNodeWithJSDoc(SyntaxKind.Unknown);
  node.decorators = parseDecorators();
  node.modifiers = parseModifiers();
  if (isAmbient) {
    ...
  }
  else {
    return parseDeclarationWorker(node);
  }
}

它会先创建一个包含 js-docnode,然后解析可能出现的各个部分,比如装饰器,
最后,调用 parseDeclarationWorker 解析变量声明的主体。

parseDeclarationWorkersrc/compiler/parser.ts#L5624

function parseDeclarationWorker(node: Statement): Statement {
  switch (token()) {
    ...
    case SyntaxKind.ConstKeyword:
      return parseVariableStatement(<VariableStatement>node);
    ...
  }
}

parseVariableStatementsrc/compiler/parser.ts#L5810

function parseVariableStatement(node: VariableStatement): VariableStatement {
  node.kind = SyntaxKind.VariableStatement;
  node.declarationList = parseVariableDeclarationList(/*inForStatementInitializer*/ false);
  parseSemicolon();
  return finishNode(node);
}

设置 node 的种类为变量表达式 SyntaxKind.VariableStatement
然后,解析出变量声明列表,declarationList
最后解析分号 parseSemicolon

我们来看 parseVariableDeclarationListsrc/compiler/parser.ts#L5763

function parseVariableDeclarationList(inForStatementInitializer: boolean): VariableDeclarationList {
  const node = <VariableDeclarationList>createNode(SyntaxKind.VariableDeclarationList);

  switch (token()) {
    ...
    case SyntaxKind.ConstKeyword:
      node.flags |= NodeFlags.Const;
      break;
    ...
  }

  nextToken();

  ...
  if (token() === SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) {
    ...
  }
  else {
    ...

    node.declarations = parseDelimitedList(ParsingContext.VariableDeclarations,
      inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation);

    ...
  }

  return finishNode(node);
}

先根据当前 token 的种类,设置 node.flags
然后调用 nextToken() 处理下一个 token。

2.3 处理过程中的 nextToken()

之前我们分析过的 nextToken 的执行过程,
它会调用 scanner.scan src/compiler/scanner.ts#L1490 返回下一个 token 的种类。

我们看 debug/index.tsconst 后的下一个 token 应该为变量 i

const i: number = 1;

跟到 scanner.scan src/compiler/scanner.ts#L1912 中,我们看到 tokenValue 的值确实为 i


回到 parseVariableDeclarationListsrc/compiler/parser.ts#L5763 中来,

function parseVariableDeclarationList(inForStatementInitializer: boolean): VariableDeclarationList {
  ...
  nextToken();

  ...
  if (token() === SyntaxKind.OfKeyword && lookAhead(canFollowContextualOfKeyword)) {
    ...
  }
  else {
    ...

    node.declarations = parseDelimitedList(ParsingContext.VariableDeclarations,
      inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation);

    ...
  }

  return finishNode(node);
}

执行完 nextToken() 之后,就开始解析 const 后面的内容了。
我们能看到这是一个递归下降的解析过程,每个产生式会对应一个 parseXXX

3. 完整的调用链路

parseDelimitedList src/compiler/parser.ts#L2115 之后的处理过程,大同小异且非常的繁琐,
这里就不再逐个函数进行介绍了,按调用的层次结构列举如下,

parseList
  parseListElement
    parseElement                  // 值为 parseStatement
      parseDeclaration
        parseDecorators
        parseModifiers
        (parseDeclarationWorker)  // 辅助函数
          parseVariableStatement
            parseVariableDeclarationList
              parseDelimitedList  // <- 当前位置
                parseListElement
                  parseElement    // 值为 parseVariableDeclarationAllowExclamation
                    parseVariableDeclarationAllowExclamation
                      parseVariableDeclaration
                        parseIdentifierOrPattern
                          parseIdentifier
                        parseTypeAnnotation
                          parseType
                            parseTypeWorker
                              parseUnionTypeOrHigher
                                parseUnionOrIntersectionType
                                  parseConstituentType      // 值为 parseIntersectionTypeOrHigher
                                    parseUnionOrIntersectionType
                                      parseConstituentType  // 值为 parseTypeOperatorOrHigher
                                        parsePostfixTypeOrHigher
                                          parseNonArrayType
                                            parsePostfixTypeOrHigher 
                        parseInitializer
                          parseAssignmentExpressionOrHigher
                            parseBinaryExpressionOrHigher
                              parseUnaryExpressionOrHigher
                                parseUpdateExpression
                                  parseLeftHandSideExpressionOrHigher
                              parseBinaryExpressionRest
                            parseConditionalExpressionRest
                              parseOptionalToken
            parseSemicolon

我们看到上文分析的 parseDelimitedList 之后,还发生了很多事情。
同一个缩进层次,表示先后发生的两件事,
更深的缩进层次,表示调用另一个 parseXXX 来完成的。

整个 parseList 处理完后会得到一个类似的 AST,

{
  sourceFile: {

    // parseList
    statements: [

      // parseDeclaration
      {

        // parseDecorators
        decorators,

        // parseModifiers
        modifiers,

        // parseVariableDeclarationList
        declarationList: {

          // parseDelimitedList
          declarations: [

            // parseVariableDeclaration
            {
              // parseIdentifierOrPattern
              name,

              // parseTypeAnnotation
              type,

              // parseInitializer
              initializer,
            }
          ],
        },

        // parseSemicolon
      }
    ]
  }
}

4. 解析完毕

解析完毕后,流程就回到了篇首,
parseSourceFileWorkersrc/compiler/parser.ts#L843

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

  ...
  return sourceFile;

  ...
}

总结

在读 TypeScript 源码之前,也了解过递归下降解析器的编写方法,
也仅限于了解 LL(k) 解析器生成器的工作原理。

实际看了 TypeScript 的解析过程之后,发现 Compiler 前端并不是特别的艰深,
写出一个足够通用的解析器生成器才是困难的,甚至需要对文法做一些处理(清理 / 左递归消除),
或者引入一些数据结构(LR 状态机)。

因为可以调试,TypeScript 源码读起来,也会容易一些,
语法解析过程无非是用硬编码的方式,生成一棵 AST。
至于每个子节点是怎么处理的,要对 TypeScript 语法结构非常熟悉才行。

要照顾到所有可能的情况,这个确实是一个比较复杂的工程。

参考

TypeScript v3.7.3

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,658评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,482评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,213评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,395评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,487评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,523评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,525评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,300评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,753评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,048评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,223评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,905评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,541评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,168评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,417评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,094评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,088评论 2 352

推荐阅读更多精彩内容