Lex和Yacc应用方法(四).语法树的应用
草木瓜 20070515
一、序
不论什么语言,语法结构总是那几种,可以想象任何程序体都可以解释成一棵语法
树,语法树的本质是递归,很显然Yacc文法的核心思想也是递归。本文就通过具体实例,
使用Yacc构建递归的语法树来解决实际问题。
比较遗憾的是,在总结的过程中想表达清楚并不容易,估且三分言传,七分会意吧。
关键在于个人去思考。
二、递归的一些思想
我们先看一个简化的C语言示例段:
i=0;
while(i<=10) {
print(i);
i=i+1;
}
print(i+i);
首先,我们将()+/* print while之类的组合称为expr(expression),仅表示基本的表
达式,可以理解通过递归可以进行任意的运算符组合。如下面每一行都可称为expr:
i=0
while(i<=10)
print(i)
i=i+1
print(i+i)
再把expr + ;的语句行称为stmt(statement),表示一条语句的结束。把{}引起来的多个stmt
称为stmt_list。如此,原示例段可表示为:
stmt
expr stmt_list
stmt
这样显然不符合递归法则,倘若stmt也可由expr stmt_list组合,程序则可以递归到最顶级
stmt
stmt
stmt
这也要求yacc文法定义必须可以递归到最顶级,即如上所示。
三、内存结构
编译过程必须在内存中形成一定规则且可递归的树形结构,比较容易理解对每一语句stmt
需要构建一棵语法树。
以下是我们准备采用的语法树实际示例:
Graph 0:
[=]
|
|----|
| |
idx(i) c(0)
Graph 1:
while
|
|----------------|
| |
[<=] [;]
| |
|-----| |----------|
| | | |
idx(i) c(10) print [=]
| |
| |-------|
| | |
idx(i) idx(i) [+]
|
|----|
| |
idx(i) c(1)
Graph 2:
print
|
|
|
[+]
|
|-----|
| |
idx(i) idx(i)
细心查看以上三张图,会发现每个stmt即对应一张树形图,树结点包含三种类型:操作符
(如 + = ; ),变量索引(如 idx(i))和值(如 c(10) c(1) )。对于每个操作符,需要保证一
个可递归的规则。
四、具体实例
A. node.h <树结点的定义头文件>
/* 定义树结点的权举类型 */
typedef enum { TYPE_CONTENT, TYPE_INDEX, TYPE_OP } NodeEnum;
/* 操作符 */
typedef struct {
int name; /* 操作符名称 */
int num; /* 操作元个数 */
struct NodeTag * node[1]; /* 操作元地址 可扩展 */
} OpNode;
typedef struct NodeTag {
NodeEnum type; /* 树结点类型 */
/* Union 必须是最后一个成员 */
union {
int content; /* 内容 */
int index; /* 索引 */
OpNode op; /* 操作符对象 */
};
} Node;
extern int Var[26];
[说明] Node即为定义的树形结点,结点可以是三种类型(CONTENT,INDEX,OP)。结点
如果是操作符对象(OpNode)的话,结点可继续递归结点。操作符结点包括了名称,个
数和子结点三个要素,其中子结点可以为多个。
B.lexya_e.l <lex文件>
%{
#include <stdlib.h>
#include "node.h"
#include "lexya_e.tab.h"
void yyerror(char *);
%}
%%
[a-z] {
yylval.sIndex = *yytext -'a';
return VARIABLE;
}
[0-9]+ {
yylval.iValue = atoi(yytext);
return INTEGER;
}
[()<>=+*/;{}.] {
return *yytext;
}
">=" return GE;
"<=" return LE;
"==" return EQ;
"!=" return NE;
"&&" return AND;
"||" return OR;
"while" return WHILE;
"if" return IF;
"else" return ELSE;
"print" return PRINT;
[/t/n]+ ; /* 去除空格,回车 */
. printf("unknow symbol:[%s]/n",yytext);
%%
int yywrap(void) {
return 1;
}
[说明] 这里的Lex文件比较简单,没啥好说的。
C.lexya_e.y <yacc文件>
001 %{
002
003 #include <stdio.h>
004 #include <stdlib.h>
005 #include <stdarg.h>
006 #include "node.h"
007
008 /* 属性操作类型 */
009 Node *opr(int name, int num, ...);
010
011 Node *set_index(int value);
012 Node *set_content(int value);
013
014 void freeNode(Node *p);
015 int exeNode(Node *p);
016
017 int yylexeNode(void);
018 void yyerror(char *s);
019
020 int Var[26]; /* 变量数组 */
021
022 %}
023
024 %union {
025 int iValue; /* 变量值 */
026 char sIndex; /* 变量数组索引 */
027 Node *nPtr; /* 结点地址 */
028 };
029
030 %token <iValue> VARIABLE
031 %token <sIndex> INTEGER
032 %token WHILE IF PRINT
033 %nonassoc IFX
034 %nonassoc ELSE
035 %left AND OR GE LE EQ NE '>' '<'
036 %left '+' '-'
037 %left '*' '/'
038 %nonassoc UMINUS
039 %type <nPtr> stmt expr stmt_list
040 %%
041 program:
042 function { exit(0); }
043 ;
044 function:
045 function stmt { exeNode($2); freeNode($2); }
046 | /* NULL */
047 ;
048 stmt:
049 ';' { $$ = opr(';', 2, NULL, NULL); }
050 | expr ';' { $$ = $1; }
051 | PRINT expr ';' { $$ = opr(PRINT, 1, $2); }
052 | VARIABLE '=' expr ';' { $$ = opr('=', 2, set_index($1), $3); }
053 | WHILE '(' expr ')' stmt { $$ = opr(WHILE, 2, $3, $5); }
054 | IF '(' expr ')' stmt %prec IFX { $$ = opr(IF, 2, $3, $5); }
055 | IF '(' expr ')' stmt ELSE stmt %prec ELSE { $$ = opr(IF, 3, $3, $5, $7); }
056 | '{' stmt_list '}' { $$ = $2; }
057 ;
058 stmt_list:
059 stmt { $$ = $1; }
060 | stmt_list stmt { $$ = opr(';', 2, $1, $2); }
061 ;
062 expr:
063 INTEGER { $$ = set_content($1); }
064 | VARIABLE { $$ = set_index($1); }
065 | '-' expr %prec UMINUS { $$ = opr(UMINUS, 1, $2); }
066 | expr '+' expr { $$ = opr('+', 2, $1, $3); }
067 | expr '-' expr { $$ = opr('-', 2, $1, $3); }
068 | expr '*' expr { $$ = opr('*', 2, $1, $3); }
069 | expr '/' expr { $$ = opr('/', 2, $1, $3); }
070 | expr '<' expr { $$ = opr('<', 2, $1, $3); }
071 | expr '>' expr { $$ = opr('>', 2, $1, $3); }
072 | expr GE expr { $$ = opr(GE, 2, $1, $3); }
073 | expr LE expr { $$ = opr(LE, 2, $1, $3); }
074 | expr NE expr { $$ = opr(NE, 2, $1, $3); }
075 | expr EQ expr { $$ = opr(EQ, 2, $1, $3); }
076 | expr AND expr { $$ = opr(AND, 2, $1, $3); }
077 | expr OR expr { $$ = opr(OR, 2, $1, $3); }
078 | '(' expr ')' { $$ = $2; }
079 ;
080 %%
081 #define SIZE_OF_NODE ((char *)&p->content - (char *)p)
082
083 Node *set_content(int value) {
084
085 Node *p;
086
087 size_t sizeNode;
088 /* 分配结点空间 */
089 sizeNode = SIZE_OF_NODE + sizeof(int);
090
091 if ((p = malloc(sizeNode)) == NULL)
092 yyerror("out of memory");
093
094 /* 复制内容 */
095 p->type = TYPE_CONTENT;
096 p->content = value;
097
098 return p;
099
100 }
101
102 Node *set_index(int value) {
103
104 Node *p;
105 size_t sizeNode;
106 /* 分配结点空间 */
107 sizeNode = SIZE_OF_NODE + sizeof(int);
108
109 if ((p = malloc(sizeNode)) == NULL)
110 yyerror("out of memory");
111
112 /* 复制内容 */
113 p->type = TYPE_INDEX;
114 p->index = value;
115
116 return p;
117 }
118
119 Node *opr(int name, int num, ...) {
120
121 va_list valist;
122 Node *p;
123 size_t sizeNode;
124 int i;
125 /* 分配结点空间 */
126 sizeNode = SIZE_OF_NODE + sizeof(OpNode) + (num - 1) * sizeof(Node*);
127
128 if ((p = malloc(sizeNode)) == NULL)
129 yyerror("out of memory");
130
131 /* 复制内容 */
132
133 p->type = TYPE_OP;
134 p->op.name = name;
135 p->op.num = num;
136
137 va_start(valist, num);
138
139 for (i = 0; i < num; i++)
140 p->op.node[i] = va_arg(valist, Node*);
141
142 va_end(valist);
143 return p;
144 }
145 void freeNode(Node *p) {
146 int i;
147 if (!p) return;
148 if (p->type == TYPE_OP) {
149 for (i = 0; i < p->op.num; i++)
150 freeNode(p->op.node[i]);
151 }
152 free (p);
153 }
154 void yyerror(char *s) {
155 fprintf(stdout, "%s/n", s);
156 }
157 int main(void) {
158 yyparse();
159 return 0;
160 }
[说明] 这个文件是核心所在,大体分为Yacc预定义文法,BNF递归文法和扩展实现函数
三个部分。
Yacc预定义文法的解释:
(1).(024-031)(039)
%union {
int iValue; /* 变量值 */
char sIndex; /* 变量数组索引 */
Node *nPtr; /* 结点地址 */
};
%token <iValue> INTEGER
%token <sIndex> VARIABLE
(024-031)这一段扩充了yystype的内容,默认yystype只是int类型,编译之后会
生成如下代码:
#ifndef YYSTYPE
typedef union {
int iValue; /* 变量值 */
char sIndex; /* 变量数组索引 */
Node *nPtr; /* 结点地址 */
} yystype;
# define YYSTYPE yystype
# define YYSTYPE_IS_TRIVIAL 1
#endif
并将<iValue>与INTEGER,<sIndex>与VARIABLE绑定,表示对lex返回的值自动
进行类型转换。
(039)将<nPtr>与Union的指针类型绑定。
即在剖析器的内容栈中,常量、变量和节点都可以由yylval表示。yylval既可以是int,
char,也可以是Node *。具体可以查看lexya_e.tab.c生成文件部分,可有更深刻的
理解。(switch (yyn) 下面)
(2).(032-039)
这段方法主要定义了操作符的优先级。nonassoc,意味着没有依赖关系。它经常在连接
词中和 %prec一起使用,用于指定一个规则的优先级。如下面的规则存在IF ELSE的二义
性,需要使用nonassoc指定规则。054对应IFX,055对应ELSE,055高于054。
054 | IF '(' expr ')' stmt %prec IFX { $$ = opr(IF, 2, $3, $5); }
055 | IF '(' expr ')' stmt ELSE stmt %prec ELSE { $$ = opr(IF, 3, $3, $5, $7); }
(039)type的关键字语句,表示后面的返回值是<ptr>类型。
BNF递归文法(040-080):
这个递归文法看起来容易,自个设计起来还是有点难度的,相关的递归思想可以参见本文
最前面的“递归的一些思想”。可以考虑一下(056)-(060)的语法定义。
扩展实现函数:
本例扩展了set_index,set_value两个赋值语句,其操作实质是在内存空间分配index
和value的两种树结点。opr这个扩展函数很重要,而且使用了动态参数,主要考虑操作
符的操作元个数是可变的,这个也与头文件“struct NodeTag * node[1];”的定义思
想一致。opr主要在内存空间中分配操作符相关的树结点。
set_index,set_value,opr从概念上是完全一致,目的就是在内存中构造一棵可递归的
语法树。
D.parser.c
#include <stdio.h>
#include "node.h"
#include "lexya_e.tab.h"
int exeNode(Node *p) {
if (!p) return 0;
switch(p->type) {
case TYPE_CONTENT: return p->content;
case TYPE_INDEX: return Var[p->index];
case TYPE_OP:
switch(p->op.name) {
case WHILE: while(exeNode(p->op.node[0]))exeNode(p->op.node[1]);
return 0;
case IF: if (exeNode(p->op.node[0]))
exeNode(p->op.node[1]);
else
if (p->op.num>2)
exeNode(p->op.node[2]);
return 0;
case PRINT: printf("%d/n", exeNode(p->op.node[0]));
return 0;
case ';': exeNode(p->op.node[0]);
return exeNode(p->op.node[1]);
case '=': return Var[p->op.node[0]->index] = exeNode(p->op.node[1]);
case UMINUS: return exeNode(p->op.node[0]);
case '+': return exeNode(p->op.node[0]) + exeNode(p->op.node[1]);
case '-': return exeNode(p->op.node[0]) - exeNode(p->op.node[1]);
case '*': return exeNode(p->op.node[0]) * exeNode(p->op.node[1]);
case '/': return exeNode(p->op.node[0]) / exeNode(p->op.node[1]);
case '<': return exeNode(p->op.node[0]) < exeNode(p->op.node[1]);
case '>': return exeNode(p->op.node[0]) > exeNode(p->op.node[1]);
case GE: return exeNode(p->op.node[0]) >= exeNode(p->op.node[1]);
case LE: return exeNode(p->op.node[0]) <= exeNode(p->op.node[1]);
case NE: return exeNode(p->op.node[0]) != exeNode(p->op.node[1]);
case EQ: return exeNode(p->op.node[0]) == exeNode(p->op.node[1]);
case AND: return exeNode(p->op.node[0]) && exeNode(p->op.node[1]);
case OR: return exeNode(p->op.node[0]) || exeNode(p->op.node[1]);
}
}
return 0;
}
这个文件是对语法树的解释分析。只包含一个递归函数exeNode。首先分树结点类型,
再根据操作符一一判定动作。
五、结束
bison -d lexya_e.y
lex lexya_e.l
gcc -g -o parser lex.yy.c lexya_e.tab.c parser.c
编译即可测试执行。
多说无益,个人感觉自已领会的东西才是最要的。本示例包含了一些C语法,不过也只是
一小部分,在后续的文章中将逐步扩展本例的功能,从而发掘yacc和lex的强大功能。
<注:本文示例参考Tom Niemann的《A Compact Guide to Lex & Yacc》
http://epaperpress.com/lexandyacc/index.html>