zoukankan      html  css  js  c++  java
  • TS 原理详细解读(5)语法2-语法解析

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

    对应的源码位于 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 如何在碰到错误后继续解析。

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

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

  • 相关阅读:
    Hive内部表外部表转化分析
    20130515
    mapred.local.dir
    经典
    hive中巧用正则表达式的贪婪匹配
    做生意十大忌
    股价是最没有用的东西,要看公司基本面和盈利
    textView文本不同颜色
    Android中扫描wifi热点
    android 与 PC的socket通信
  • 原文地址:https://www.cnblogs.com/xuld/p/12266985.html
Copyright © 2011-2022 走看看