从0开始自制解释器——添加对乘除法的支持
在上一篇中,我们实现了对减法的支持,并且介绍了语法图。针对简单的语法进行描述,用语法图描述当然是没问题的。但是针对一些复杂的语法进行描述,如果每个部分都通过语法图来描述就显得有些繁琐了。这篇我们先介绍另一种描述语法的方式,并进一步介绍一些关于语法分析的知识。
BNF范式与上下文无关文法
巴科斯范式 以美国人巴科斯(Backus)和丹麦人诺尔(Naur)的名字命名的一种形式化的语法表示方法,用来描述语法的一种形式体系,是一种典型的元语言。又称巴科斯-诺尔形式(Backus-Naur form)。它不仅能严格地表示语法规则,而且所描述的语法是与上下文无关的。它以递归方式描述语言中的各种成分,凡遵守其规则的程序就可保证语法上的正确性。它具有语法简单,表示明确,便于语法分析和编译的特点。
BNF表示语法规则的方式为:非终结符用尖括号括起。每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以“::=”分开。具有相同左部的规则可以共用一个左部,各右部之间以直竖“|”隔开。所谓非终结符就是语言中某些抽象的概念不能直接出现在语言中的符号,终结符就是可以直接出现在语言中的符号。
其实这些都是一些官话,初看起来只觉得拗口和难以理解,但是它的形式非常简单。它主要是用下面几个符号来表达含义
- 使用<>来表示必须包含的部分
- 使用[]来表示可选部分
- 使用{}来表示可以重复0次或者无数次
- 使用|来表示左右两边任选一部分,相当于OR
- 使用::=来表示被定义为
现在来给出具体的例子,我们都看过《西游记》,里面的取经4人组包括唐僧、孙悟空、猪八戒和沙僧。使用BNF范式进行定义,可以写成 <取经团队>::=<唐僧><孙悟空><猪八戒><沙僧>
。
我们再来举一个例子,我们知道一个文章由若干个段落组成、一个段落由若干个句子组成、一个句子由符合一定语法规则的汉字组成并且以句号作为结尾。我们简单的将句子的语法规则定义为主谓宾三个部分。而这里的主谓宾我们简单的用一些名词和动词来定义。因此这里的一系列结构可以定义为如下内容
<文章>::={<段落>}
<段落>::={<句子>}
<句子>::=<主语><谓语><宾语>。
<主语>::=人|狗|猫|天
<谓语>::=吃|抓|下
<宾语>::=饭|雨|肉|鱼
根据这个表达式我们很容易的推出类似 人吃饭。
、天下雨。
、猫抓鱼。
这样的句子。相信到这里小伙伴应该明白BNF范式的一些基本概念和使用方式了。
我们再来插入一个题外话,既然这里提到BNF范式是一种上下文无关文法,那什么是上下文、什么是上下文无关。先别着急了解概念,我们仍然通过例子来说明。在上述的句子的定义中,我们一共可以生成 4 * 3 * 4 = 48
种 结果,我们可以获得类似 人吃饭。
、猫抓鱼。
这种有意义的句子,也可能产生像天吃鱼。
、人下雨
这种读起来感觉别扭的非正常语句。但是在上下文无关的语法中,主语宾语和谓语的内容没有相互关联,也就是说谓语和宾语的产生与主语无关。那上下文有关的文法呢?这里为了产生一些有意义的句子,我们给它加上一些限定。例如人
后面只能接 吃
抓
作为谓语、而当吃作为谓语时只能将 饭
、肉
、鱼
作为宾语。针对这种需求,我们可以进行如下定义
<句子>::=<主语><谓语><宾语>。
<主语>::=人|狗|猫|天
人<谓语>::=人(吃|抓)
吃<宾语>::=吃(饭|肉|鱼)
这样我们对这个产生式进行了一些限定,当主语是人的时候,谓语只能产生吃和抓这样的宾语。这种情况下的描述就被称之为上下文有关。上下文无关我自己的理解就是后续表达式的产生不依赖前面已产生的内容。而上下文有关的含义则与之相法。这个上下文就跟我们这么多年阅读理解题里面写的“请根据上下文来理解某个词表达了作者怎样的心情”这里的上下文类似。
当然更加规范的说法就是,在应用一个产生式进行推导时,前后已经推导出的部分结果就是上下文。上下文无关就是只要文法的定义里面有一个定义,不管前面的产生串是什么都可以应用相应的产生推导后面的内容。
代码编写
上面的定义只是开胃菜,希望通过上面的描述,小伙伴能够理解BNF范式的应用,至于上下文无关和上下文有关。这些暂时不用考虑,毕竟我们目前还是在做上下文无关文法相关的内容。
这里我们要支持乘法和除法,首先要做的就是在 ETokenType
结构中添加对乘法和除法相关的定义
typedef enum e_TokenType
{
CINT = 0, //整数
PLUS, //加法
MINUS, //减法
DIV, //乘法
MUL, //除法
END_OF_FILE // 字符串末尾结束符号
}ETokenType;
接着在 get_next_token
和 get_oper()
函数中添加对这两个运算符的支持
// get_next_token
else if (c == '*')
{
pToken->type = DIV;
dyncstring_catch(&pToken->value, '*');
}
else if (c == '/')
{
pToken->type = MUL;
dyncstring_catch(&pToken->value, '/');
}
// get_oper
if (get_next_token(&token) && (token.type == PLUS || token.type == MINUS || token.type == DIV || token.type == MUL))
{
oper = token.type;
if (pRet)
*pRet = true;
}
现在词法分析部分已经可以支持乘除法的符号解析了。接着来完成语法分析的部分。首先我们来定义一下这个简单计算器的文法。
<expr>::=<term>{<oper><term>}
<term>::={0|1|2|3|4|5|6|7|8|9}
<oper>::=PLUS|MINUS|DIV|MUL
回忆一下上一节给出的语法图,理解这个表达式并不算困难。但是这里我们定义的文法有一个问题,就是从文法上体现不出运算的优先级。学过小学数学的都知道算数运算中优先计算乘除法,最后算加减法。但是根据这个文法我们无法体现出乘除法的优先级。因此这里我们需要修改定义。优先计算乘除法在文法上可以理解成,乘除法单独成一个部分,我们获取这个部分的计算结果最后与其他部分一起计算加减法。用BNF范式来体现就是
<expr>::=<term>{(PLUS|MINUS)<term>}
<term>::=<factor>{(DIV|MUL)<factor>}
<factor>::={0|1|2|3|4|5|6|7|8|9}
与语法图类似,范式也可以很容易转化为代码。允许出现多次的我们在代码实现上体现为循环。而文法中相关的定义我们直接采用一些get方式来获取对应被打上标记的值即可。上述文法描述可以转化为如下的c
代码
int expr()
{
bool bRet = false;
int result = get_term(&bRet);
int bEOF = false;
do
{
ETokenType oper = get_oper(&bRet);
switch (oper)
{
case PLUS:
{
int num = get_term(&bRet);
if(bRet)
result += num;
}
break;
case MINUS:
{
int num = get_term(&bRet);
if(bRet)
result -= num;
}
break;
case END_OF_FILE:
printf("%d\n", result);
bEOF = true;
break;
default:
bRet = false;
break;
}
} while (bRet && !bEOF);
if (!bRet)
{
printf("Syntax Error!\n");
}
return 0;
}
上述expr
的定义就是由一个term
加若干个 +|-
和后面的若干个term
来组成,因此这里有一个循环。来取出所有term
和所有加减法,并进行计算。
int get_term(bool* pValid)
{
int result = get_factor(pValid);
int bEOF = false;
do
{
ETokenType oper = get_oper(pValid);
switch (oper)
{
case DIV:
{
int num = get_factor(pValid);
if (*pValid)
result *= num;
}
break;
case MUL:
{
int num = get_factor(pValid);
if (*pValid)
result /= num;
}
break;
case PLUS:
case MINUS:
{
g_pPosition--;
bEOF = true;
}
break;
case END_OF_FILE:
{
g_pPosition--;
bEOF = true;
}
}
} while (pValid && !bEOF);
return result;
}
而term
则是由整数以及若干个乘除法和另一个整数组成,所以代码中也用循环来取一直到取到不是这个term
定义所组成的部。注意这里与之前一样,当取到term的结束部分,我们仍然需要将索引进行递减。
而最终的oper
和 factor
则保持原来的算法不变。
好了,本篇到此也就结束了,小伙伴可以到该位置 取出代码来进行阅读和修改。