C编译器剖析_1.3 由文法到分析器_声明Declaration

时间:2020-12-10 15:48:05

    下面,让我们再看看ucc\examples\sc\decl.c,这里我们要处理的是变量或函数的声明。

为了使函数能带不同个数的参数,此处增加了一个ParameterList,实际运用的文法如下所示:

     Declaration   ----->    int Declarator

     Declarator   ----->   * Declarator  | PostfixDeclarator 

     PostfixDeclarator ---> DirectDeclarator | PostfixDeclarator [num]

                          | PostfixDeclarator (ParameterList)

     ParameterList --->  int |  ParameterList , int

     DirectDeclarator ----->  id  | (Declarator)

    我们以” int (*f(int,int,int))[4];”这个看起来复杂一点的声明为例子,进入命令行,运行以

下命令,我们得到的结果是” f is:  function(int,int,int)which returns pointer to array[4] of int”,分析器告诉我们,f实际上是一个带3个int参数的函数,其返回值是一个指针,指向包含4个整数的数组。

         // Windows 8

         D:\src\ucc\examples\sc>       nmake-f Makefile.win

         // Linux

         iron@ubuntu:sc$  make

    由于我们在这个简单的语言中只引入了int这个基本类型,所以所有的声明都是以int开头的,之后再跟上声明符Declarator。在声明符Declarator中,我们可以进一步指定要声明的标志符为指针类型、数组类型或者函数类型。如果把*、[]和()看成类型运算符,把int这样的基本类型看成操作数,通过声明,我们实际上是在书写类型表达式。通过整数的四则运算表达式,我们希望CPU做计算,从而可以得到运算结果;而通过类型表达式,程序员为各个标志符指明了其类型。非终结符Declarator代表的实际上是以下集合:

    {PostfixDeclarator, * PostfixDeclarator, ** PostfixDeclarator, ***PostfixDeclarator, …}

    而非终结符PostfixDeclarator代表的则是以下集合。

    {

         DirectDeclarator, DirectDeclarator [num], DirectDeclarator (ParameterList),

         DirectDeclarator[num][num], DirectDeclarator (ParameterList)( ParameterList),

         DirectDeclarator[num] ( ParameterList), DirectDeclarator (ParameterList) [num],

         …….

    }

    在这里,我们可以发现int f(int)(int)竟然也是一个符合文法的声明,因为我们完全可以通过非终结符Delcaration生成这个字符串。但GCC、Visual Studio、Clang和UCC都会给我们一个大大的错误提示,int f(int)(int)是个非法的声明,因为这个函数企图把函数作为返回值。

    iron@ubuntu:test$gcc hello.c

    hello.c:2:5:error: ‘f’ declared as function returning a function

    iron@ubuntu:test$ucc hello.c

   (declchk.c,629):(hello.c,2):error:functioncannot return function type

   ucc invokecommand error:/home/iron/bin/ucl -o hello.s hello.i

   iron@ubuntu:test$clang hello.c

   hello.c:2:6:error: function cannot return function type 'int (int)'

   int f(int)(int);

     ^

    1 errorgenerated.

    而在标准C语言中,这个声明之所以“非法”,并不是因为其不遵守文法制定的规则,而是它不遵守语义规则。C语言的标准文法规定了语法结构,但文法的能力是有限的,很多其他的限制和要求需要通过英语等自然语言来表达。 C语言附加的语义规则规定不能把函数作为返回值,请参见ucc\ansi_C_reference.txt的”3.5.4.3 Function declarators”这一节。我们为了精准地表达编程语言这样的无穷集合,引入了文法,但最后却发现世界并不是那么完美,有许多的语义规则很难用我们现在介绍的文法来表达,或者即使能用文法来表达,也会把整个文法弄得非常复杂。即便是规定“函数调用时,其实参个数应和函数声明时的形参个数一致”这样的语义规则,要在文法中表达,都要费一番周折。解决问题的思路可以有:(1)引入表达能力更强的文法,到目前为止,我们介绍的文法被称为上下文无关文法,context-free grammar, 实际上存在能力更强的文法。用集合的观点,通俗的来说,就是我们目前介绍的文法所代表的只是一个较小范围的集合,要表示范围更大的集合,需要能力更强的文法。关于这方面的研究属于“形式语言与自动机”范畴,沿着这个方向走下去,最终我们会膜拜图灵和丘奇等开天辟地的祖师爷。(2) 就如企业面试有一面、二面、三面和四面一样,我们可以仍基于上下文无关文法进行语法分析,这相当于一面,通过后,再根据自然语言描述的语义规则去编写语义分析器,相当于作二面,这样的好处是为编程语言制定的标准文法简明易懂,但“即使是同一篇文章,不同人读后都会有不同的感受”,这就是所谓的” There are a thousand Hamlets in a thousand people's eyes”。而用自然语言表述的语义规则,各大公司实现出来的语义分析器也会有些细微的差别。在后续章节分析ucc语义检查的代码时,我们会与gcc、clang和cl.exe等常见编译器进行对比,届时可以发现各个编译器的行为是存在一些差异的。实际的编译器几乎都是沿着(2)这个方向走的。如果有一天,再复杂的语义规则都可以形式化了,那按照“可以形式化,就可以自动化”的思路,程序员差不多就到了下岗的时侯了。

言归正传,上述非终结符Declaration要做的是构造类型表达式,就如非终结符Expression是构造算术或逻辑运算表达式一样。只不过Declaration中的运算符换成了*,[]和(),操作数换成了int等基本类型。如果你熟悉C++,提到把类型作为参数,你一定会想到模板。不论是类模板还是函数模板,都可以视为一个不完整的类型表达式,需要程序员提供类型作为操作数以构成一个完整的类型表达式。我们会为声明”int (*f(int,int,int))[4];”构造一棵形如图1.9的语法树。为了能更清楚地看到语法树的构造过程,结合decl.c中的代码,我们可画出相应的分析树。

C编译器剖析_1.3 由文法到分析器_声明Declaration

图1.9  声明的分析树与语法树

    虽然沿用二叉树的结点astNode,但此处我们实际上用每个astNode的kids[1]域构成了一个链表来描述标志符f的类型信息,较特别的是在FUNCION()结点中,我们用其kids[0]域来记录其形参列表。图1.9可代表的主要类型信息可表达为以下链表:

    INT  ---- > ARRAY[4]  ---- > POINTER ----> FUNCTION ---- > f

    我们由链首出发,从左向右来构造一个新的类型,其含义是:

   (1) INT为基本类型,所得类型表述为int

   (2) 把ARRAY[4]类型运算符作用于上一步的结果, 所得类型为array[4] of int

   (3) 把POINTER类型运算符作用于上一步的结果,所得类型为

               pointer toarray[4] of int

   (4) 把FUNCTION类型运算符作用于上一步的结果,即把上一步的类型作为函数返回值的类型,而函数的形参列表则存于结点FUNCTION()的kids[0]域中,所得类型为 function(int,int,int) which returns pointer to array[4] of int

   (5) 前一步所得的类型即为标志符f的类型,表述为

f is:  function(int,int,int) which returns pointerto array[4] of int

    如果我们想把f的类型信息打印出来,那显然我们需要先把打印出”fis : “,而但f所对应的结点实际上是位于链表的末尾的,而单向链表的扫描需要从链首开始出发的,这里我们可用函数递归调用来实现单向链表的“逆向遍历”,如图1-10所示。图中第108行即代表了只有遍历了当前结点的后继后,才能遍历当前结点,由此实现了单链表的“逆向遍历”。之后的第109至137行则是根据单向链表中各结点的不同类型作不同的输出。


C编译器剖析_1.3 由文法到分析器_声明Declaration

图1.10 输出类型信息

     接下来,我们以非终结符PostfixDeclarator为例,来分析一下如何构建形如图1.9的语法树。我们不妨以二维数组 int arr[3][5]为例,我们期望用以下单向链表来存放arr的类型信息。当然类似于图1.9,我们是把二叉树当单向链表来用了。

       INT  ---- > ARRAY[5]  ---- >  ARRAY[3] ---- > arr

    其含义是:

   (1) INT为基本类型,所得类型表述为int

   (2) 把ARRAY[5]类型运算符作用于上一步的结果, 所得类型为array[5] of int

   (3) 把ARRAY[3]类型运算符作用于上一步的结果,

         所得类型为array[3] of  array[5] of int

    (4)  前一步所得的类型即为标志符arr的类型,表述为

         arr is: array[3] of array[5] of int

    但是,站在编译器的词法分析器的角度出发,对程序员编写的源代码的扫描是从左向右进行的,最先遇到的是int,之后是[3],然后才是[5],但在构造类型信息时,我们需要先把ARRAY[5]作用在int上,图1.11中的第45至56行实现了此部分的功能。通过调用Declaration ()函数,我们先构建了int结点,接着在DirectDeclarator()函数中,我们构造了arr对应的结点,然后把ARRAY[3]作为其前驱。    

   (1)  int结点               在Declaration ()函数中进构建

   (2)  arr结点               在DirectDeclarator()函数中进构建

   (3)  ARRAY[3]  ---- > arr       在PostfixDeclarator ()函数中进构建

   (4)  ARRAY[5]  ---- >  ARRAY[3] ---- > arr    在PostfixDeclarator ()函数中进构建

   (5)  INT  ---- >  ARRAY[5] ---- >  ARRAY[3]  ---- > arr

                                                       在Declaration ()函数中进构建

    在Declaration()函数返回后,我们就得到了关于标志符arr的完整类型信息。而非终结符Declarator、DirectDeclarator和Declaration的处理函数,与PostfixDeclarator类似,就不再赘述。

C编译器剖析_1.3 由文法到分析器_声明Declaration

图1.11     PostfixDeclarator()

     观察Declaration的产生式,我们可以发现这样的产生式一次只能声明一个标志符,无法同时声明多个标志符,例如”int  a, b;”。

      Declaration   ----->    int Declarator

     这个问题不难解决,把上述产生式改为如下即可。

       Declaration   ----->    int DeclaratorList

       DeclaratorList  ----->  Declarator |  DeclaratorList , Declarator

     从上面的产生式,我们也能很好地理解在下面的代码中,为什么c是指针,而d只是int。因为从语法结构上, “ *  c ”是由一个声明符Declarator生成,而”d”由另一个声明符Declarator生成。

         int   *  c, d;

          当然在前文中我们举了” int (*f(int,int,int))[4];”这个例子,实际上这样复杂的声明难不倒编译器,但会难倒不少程序员,同一战壕的战友何必互相为难啦。更好的编程风格是使用typedef,使得类型信息清晰可见。在多数人看代码习惯从左到右扫描的,但是形如” int(*f(int,int,int))[4];”这样的复杂声明,需要先看f(int,int,int),再向外看。这不符合人的直觉。所以,还是建议写成以下这样符合从左向右阅读习惯的代码,毕竟代码是写给大家看的。

         typedef  int ARRAY[4];

         ARRAY * f(int, int ,int);