TS 原理詳細解讀(5)語法2-語法解析

上一節介紹了語法樹的結構,本節則介紹如何解析標記組成語法樹。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));
}
若是你仔細讀了這段代碼,你可能會有這些疑問:

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,就繼續讀一個語句,不然直接終止,而後從新判斷下一個標記的語法。 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 的核心就是一個循環,只要列表沒有結束,就一直解析同一種語法。

好比解析參數列表,碰到「)」表示列表結束,不然一直解析「參數」;好比解析數組表達式,碰到「]」結束。

若是理論接下來應該解析參數時,但下一個標記又不是參數,則會出現語法錯誤,但接下來應該解析解析參數,仍是再也不繼續參數列表,這時候用 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 如何在碰到錯誤後繼續解析。

下節將重點介紹增量解析。 #不定時更新#

 

時間有限,文章未校驗,若是發現錯誤請指出。

相關文章
相關標籤/搜索