上一节介绍了语法树的结构,本节则介绍如何解析标记组成语法树。

对应的源码位于 src/compiler/parser.ts。

入口函数

要解析一份源码,输入当然是源码内容(字符串),同时还提供路径(用于报错)、语言版本(比如ES3 和 ES5 在有些细节不同)。

createSourceFile 是负责将源码解析为语法树的入口函数,用户可以直接调用:比如 ts.createSourceFile(‘<stdio>’, 'var xld;')。

export function createSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {
performance.mark("beforeParse");
let result: SourceFile; perfLogger.logStartParseSourceFile(fileName);
if (languageVersion === ScriptTarget.JSON) {
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, ScriptKind.JSON);
}
else {
result = Parser.parseSourceFile(fileName, sourceText, languageVersion, /*syntaxCursor*/ undefined, setParentNodes, scriptKind);
}
perfLogger.logStopParseSourceFile(); performance.mark("afterParse");
performance.measure("Parse", "beforeParse", "afterParse");
return result;
}

入口函数内部除了某些性能测试代码,主要是调用 Parser.parseSourceFile 完成解析。

解析源文件对象

export function parseSourceFile(fileName: string, sourceText: string, languageVersion: ScriptTarget, syntaxCursor: IncrementalParser.SyntaxCursor | undefined, setParentNodes = false, scriptKind?: ScriptKind): SourceFile {
scriptKind = ensureScriptKind(fileName, scriptKind);
/* ...(略)... */ initializeState(sourceText, languageVersion, syntaxCursor, scriptKind); const result = parseSourceFileWorker(fileName, languageVersion, setParentNodes, scriptKind); clearState(); return result;
} function initializeState(_sourceText: string, languageVersion: ScriptTarget, _syntaxCursor: IncrementalParser.SyntaxCursor | undefined, scriptKind: ScriptKind) {
NodeConstructor = objectAllocator.getNodeConstructor();
TokenConstructor = objectAllocator.getTokenConstructor();
IdentifierConstructor = objectAllocator.getIdentifierConstructor();
SourceFileConstructor = objectAllocator.getSourceFileConstructor(); sourceText = _sourceText;
syntaxCursor = _syntaxCursor; parseDiagnostics = [];
parsingContext = 0;
identifiers = createMap<string>();
identifierCount = 0;
nodeCount = 0; switch (scriptKind) {
case ScriptKind.JS:
case ScriptKind.JSX:
contextFlags = NodeFlags.JavaScriptFile;
break;
case ScriptKind.JSON:
contextFlags = NodeFlags.JavaScriptFile | NodeFlags.JsonFile;
break;
default:
contextFlags = NodeFlags.None;
break;
}
parseErrorBeforeNextFinishedNode = false; // Initialize and prime the scanner before parsing the source elements.
scanner.setText(sourceText);
scanner.setOnError(scanError);
scanner.setScriptTarget(languageVersion);
scanner.setLanguageVariant(getLanguageVariant(scriptKind));
}
如果你仔细读了这段代码,你可能会有这些疑问:

1. NodeConstructor 等是什么

你可以直接将它看成 Node 类的构造函数,new NodeConstructor 和 new Node 是一回事。那为什么不直接用 new Node? 这是一种性能优化手段。TS 设计的目的是用于任何 JS 引擎,包括浏览器、Node.js、微软自己的 JS 引擎,而 Node 代表语法树节点,数目会非常多,TS 允许针对不同的环境使用不同的 Node 类型,以达到最节约内存的效果。

2. syntaxCursor 是什么

这是用于增量解析的对象,如果不执行增量解析,它是空的。增量解析是指如果之前已解析过一次源码,第二次解析时可以复用上次解析的结果,主要在编译器场景使用:编辑完源码后,源码要重新解析为语法树,如果通过增量解析,可以大幅减少解析次数。增量解析将在下一节中详细介绍。

3. identifiers 是什么

一般地,我们认为:源码中的单词都会用两次以上(变量名总会有定义和使用的时候,这里就有两次),如果将相同内容的字符串共用相同的引用,可以节约内存。identifiers 就保存了每个字符串内存的唯一引用。

4. parsingContext 是什么

用于指代当前解析所在的标记位,比如当前函数是否有 async,这样可以判断 await 是否合法。

5. parseErrorBeforeNextFinishedNode 是什么

每个语法树节点,都通过 createNode 创建,然后结束时会调用 finishNode,如果在解析一个语法树节点时出现错误(可能是词法扫描错误、也可能是语法错误),都会把 parseErrorBeforeNextFinishedNode 改成 true,在 finishNode 中会判断这个变量,然后标记这个语法树节点存在语法错误。TypeScript 比其它语法解析器强大的地方在于碰到语法错误后并不会终止解析,而是尝试修复源码。(因为在编辑器环境,不可能因为存在错误就停止自动补全)。这里标记节点语法错误,是为了下次增量解析时禁止重用此节点。

解析过程

虽然这篇文章叫 TypeScript 源码解读,但其实主要是介绍编译器的实现原理,知道了这些原理,无论什么语言的编译器你都能弄明白,反过来如果你之前没有什么基础想要自己读懂 TypeScript,那是很难的。源码就像水果,你需要自己剥;这篇文章就像果汁,营养吸收地更快。插图是展示原理的最好方式,因此文中会包含大量的插图,如果你现在读的这个网页是纯文字,一张插图都没有,那么这个网站就是盗版侵权的,请重新百度。原版都是有插图的,插图可以让你快速理解原理!这类文章目前不止中文版的稀缺,英文版的同样稀缺,毕竟了解编译原理、且真正能开发出语言的人是非常少的。有兴趣读这些文章的也绝不是只知道搬砖赚钱的菜鸟,请支持原版!

解析器每次读取一个标记,并根据这个标记判断接下来是什么语法,比如碰到 if 就知道是 if 语句,碰到 var 知道是变量声明。

当发现 if 之后,根据 if 语句的定义,接下来会强制读取一个 “(” 标记,如果读不到就报错:语法错误,缺少“(”。读完 “(” 后解析一个表达式,然后再解析一个“)”,然后再解析一个语句,如果这时接下来发现一个 else,就继续读一个语句,否则直接终止,然后重新判断下一个标记的语法。

if 语句的语法定义是这样的:

IfStatement:
if ( Expression ) Statement
if ( Expression ) Statement else Statement

这个定义的意思是:if 语句(IfStatement)有两种语法,当然无论哪种,开头都是 if ( Expression ) Statement

为什么是这样定义的呢,这是因为JS是遵守ECMA-262规范的,而ECMA-262规范就像一种协议,规定了 if 语句要怎么定义。

ECMA-262 规范也有很多版本,熟悉的ES3,ES5,ES6 这些其实就是这个规范的版本。ES10的版本可以在这里查看:http://www.ecma-international.org/ecma-262/10.0/index.html#sec-grammar-summary

代码实现

源文件由语句组成,首先读取下一个标记(nextToken);然后解析语句列表(parseList, parseStatement)

function parseSourceFileWorker(fileName: string, languageVersion: ScriptTarget, setParentNodes: boolean, scriptKind: ScriptKind): SourceFile {
const isDeclarationFile = isDeclarationFileName(fileName); sourceFile = createSourceFile(fileName, languageVersion, scriptKind, isDeclarationFile);
sourceFile.flags = contextFlags; // Prime the scanner.
nextToken();
// A member of ReadonlyArray<T> isn't assignable to a member of T[] (and prevents a direct cast) - but this is where we set up those members so they can be readonly in the future
processCommentPragmas(sourceFile as {} as PragmaContext, sourceText);
processPragmasIntoFields(sourceFile as {} as PragmaContext, reportPragmaDiagnostic); sourceFile.statements = parseList(ParsingContext.SourceElements, parseStatement);
Debug.assert(token() === SyntaxKind.EndOfFileToken);
sourceFile.endOfFileToken = addJSDocComment(parseTokenNode()); setExternalModuleIndicator(sourceFile); sourceFile.nodeCount = nodeCount;
sourceFile.identifierCount = identifierCount;
sourceFile.identifiers = identifiers;
sourceFile.parseDiagnostics = parseDiagnostics; if (setParentNodes) {
fixupParentReferences(sourceFile);
} return sourceFile; function reportPragmaDiagnostic(pos: number, end: number, diagnostic: DiagnosticMessage) {
parseDiagnostics.push(createFileDiagnostic(sourceFile, pos, end, diagnostic));
}
}

解析一个语句:

 function parseStatement(): Statement {
switch (token()) {
case SyntaxKind.SemicolonToken:
return parseEmptyStatement();
case SyntaxKind.OpenBraceToken:
return parseBlock(/*ignoreMissingOpenBrace*/ false);
case SyntaxKind.VarKeyword:
return parseVariableStatement(<VariableStatement>createNodeWithJSDoc(SyntaxKind.VariableDeclaration));
// ...(略)
}
}

规则很简单:

先看现在标记是什么,比如是 var,说明是一个 var 语句,那就继续解析 var 语句:

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

var 语句的解析过程为先解析一个声明列表,然后解析分号(parseSemicolon)

再看一个 while 语句的解析:

function parseWhileStatement(): WhileStatement {
const node = <WhileStatement>createNode(SyntaxKind.WhileStatement);
parseExpected(SyntaxKind.WhileKeyword); // while
parseExpected(SyntaxKind.OpenParenToken); // (
node.expression = allowInAnd(parseExpression); // *Expession*
parseExpected(SyntaxKind.CloseParenToken); // )
node.statement = parseStatement(); // *Statement*
return finishNode(node);
}

所谓语法解析,就是把每个不同的语法都这样解析一次,然后得到语法树。

其中,最复杂的应该是解析列表(parseList):

 // Parses a list of elements
function parseList<T extends Node>(kind: ParsingContext, parseElement: () => T): NodeArray<T> {
const saveParsingContext = parsingContext;
parsingContext |= 1 << kind;
const list = [];
const listPos = getNodePos(); while (!isListTerminator(kind)) {
if (isListElement(kind, /*inErrorRecovery*/ false)) {
const element = parseListElement(kind, parseElement);
list.push(element); continue;
} if (abortParsingListOrMoveToNextToken(kind)) {
break;
}
} parsingContext = saveParsingContext;
return createNodeArray(list, listPos);
}

parseList 的核心就是一个循环,只要列表没有结束,就一直解析同一种语法。

比如解析参数列表,碰到“)”表示列表结束,否则一直解析“参数”;比如解析数组表达式,碰到“]”结束。

如果理论接下来应该解析参数时,但下一个标记又不是参数,则会出现语法错误,但接下来应该解析解析参数,还是不再继续参数列表,这时候用 abortParsingListOrMoveToNextToken 判断。
 
其中,kind: ParsingContext 用于区分不同的列表(是参数,还是数组?或者别的?)
 

列表结束

// True if positioned at a list terminator
function isListTerminator(kind: ParsingContext): boolean {
if (token() === SyntaxKind.EndOfFileToken) {
// Being at the end of the file ends all lists.
return true;
} switch (kind) {
case ParsingContext.BlockStatements:
case ParsingContext.SwitchClauses:
case ParsingContext.TypeMembers:
return token() === SyntaxKind.CloseBraceToken;
// ...(略)
}
}

总结:对于有括号的标记,只有碰到右半括号,才能停止解析,其它的比如继承列表(extends A, B, C) 碰到 “{” 就结束。

解析元素

function parseListElement<T extends Node>(parsingContext: ParsingContext, parseElement: () => T): T {
const node = currentNode(parsingContext);
if (node) {
return <T>consumeNode(node);
} return parseElement();
}

这里本质是使用了 parseElement,其它代码是为了增量解析(后面详解)

继续列表?

function abortParsingListOrMoveToNextToken(kind: ParsingContext) {
parseErrorAtCurrentToken(parsingContextErrors(kind));
if (isInSomeParsingContext()) {
return true;
} nextToken();
return false;
}
// True if positioned at element or terminator of the current list or any enclosing list
function isInSomeParsingContext(): boolean {
for (let kind = 0; kind < ParsingContext.Count; kind++) {
if (parsingContext & (1 << kind)) { // 只要是任意一种上下文
if (isListElement(kind, /*inErrorRecovery*/ true) || isListTerminator(kind)) {
return true;
}
}
} return false;
}

总结:如果接下来的标记是合法的元素,就继续解析,此时解析器认为用户只是忘打逗号之类的分隔符。如果不是说明这个列表根本就是有问题的,不再继续犯错。

上面重点介绍了 if 的语法,其它都大同小异,就不再介绍。

现在你应该知道语法树产生的大致过程了,如果仍不懂的,可在此处停顿往回复习,并对照源码,加以理解。

语法上下文

有些语法的使用是有要求的,比如 await 只在 async 函数内部才作关键字。

源码中用闭包内全局的变量存储这些信息。思路是:先设置允许 await 标记位,然后解析表达式(这时标记位已设置成允许 await),解析完成则清除标记位。

function doInAwaitContext<T>(func: () => T): T {
return doInsideOfContext(NodeFlags.AwaitContext, func);
} function doInsideOfContext<T>(context: NodeFlags, func: () => T): T {
// contextFlagsToSet will contain only the context flags that
// are not currently set that we need to temporarily enable.
// We don't just blindly reset to the previous flags to ensure
// that we do not mutate cached flags for the incremental
// parser (ThisNodeHasError, ThisNodeOrAnySubNodesHasError, and
// HasAggregatedChildData).
const contextFlagsToSet = context & ~contextFlags;
if (contextFlagsToSet) {
// set the requested context flags
setContextFlag(/*val*/ true, contextFlagsToSet);
const result = func();
// reset the context flags we just set
setContextFlag(/*val*/ false, contextFlagsToSet);
return result;
} // no need to do anything special as we are already in all of the requested contexts
return func();
}

这也是为什么规范中每个语法名称后面都带了一个小括号的原因:表示此处的表达式是否包括 await 这样的意义。

IfStatement[Yield, Await, Return]:
if ( Expression[+In, ?Yield, ?Await] ) Statement[?Yield, ?Await, ?Return] else Statement[?Yield, ?Await, ?Return]
if ( Expression[+In, ?Yield, ?Await] ) Statement[?Yield, ?Await, ?Return]

其中,+In 表示允许 in 标记,?yield 表示 yield 标记保持不变,- in 表示禁止 in 标记。

通过上下文语法,下面这样的代码是不允许的:

for(var x = key in item; x; ) {

}

(虽然 in 是本身也是可以直接使用的运算符,但不能用于 for 的初始值,否则按 for..in 解析。)

后瞻(lookahead)

上面举的例子,都是可以通过第一个标记就可以确定后面的语法(比如碰到 if 就按 if 语句处理)。那有没可能只看第一个标记无法确定之后的语法呢?

早期的 JS 版本是没有的(毕竟这样编译器做起来简单),但随着 JS 功能不断增加,就出现了这样的情况。

比如直接 x 是变量,如果后面有箭头, x =>,就成了参数。

这时需要用到语法后瞻,所谓的后瞻就是提前看一下后面的标记,然后决定。

/** Invokes the provided callback then unconditionally restores the parser to the state it
* was in immediately prior to invoking the callback. The result of invoking the callback
* is returned from this function.
*/
function lookAhead<T>(callback: () => T): T {
return speculationHelper(callback, /*isLookAhead*/ true);
} function speculationHelper<T>(callback: () => T, isLookAhead: boolean): T {
// Keep track of the state we'll need to rollback to if lookahead fails (or if the
// caller asked us to always reset our state).
const saveToken = currentToken;
const saveParseDiagnosticsLength = parseDiagnostics.length;
const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; // Note: it is not actually necessary to save/restore the context flags here. That's
// because the saving/restoring of these flags happens naturally through the recursive
// descent nature of our parser. However, we still store this here just so we can
// assert that invariant holds.
const saveContextFlags = contextFlags; // If we're only looking ahead, then tell the scanner to only lookahead as well.
// Otherwise, if we're actually speculatively parsing, then tell the scanner to do the
// same.
const result = isLookAhead
? scanner.lookAhead(callback)
: scanner.tryScan(callback); Debug.assert(saveContextFlags === contextFlags); // If our callback returned something 'falsy' or we're just looking ahead,
// then unconditionally restore us to where we were.
if (!result || isLookAhead) {
currentToken = saveToken;
parseDiagnostics.length = saveParseDiagnosticsLength;
parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode;
} return result;
}

说的比较简单,具体实现稍微麻烦:如果预览的时候发现标记错误咋办?所以需要先记住当前的错误信息,然后使用扫描器的预览功能读取之后的标记,之后完全恢复到之前的状态。

TypeScript 中有哪些语法要后瞻呢?

  • <T> 可能是类型转换(<any>x)、箭头函数( <T> x => {})或 JSX (<T>X</T>)
  • public 可能是修饰符(public class A {}),或变量(public++)
  • type 在后面跟标识符时才是别名类型,否则作变量
  • let 只有在后面跟标识符时才是变量声明,否则是变量,但 let let = 1 是不对的。

可以看到语法后瞻增加了编译器的复杂度,也浪费了一些性能。

虽然语法设计者尽量避免出现这样的后瞻,但还是有一些地方,因为兼容问题不得不采用这个方案。

语法歧义

/ 的歧义

之前提到的 / 可能是除号或正则表达式,在词法阶段还无法分析,但在语法解析阶段,因为已知道现在需要什么语法,可以正确地处理这个符号。

比如需要表达式的时候,碰到 /,因为 / 不能是表达式的开头,只能把 / 重新按正则表达式标记解析。如果在表达式后面碰到 /,那就作除号。

但也有些歧义是语法解析阶段都很难处理的。

< 的歧义

比如 call<number,  any>(x),你可能觉得是调用 call 泛型函数(参数  x),但它也可以理解成: (call < number) , (any > (x))

所有支持泛型的C风格语言都有类似的问题,多数编译器的做法是:和你想的一样,按泛型看,毕竟一般人很少在 > 后面写括号。

在 TS 设计之初,<T>x 是表示类型转换的,这个设计源于 C。但后来为了支持 JSX,这个语法就和 JSX 彻底冲突了。

因此 TS 选择的方案是:引入 as 语法,<T>x 和 x as T 完全相同。同时引入 tsx 扩展名,在 tsx 中,<T> 当 JSX,在普通 ts,<T> 依然是类型转换(为兼容)。

插入分号

JS 一向允许省略分号,在需要解析分号的地方判断后面的标记是否是“}”,或包含空行。

function canParseSemicolon() {
// If there's a real semicolon, then we can always parse it out.
if (token() === SyntaxKind.SemicolonToken) {
return true;
} // We can parse out an optional semicolon in ASI cases in the following cases.
return token() === SyntaxKind.CloseBraceToken || token() === SyntaxKind.EndOfFileToken || scanner.hasPrecedingLineBreak();
}

JSDoc

TS 为了尽可能兼容 JS,允许用户直接使用 JS + JSDoc 的方式备注类型,所以 JS 里的 JSDoc 注释也按源码的一部分解析。

/** @type {string} */
var x = 120
export function parseIsolatedJSDocComment(content: string, start: number | undefined, length: number | undefined): { jsDoc: JSDoc, diagnostics: Diagnostic[] } | undefined {
initializeState(content, ScriptTarget.Latest, /*_syntaxCursor:*/ undefined, ScriptKind.JS);
sourceFile = <SourceFile>{ languageVariant: LanguageVariant.Standard, text: content };
const jsDoc = doInsideOfContext(NodeFlags.None, () => parseJSDocCommentWorker(start, length));
const diagnostics = parseDiagnostics;
clearState(); return jsDoc ? { jsDoc, diagnostics } : undefined;
} export function parseJSDocComment(parent: HasJSDoc, start: number, length: number): JSDoc | undefined {
const saveToken = currentToken;
const saveParseDiagnosticsLength = parseDiagnostics.length;
const saveParseErrorBeforeNextFinishedNode = parseErrorBeforeNextFinishedNode; const comment = doInsideOfContext(NodeFlags.None, () => parseJSDocCommentWorker(start, length));
if (comment) {
comment.parent = parent;
} if (contextFlags & NodeFlags.JavaScriptFile) {
if (!sourceFile.jsDocDiagnostics) {
sourceFile.jsDocDiagnostics = [];
}
sourceFile.jsDocDiagnostics.push(...parseDiagnostics);
}
currentToken = saveToken;
parseDiagnostics.length = saveParseDiagnosticsLength;
parseErrorBeforeNextFinishedNode = saveParseErrorBeforeNextFinishedNode; return comment;
}

小结

本节介绍了语法解析,并提到了 TS 如何在碰到错误后继续解析。

下节将重点介绍增量解析。 #不定时更新#

时间有限,文章未校验,如果发现错误请指出。

最新文章

  1. 天气api
  2. BarManager.ContextMenu
  3. Mysql 字符串处理函数
  4. [ CodeVS冲杯之路 ] P1165
  5. 角色控制器(CharacterController)
  6. 【转】YUV420P的格式以及转换为RGB565的代码(Android摄像头的输出一般为YUV420P)
  7. HDU 5904 LCIS
  8. cnblogs博客自定义
  9. 用TensorFlow实现文本分析模型,做个聊天机器人
  10. Go语言复制文件
  11. Openstack配置文件管理的变迁之路
  12. keycloak
  13. py-day1-6 python 5个灰魔法 【len,index索引,for循环,切片】
  14. pc-H5 适配方案
  15. .NET Core1.1+VS2017RC+MySQL+EF搭建多层Web应用程序
  16. windows32位系统 安装MongoDB
  17. web.py尝试性学习!
  18. WebApi是轻量级的,WCF是重量级的,可以Api调用WCF,更灵活
  19. pygame--颜色变化
  20. jQuery事件篇---高级事件

热门文章

  1. Spring应用事件(Application Event)
  2. 毕业两年半,入手人生第一款macbook pro
  3. html1,初识html
  4. HashMap 源码赏析 JDK8
  5. Vue CLI及其vue.config.js(一)
  6. 小程序中组件公用属性和data-的使用
  7. 2次方的期望dp
  8. 发布到远程存储库时遇到错误: Git failed with a fatal error.
  9. C# html生成图片保存下载
  10. Spring加载早期获取BasePackage