在上一節介紹了語法樹的結構,本節則介紹如何解析標記組成語法樹。html
對應的源碼位於 src/compiler/parser.ts。node
要解析一份源碼,輸入固然是源碼內容(字符串),同時還提供路徑(用於報錯)、語言版本(好比ES3 和 ES5 在有些細節不一樣)。c++
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 完成解析。express
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)); }
用於指代當前解析所在的標記位,好比當前函數是否有 async,這樣能夠判斷 await 是否合法。數組
每一個語法樹節點,都經過 createNode 建立,而後結束時會調用 finishNode,若是在解析一個語法樹節點時出現錯誤(多是詞法掃描錯誤、也多是語法錯誤),都會把 parseErrorBeforeNextFinishedNode 改爲 true,在 finishNode 中會判斷這個變量,而後標記這個語法樹節點存在語法錯誤。TypeScript 比其它語法解析器強大的地方在於碰到語法錯誤後並不會終止解析,而是嘗試修復源碼。(由於在編輯器環境,不可能由於存在錯誤就中止自動補全)。這裏標記節點語法錯誤,是爲了下次增量解析時禁止重用此節點。瀏覽器
雖然這篇文章叫 TypeScript 源碼解讀,但其實主要是介紹編譯器的實現原理,知道了這些原理,不管什麼語言的編譯器你都能弄明白,反過來若是你以前沒有什麼基礎想要本身讀懂 TypeScript,那是很難的。源碼就像水果,你須要本身剝;這篇文章就像果汁,養分吸取地更快。插圖是展現原理的最好方式,所以文中會包含大量的插圖,若是你如今讀的這個網頁是純文字,一張插圖都沒有,那麼這個網站就是盜版侵權的,請從新百度。原版都是有插圖的,插圖可讓你快速理解原理!這類文章目前不止中文版的稀缺,英文版的一樣稀缺,畢竟瞭解編譯原理、且真正能開發出語言的人是很是少的。有興趣讀這些文章的也毫不是隻知道搬磚賺錢的菜鳥,請支持原版!性能優化
解析器每次讀取一個標記,並根據這個標記判斷接下來是什麼語法,好比碰到 if 就知道是 if 語句,碰到 var 知道是變量聲明。閉包
當發現 if 以後,根據 if 語句的定義,接下來會強制讀取一個 「(」 標記,若是讀不到就報錯:語法錯誤,缺乏「(」。讀完 「(」 後解析一個表達式,而後再解析一個「)」,而後再解析一個語句,若是這時接下來發現一個 else,就繼續讀一個語句,不然直接終止,而後從新判斷下一個標記的語法。 app
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 的核心就是一個循環,只要列表沒有結束,就一直解析同一種語法。
好比解析參數列表,碰到「)」表示列表結束,不然一直解析「參數」;好比解析數組表達式,碰到「]」結束。
// 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 解析。)
上面舉的例子,都是能夠經過第一個標記就能夠肯定後面的語法(好比碰到 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 中有哪些語法要後瞻呢?
能夠看到語法後瞻增長了編譯器的複雜度,也浪費了一些性能。
雖然語法設計者儘可能避免出現這樣的後瞻,但仍是有一些地方,由於兼容問題不得不採用這個方案。
以前提到的 / 多是除號或正則表達式,在詞法階段還沒法分析,但在語法解析階段,由於已知道如今須要什麼語法,能夠正確地處理這個符號。
好比須要表達式的時候,碰到 /,由於 / 不能是表達式的開頭,只能把 / 從新按正則表達式標記解析。若是在表達式後面碰到 /,那就做除號。
但也有些歧義是語法解析階段都很難處理的。
好比 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(); }
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 如何在碰到錯誤後繼續解析。
下節將重點介紹增量解析。 #不定時更新#
時間有限,文章未校驗,若是發現錯誤請指出。