函数调用,是编程语言的一个重要语法功能。
一般以函数名加小括号表示,小括号内添加实参列表(如果有参数的话)。
每一个实参,都是一个表达式,多个参数之间以逗号分隔。
函数调用语句,我们也可以把它看作一个特殊的表达式。它可以作为表达式的子表达式存在,它的实参表达式里也可以含有另一个函数调用表达式,即函数可以嵌套调用。
例如,add(1, sub(2, mul(3, 5))),这个函数调用需要逐层的递归分析。
函数调用模块的上下文见上图,每一层函数调用都生成这么一个上下文,嵌套的函数调用通过栈来处理。
这个栈,作为该模块的模块数据,见下图的init_module函数。
它的dfa节点如下:
lp,左小括号,表示函数调用的开始,它前面是函数名。
rp,右小括号,表示函数调用的结束,它之后可以跟分号、逗号、或二元运算符。
comma,逗号,在这里用于分隔实参列表。
lp_stat,用于统计左小括号的个数,与右小括号要匹配。因为实参表达式里也可能出现小括号,这个统计是必要的。
语法的编辑规则如上图,305-308行,expr与comma互相连接组成实参列表的递归分析,最后以右小括号结束。
298-303行,是参数里含有new运算符的分析。
统计小括号个数的action函数:
它是作为dfa hook来运作的,当出现左小括号时触发,并统计当前函数调用中的左小括号个数。
左小括号的action函数:
标志符之后跟着小括号,表示函数调用,标志符是被调函数的名字。
这个函数必须能够用函数名在ast语法树上找到,否则要报错:函数未声明。
如果调用的是外部函数,函数的声明必须放在调用代码之前的某个位置,以保证编译器可以找到。C/C++都是放在头文件里,直接包含头文件就行。
不管是通过函数名的直接调用,还是通过函数指针的间接调用,我们都当作函数指针来处理。
只是给前者设置const标志,表示直接调用的函数不会变化,在生成二进制码时可以给它生成一个重定位符号(re-location symbol)。
重定位符号,可以被连接器(Linker)替换为函数的实际地址。
给这个函数指针变量生成一个ast节点:node_pf。
然后生成一个函数调用的call节点,它的类型是SCF_OP_CALL,表示函数调用语句。
它的第一个子节点,是被调函数的指针(直接调用加上const标志)。
申请一个dfa_call_data_t的上下文,把这些信息存下来,然后入栈。栈顶永远是当前函数调用的上下文。
添加3个hook,分别检测右小括号、逗号、左小括号。哪个出现的最晚,哪个在最前面,因为hook链表也是栈结构。
逗号的action函数:
逗号出现时,说明分析完了一个实参的表达式。
把它添加到函数调用上下文(在栈顶)的参数数组argv里。
切换到这个dfa节点,继续分析下一个参数,所以给dfa框架返回DFA_SWITCH_TO。
右小括号的action函数:
右小括号出现时,要比较左右小括号的个数是否匹配。如果匹配,说明函数调用的参数分析完了。不匹配,则继续分析。
167-172行,如果这个函数调用是某个表达式的子表达式,则把它添加到母表达式里。
174-187行,添加各个参数到call节点。
然后把它设置为当前的表达式d->expr,因为不确定它是子表达式,还是某个顺序块的单独语句(以分号结尾)。
函数调用模块,实际被当作表达式模块的子模块。它的语法实际被编辑在dfa expr模块的798-802行,可以查看之前的两篇文章:
怎么用C语言写语法分析3,基于有限自动机的表达式分析
怎么用C语言写语法分析4,表达式分析的代码
写完今天这篇,编译器里复杂的语法分析部分就写完了。
当然并不完善,没有包括发现语法错误时怎么打印提示信息,以及怎么继续分析。
否则发现一个错误就会终止,实际的编译器都是发现多个错误后再终止,这样可以一次打印很多错误信息。
(有兴趣的可以自己添加)
接下来的文章,继续说语义分析。
想了解更多精彩内容,快来关注闲聊代码