龙空技术网

技术文章:说一下我写的语法分析框架

底层技术栈 918

前言:

现时同学们对“词法分析器作用是什么”大致比较关心,姐妹们都想要了解一些“词法分析器作用是什么”的相关内容。那么小编同时在网摘上收集了一些有关“词法分析器作用是什么””的相关知识,希望小伙伴们能喜欢,兄弟们快快来学习一下吧!

任何一个框架都是由两部分组成的:一是框架代码,二是模块上下文。

框架代码,是把各个模块组合起来的胶水。

模块上下文,是各个模块的私有数据。

这两者之间通过一些函数指针作为接口进行联系,C语言是这么设计的,C++的话多使用虚函数。

这些函数指针接口,一般包括初始化、结束、其他这3类,分别用于框架的初始化、析构、运行。

大多数情况下,用于初始化的函数指针有多个,分多步运行,以解决多个模块之间的耦合问题。

各种各样的框架基本都是这么设计的,scf编译器的语法分析框架也不例外。

上图的scf_parse_s是scf的总结构体,整个编译器框架的代码都是围绕它进行的。

其中的2个成员变量dfa和dfa_data,分别是语法分析框架和它的总上下文。

dfa_parse_data_t的数据结构如下,内容的注释见下图的绿字

dfa_parse_data_t数据结构

其中第1项void** module_datas是各个语法模块的上下文指针数组,是最重要的一项。具体的(各模块)上下文类型是由各个模块自己定义的。

module_datas指针数组的大小与模块个数是一致的。

其他的都是当前正在分析的语法内容:有些语法元素需要在多个模块之间处理,就放在这个总上下文里。

例如:int a[2] = {24 * 3600, 1};

数组变量a在分析初始化表达式的时候也要用到,但变量的声明和初始化表达式的分析是2个模块,所以总上下文里记录了一个current_var,表明当前正在分析的初始化表达式是数组a的。

scf_stack_t* current_identities是当前的标志符组成的栈,语法分析的场景里每个标志符的语义是不一样的(一般分为函数、变量、类型这3种),有时候没法一下子确定,所以把暂时确定不了的标志符放在这个栈里。

例如:type t;

显然第一个标志符type是类型,第二个标志符t是变量,但在刚分析到第一个标志符的时候是没法确定type到底是变量还是类型的,所以就暂时把它放在这个栈里(等可以确定时再一起出栈)。

语法分析模块的结构体

语法分析的各个模块,是以上图的结构体表示的。它有2个初始化函数,和1个析构函数。

初始化分2步,模块初始化init_module()和语法初始化init_syntax(),在多个模块之间在初始化时有耦合的情况下,可以通过这2步来解耦。

语法分析框架是先运行所有模块的init_module()函数,然后再运行所有模块的init_syntax()函数。

例如:while (i < n) i++;

它的语法是while (条件表达式) 主体顺序块,所以while模块初始化的时候表达式模块必须已经初始化完成了。

只要把表达式的初始化放在init_module()里,而把while的初始化放在init_syntax()里,就可以保证它们的初始化顺序。

dfa节点的名字是以“模块名_节点名”命名的,例如表达式模块的左小括号节点名字是expr_lp,while模块的左小括号节点是while_lp。

所有的语法节点都放在scf_dfa_s结构体的nodes动态数组里。

所有语句类型的语法规则都放在scf_dfa_s结构体的syntaxes动态数组里。

如果scf_parse_t* parse是整个编译器框架的总上下文:

parse->dfa->nodes就是语法节点的数组,

parse->dfa->syntaxes就是语法规则的数组。

语法节点的添加和查找,2个宏

语法节点的添加和查找,我给它做了2个宏。

对源代码文件的语法分析入口,是下图的scf_parse_file()函数,已添加注释:

在我的scf框架里,用scf_block_t表示一个作用域和顺序块,可以是全局域、文件域、函数域、类型域,等等。

结构体类型或类类型的成员变量,就是属于类型域里的变量。

这个函数最后的while循环,从词法分析器里读取一个个的词,进行具体的语法分析。

它的代码如下图:

scf_dfa_parse_word()函数,是语法分析框架的入口函数。

从词法分析器读取一个单词,然后调用scf_dfa_parse_word()就可以进行具体的语法分析,它会一直分析完一个完整的语法块。

例如:while (*p) p++;

读取的第一个词是while,这时候调用scf_dfa_parse_word()会一直分析完整个while循环。

scf_dfa_parse_word()函数

scf_dfa_parse_word()函数,内容如上,它会调用_scf_dfa_childs_parse_word()函数。

_scf_dfa_childs_parse_word()函数,绿字注释

_scf_dfa_childs_parse_word()函数:它首先查看有没有接收这个词的前置hook,然后根据前置hook进行语法分析,并且清除这个hook;

如果没有前置hook,就判断要不要接收这个词,如果接收就调用_scf_dfa_node_parse_word()函数进行语法分析。

这个函数的主要功能,实际就是查看使用哪个子节点去进行下一步的语法分析。

具体的语法分析,在_scf_dfa_node_parse_word()里实现,见下面的几个图:

使用当前语法node进行分析

使用后置hook进行语法分析

后置hook的使用场景是这样的:

while (*p) p++;

这里的右小括号,既是while的条件表达式结束的小括号,又是普通表达式(*p)结束的小括号。

普通表达式模块先运行,while模块后运行,但实际只有一个右小括号。

也就是说这1个小括号要触发两个模块的action()函数,所以后运行的那个模块就添加一个后置hook给dfa框架,让它自己可以在前一个模块运行完之后被正确触发。

end hook,用于模拟“递归函数”的级联返回

end hook,结束hook,它在一个代码块的语法分析完成之后触发。

还是以上面的while循环为例,当循环体代码p++;分析完之后,整个while循环也就分析完了。

所以,最后的分号不但表示表达式p++的结束,也表示while循环的结束。

如果是while (*p) { p++; },则右大括号不但表示循环体的结束,也表示整个while循环的结束。

这种情况下触发多层的end hook。

例如:

while (i < 10)

while (j < i) i--, j++;

最后的分号会触发循环体和两层while循环的返回,也是使用end hook。

_scf_dfa_node_parse_word()的最后部分

如果之前节点的action()函数返回NEXT_WORD,就读取下一个单词,继续进行语法分析。

如果是其他情况就返回对应的值,包括出现语法错误,或者一个代码块分析完了。

在需要继续分析的情况下,如果当前节点没有子节点了,就使用下一条语法规则。

如果当前节点有子节点,就使用它的子节点继续分析:

这里会调用_scf_dfa_childs_parse_word()函数,形成与_scf_dfa_node_parse_word()函数的递归调用。

这个递归调用的存在,让编辑语法规则的时候只要把某个节点添加到它自己的子节点,就可以自动实现递归。

只要把星号节点添加到它自己的子节点,就可以分析void**** ctx这种4级指针[呲牙]

最后给个scf框架的类型模块的语法规则,它是这么编辑的:

模块结构体

init_module()函数

语法编辑在_dfa_init_syntax_type()函数被框架调用时进行,

编辑的方式,就是代码怎么写的就把对应的语法节点连接起来就行。

例如,static const int** p = NULL; 那么:

const是static的子节点,int是const的子节点,*是int的子节点,

同时*也是它自己的子节点,这样就可以通过框架的_scf_dfa_childs_parse_word()和_scf_dfa_node_parse_word()这两个函数的互相递归调用,实现对源代码的递归分析。

多级指针,是这种情况的典型场景。

具体的语法编辑,代码如下,2张图,也可以看我在gitee上的代码。

标签: #词法分析器作用是什么