Clang Parser漫步(三)——declarator的解析
非常抱歉!!最近忙著寫畢業論文,最近幾天完成了初稿,今天抽個空把這個部分接著寫。
上文Clang Parser漫步--declaration-specifiers(二) - 知乎專欄說道,我們在解析了decl-spec之後,按照C99語法標準,接著需要解析的是declarator部分。下面C99該部分的EBNF形式的描述。在如下EBNF文法中,小寫字母表示編譯原理書中的非終結符,大寫字母表示終結符。nvar-declaration::n decl-specs declarator initialzier-exprnn[C99 6.7.5] [C++ 8p4, dcl.decl]ndeclarator:: n pointer[opt] direct-declaratornnpointer:: [C99 6.7.5]n * type-qualifier-list[opt]n * type-qualifier-list[opt] pointernndirect-declarator::n ( declarator )n IDENTIFIERn direct-declarator [ constant-expression[opt] ]n direct-declarator [ type-qual-list[opt] assignment-expr[opt] ]n direct-declarator [ static type-qual-list[opt] assign-expr ]n direct-declarator [ type-qual-list static assignment-expr ]n direct-declarator [ type-qual-list[opt] * ]n direct-declarator ( parameter-type-list )n direct-declarator ( identifier-list[opt] )n
示例如下代碼:
static const int * testVar1 = NULL;n~~~~~~~~~~~~~~~ ~~ ~~~~~~~ ~~~~~~ ;n (1) (2) (3) (4) (5)n(1).此部分是qualifiers和specifiers,在上文中敘述中Clang已經解析過了,並將相應的信息保持在DeclSpec類中;n(2).表示之後的direct-delcarator(此處是testVar)是指針類型;n(3).declarator->IDENTIFIER,該產生式,表明direct-declarator聲明的是一個標識符;n(4).initializer-exprn(5).SEMICOLONnn一下的一些例子,各位可以使用如下的代碼進行解析,以判斷符合何種語法。nstatic int * testVar2[2];nstatic int * testVar3[];nstatic int * testVar4[const 2];nstatic int * testVar4[static const 2];nstatic int * testVar5[const static 2];nstatic int * testVar6[const *];nnint * func1(int x, int y, ...)nint * func2(int x, int y)nint * func3(x, y) int x, int y; // 此種情況非常特殊,這是K&R語法形式,n誕生於標準C語言之前,但在早起應用較廣,為了兼容老的代碼,當前的C語言編譯器,n如GCC,Clang都支持這種語法形式的函數聲明和定義,等價與func2聲明的形式nint func4(int x);nnfunc5(int x); // 此種情況也非常的特殊,這是C89中的implicit int返回值類型的語n法,如果一個函數聲明沒有type-specifiers,那麼在C89語法中,將會給此函數一個默認的n返回int類型,等價於func4聲明n
ParseDeclarationOrFunctionDefinition
Clang的處理流程則是按照上述BNF語法進行模式匹配。我們以解析函數聲明和變數聲明的ParseDeclarationOrFunctionDefinition函數開始觀察Clang處理時的函數調用圖。
ParseDeclarator正如上述我們的示例代碼中的解釋,先處理decl-spec。之後則是處理一些C++、ObjC語法的特殊之處,我們暫且忽略。然後則是調用ParseDeclarator函數解析declarator。
函數ParseDeclarator其實是一個中轉函數,中轉到ParseDeclaratorInternal函數,並傳入了一個指向ParseDirectDeclarator的函數指針用於處理direct-declarator。
ParseDeclaratorInternal
我們忽略ParseDeclaratorInternal函數開始處的用於處理C++特性的部分代碼,直接跳到處理pointer部分的代碼,如下圖,如果不存在*,則說明這是direct-pointer,轉入到剛剛傳入進來的函數指針ParseDirectDeclarator。
否則,按照模式 * type-qualifiers-list declarator進行處理。當完成解析之後,執行代碼2078行的邏輯,往D(Declarator類型)中的Chunk數組增加一個類型信息,表明這個declarator是一個指針類型,並將相應的type-qualifiers-list,Loc保存。注意:tok::caret是處理ObjC語法的邏輯,此處可以忽略。ParseDirectDecalator
此函數在代碼2060行被調用,下圖則是它的函數定義,我們忽略了開始處處理C++語法的代碼邏輯:
按照BNF語法的形式,此處的語法是一個明顯的左遞歸情況,為了能夠適應Clang中手寫的遞歸下降語法分析器,此處將會對左遞歸提取左公因子,此處明顯這是 IDENTIFIER 和 (。
下面的代碼則是用於處理這兩個左公因子。先判斷當前tok是否為IDENTIFIER,或者 (,處理邏輯如下所示,相應的,為了能夠更好的提高診斷的精準度,會對很多非法的情況進行處理,並將錯誤信息發送至Diagnostic,待將來被Emit函數調用ProcessDiag函數,使用DiagnosticClient實例按照某種格式將錯誤、警告報告給用戶(通常是列印到標準輸出,則會使用類TextDiagnosticPrinter)。
if (Tok.is(tok::identifier) && D.mayHaveIdentifier()) {n // 此處的代碼處理 direct-declarator:: IDENTIFIER的產生式n assert(!getLang().CPlusPlus &&n "Theres a C++-specific check for tok::identifier above");n assert(Tok.getIdentifierInfo() && "Not an identifier?");n // 將得到的IDENTIFIER保存至Declarator對象D中n D.SetIdentifier(Tok.getIdentifierInfo(), Tok.getLocation());n ConsumeToken();n } else if (Tok.is(tok::l_paren)) {n // 處理direct-declarator:: (declarator )n // direct-declarator: ( declarator )n // Example: char (*X) or int (*XX)(void)n n // 調用ParseParenDeclarator函數處理括弧中的declarator,當然之後依然會回到ParseDeclarator的邏輯,所以我省略了對ParseParenDeclarator函數的分析。n ParseParenDeclarator(D);n } else if (D.mayOmitIdentifier()) {n // 由於ParseDirectDeclarator函數是處理declarator和abstract-declarator的公共函數,所以此處會進行判斷n // This could be something simple like "int" (in which case the declarator portion is empty), if an abstract-declarator is allowed.n D.SetIdentifier(0, Tok.getLocation());n } else {n // 該分支用於更好的進行錯誤診斷,並將D中的IDENTIFIER設為無效n if (D.getContext() == Declarator::MemberContext)n Diag(Tok, diag::err_expected_member_name_or_semi)n << D.getDeclSpec().getSourceRange();n else if (getLang().CPlusPlus)n Diag(Tok, diag::err_expected_unqualified_id);n elsen Diag(Tok, diag::err_expected_ident_lparen);n D.SetIdentifier(0, Tok.getLocation());n D.setInvalidType(true);n }n
沿著代碼的執行路徑繼續執行,會到達如下的代碼,這說明我們之前遇到的語法節點一定是下面兩者選其一。
direct-declarator:: n IDENTIFIER nn ( declarator )n
之後,則為進行循環處理數組或函數參數列表的聲明,
ParseFunctionDeclarator從ParseDirectDeclarator函數的2313行轉入該函數,用於解析函數形參列表的聲明。相應的EBNF語法如下:
parameter-type-list: [C99 6.7.5]n/// parameter-listn/// parameter-list , ...n///n/// parameter-list: [C99 6.7.5]n/// parameter-declarationn/// parameter-list , parameter-declarationn///n/// parameter-declaration: [C99 6.7.5]n/// declaration-specifiers declaratorn/// declaration-specifiers abstract-declarator[opt]n
下圖中選中的代碼用於處理空形參列表。
我們同樣省略處理C++的代碼邏輯。隨後,聲明一個函數原型作用域,在一個while死循環中處理每個形參聲明,同時,對形參名字的重複性進行檢查。同樣為了將討論的範圍局限在C99語言中,可以忽略處理GNU語法的擴充attributes,和C++默認函數形參聲明的語法。在如下圖中,先判斷是否遇見了ellipsis,表明這是否是一個可變參數類別。之後,對每一個形參變數聲明,構造一個DeclSpec對象,按照普通的變數一樣,調用ParseDeclarationSpecifiers函數用於解析decl-spec,調用ParseDeclarator遞歸的解析declarator。然後判斷接下來的一個tok是否是 COMMA (,),如果是,則消耗之,否則說明形參列表聲明已經結束,跳出該循環。
之後,如下圖所示,調用DeclaratorChunk::getFunction()函數使用剛剛解析得到的變數列表,是否為可變參數等信息構造一個函數聲明FunctionChunk對象,將其添加到Declarator的Chunk數組中。ParseBracketDeclarator
從ParseDirectDeclarator函數的2315行轉入該函數,用於解析數組[ constant-expression ]部分。處理邏輯依照如下的產生式進行處理。
direct-declarator [ constant-expression[opt] ]ndirect-declarator [ type-qual-list[opt] assignment-expr[opt] ]ndirect-declarator [ static type-qual-list[opt] assign-expr ]ndirect-declarator [ type-qual-list static assignment-expr ]ndirect-declarator [ type-qual-list[opt] * ]n
為了提高效率,首先對常見的情況進行單獨處理,避免更深入的函數調用。第一種則是空的中括弧列表 [],如果是該種情況,則使用DeclaratorChunk::getArray函數構造一個ArrayChunk,添加至Chunk數組中,之後返回。
另外一種常見的情況,則是如:int arr[3],數組的維度表達式為一個整型常量。該情況和第一種類似,不同之處在於,先對該常量表達式進行評估之後,返回一個ArrayChunk。
完成了常見情況的解析之後,則按照上述給出的產生式對比較少見但有不可或缺的語法進行解析,得到type-qualifers-list,數組維度表達式numElements,是否為一個*號等信息。然後構造一個ArrayChunk添加至Declarator的Chunk數組中,之後返回。至此,declarator非終結符的所有產生式都已經被處理了。
推薦閱讀:
※C++特性問題?
※為什麼微軟不單獨發行編譯器和鏈接器?
※單純學習C,windows下有什麼好的編譯器?
※tcc -O2會做什麼?