算符优先分析用来处理表达式非常便捷,甚至你可以忘记算术运算相关的一切产生式。对于算符优先分析来说最重要的东西有三:运算符的优先级、元和结合方式。优先级在任务布置时已经讲过了;元很简单,一般运算符都是二元的,只有正号和负号是一元的;结合方式一般分两种,普通的运算都是左结合的,赋值和乘幂运算是右结合的,而Jerry不支持乘幂运算。简单的表达式求值算法可以参考这篇文章,文中相关实现也是基于该算法的。
算符分析器需要两个栈,一个用来存放符号,一个用来存放操作数,因此该结构可以这样实现
/* datastruct.h */ struct OperationAnalyser { memberSyntaxAnalyser int needFactor; struct Stack* opStack; struct Stack* numStack; };
而needFactor则表示该算符当前应该遭遇一个因子(needFactor == 1时)还是一个运算符(needFactor == 0时)。
算符分析器的构造函数可以这样实现:
void consumeToken_OpAna(void* self, struct Token* token); void consumeNonTerminal_OpAna(void* self, struct AbstractSyntaxNode* token); struct OperationAnalyser* newOperationAnalyser(void) { struct OperationAnalyser* opana = (struct OperationAnalyser*) allocate(sizeof(struct OperationAnalyser)); opana->needFactor = 1; opana->numStack = newStack(); opana->opStack = newStack(); // 将一个左括号压入栈顶 opana->consumeToken = consumeToken_OpAna; opana->consumeNonTerminal = consumeNonTerminal_OpAna; return opana; }
上一节数据结构中,在算符优先分析器结构中有个神秘的needFactor成员我并没有给出注释,现在补述一下:这个成员指出接下来一个词法分析得到的Token是否应该某个因子的一部分,特别是读入"+"或"-",只有借助它来判别是正负号还是加减号。另外,它在算符优先分析中可以为错误处理提供强力帮助:由于一个算符优先分析器要么立即需要一个因子,要么立即需要一个运算符,所以词法分析获取一个Token传入该分析器以后,可以查询该Token的类型是否属于因子或者算符的First集合来判别当前Token类型是否正确。在这里
First( 因子 ) = { PLUS, MINUS, INTEGER, REAL, LPARENT, IDENT }
First( 算符 ) = { 各种运算符 }
当然不必担心两者交集不为空会导致什么故障发生,这里并不是先判断该符号属于哪个集合,而是先看needFactor再分开分析,即OperationAnalyser的consumeToken函数可以实现为一个中转函数:
void consumeToken_OpAna(void* self, struct Token* token) { struct OperationAnalyser* opana = (struct OperationAnalyser*)self; if(opana->needFactor) { consumeFactor(opana, token); } else { consumeOperator(opana, token); } }
不过,有一个例外,就是所有左置的一元运算符,如NOT,它们的出现也会打乱区分工作,因此它们都应该看作 First( 因子 ) 中的成员,如consumeFactor的一种实现方式:
void consumeFactor(struct OperationAnalyser* self, struct Token* token) { if(NOT == token->type) { self->opStack->push(self->opStack, newOperator(token->type, /* 未实现:对应的优先级数 */ -1, unaryOperate)); self->needFactor = 1; } else if(MINUS == token->type || PLUS == token->type) { // 将 MINUS 或 PLUS 作为一元运算符入栈 self->needFactor = 1; } else if(IDENT == token->type) { // 弄一个识别变量的分析器 varAna 扔到 分析器栈 栈顶 // varAna->consumeToken(varAna, token); return; } else if(INTEGER == token->type) { self->numStack->push(self->numStack, newIntegerNode(atoi(token->image))); self->needFactor = 0; } else if(REAL == token->type) { self->numStack->push(self->numStack, newRealNode(atof(token->image))); self->needFactor = 0; } else if(LPARENT == token->type) { // 正括号入栈 self->needFactor = 1; } else { // 报错 } }
这里有个小问题,就是将MINUS或PLUS作为一元运算符入栈时,栈内存放的是什么?如果是一个Token*,那么一来信息太少,以后从栈中取出操作符时就无从知道它到底是一元操作符还是二元的,另外,对于提供Token*的函数,它必须知道这个Token*是否还能重用,比如如果压入的是数,那么重用是没问题的,因为Token*已经被变成了IntegerNode或RealNode,而符号就不一样,这样增加了词法的复杂度。因此,需要另一种数据结构。
struct Operator { void (*operate)(struct Operator*, struct Stack*); AcceptType op; int priority; int rightCombination; }; struct Operator* newOperator(AcceptType op, int priority, void (*operate)(struct Operator*, struct Stack*)); void nullOperate(struct Operator*, struct Stack*); void unaryOperate(struct Operator*, struct Stack*); void binaryOperate(struct Operator*, struct Stack*);
其中,op表示算符类型,priority表示算符优先级数,越小优先级越高,rightCombination表示是否右结合,operate成员函数则指出该算符应该如何进行运算,它应该是nullOperate(非运算),unaryOperate(一元运算),binaryOperate(二元运算)三者之一。newOperator则提供了一个构造函数。这些劳什子的可以这样实现:
struct Operator* newOperator(AcceptType op, int priority, void (*operate)(struct Operator*, struct Stack*)) { struct Operator* oper = (struct Operator*)allocate(sizeof(struct Operator)); oper->op = op; oper->priority = priority; oper->rightCombination = (ASSIGN == op); // ... 暂时就这么办吧 oper->operate = operate; return oper; } void nullOperate(struct Operator* oper, struct Stack* numStack) { revert(oper); } void unaryOperate(struct Operator* oper, struct Stack* numStack) { struct AbstractValueNode* value; if(MINUS == oper->op) { value = (struct AbstractValueNode*)(numStack->pop(numStack)); numStack->push(numStack, newOperationNode(oper->op, (struct AbstractValueNode*)newIntegerNode(0), value)); } revert(oper); } void binaryOperate(struct Operator* oper, struct Stack* numStack) { struct AbstractValueNode* left,* right; right = (struct AbstractValueNode*)(numStack->pop(numStack)); left = (struct AbstractValueNode*)(numStack->pop(numStack)); numStack->push(numStack, newOperationNode(oper->op, left, right)); revert(oper); }
这样一来consumeFactor中某些分支可以这样写:
if(MINUS == token->type || PLUS == token->type) { self->opStack->push(self->opStack, newOperator(token->type, 0, unaryOperate)); self->needFactor = 1; } // ... ... if(LPARENT == token->type) { self->opStack->push(self->opStack, newOperator(token->type, 0x7fffffff, /* max integer */ nullOperate)); self->needFactor = 1; }
接下来是consumeOperator的实现。为了不让优先级成为一个头痛的问题,这里弄一个优先级表,它的顺序对应于AcceptType中各符号:
const int PRIORITY[] = { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 2, 1, 1, 5, 3, 3, 3, 3, 3, 3, 5, 6, 4, 0, 0, 0x7fffffff, 0, 0, 0, 0, 0 };
这里面的0不是优先级最高的意思了(优先级数为0的正负号在构造时是手动设置的),而是表示不应该在这时出现这样的符号,当然有一个例外,就是反括号。consumeOperator可以这样部分地实现:
void consumeOperator(struct OperationAnalyser* self, struct Token* token) { int priority = PRIORITY[token->type]; if(0 < priority && priority < PRIORITY[LPARENT]) { int push = 0; struct Operator* topOp = (struct Operator*) (self->opStack->peek(self->opStack)); push |= (priority < topOp->priority); push |= (priority == topOp->priority && topOp->rightCombination); while(!push) { topOp = (struct Operator*)(self->opStack->pop(self->opStack)); topOp->operate(topOp, self->numStack); topOp = (struct Operator*)(self->opStack->peek(self->opStack)); push |= (priority < topOp->priority); push |= (priority == topOp->priority && topOp->rightCombination); } self->opStack->push(self->opStack, newOperator(token->type, priority, binaryOperate)); self->needFactor = 1; } else if(RPARENT == token->type) { struct Operator* topOp = (struct Operator*) (self->opStack->pop(self->opStack)); while(nullOperate != topOp->operate) { // 注1 topOp->operate(topOp, self->numStack); topOp = (struct Operator*)(self->opStack->pop(self->opStack)); } topOp->operate(topOp, self->numStack); if(0 == self->opStack->getSize(self->opStack)) { // 注2 // 报错 } self->needFactor = 0; } else { // 终止 } }
前面读到一般运算符时进行的分析,思路还算清晰;后面注出的两个条件,可能形式上不是很确切。
注1:nullOperate != topOp->operate 实际上是在查看栈顶运算符是否是正括号,因为只有正括号的operate函数才为nullOperate
注2:0 == self->opStack->getSize(self->opStack) 判断栈底默认压入的正括号是否被弹出,如果被弹出则说明正反括号实际上是不配对的(反括号多1)。
此外,这个循环
push |= (priority < topOp->priority); push |= (priority == topOp->priority && topOp->rightCombination); while(!push) { topOp = (struct Operator*)(self->opStack->pop(self->opStack)); topOp->operate(topOp, self->numStack); topOp = (struct Operator*)(self->opStack->peek(self->opStack)); push |= (priority < topOp->priority); push |= (priority == topOp->priority && topOp->rightCombination); }
其实你可以把它当作一个for循环来理解,push这个量为1时,表示当前传入的这个运算符应该被压入栈中。
最后,读到不正确的符号时,并不能说明分析出错,也很可能是因为一个表达式已经接近完成并应该返回给分析器栈次栈顶的分析器了。
最后大致提一下consumeNonTerminal_OpAna这个函数,其实它非常简单,因为算符优先分析只会委托另一分析器为之提供一个VariableNode,因此只要该分析器正确返回,那么只需
void consumeNonTerminal_OpAna(void* self, struct AbstractSyntaxNode* node) { struct OperationAnalyser* opana = (struct OperationAnalyser*)self; opana->numStack->push(opana->numStack, node); opana->needFactor = 0; }
就行了,而如果该分析器没有正常返回——就把错误处理给那个分析器做吧。
这一篇文章中很多代码都是未完成的,因为分析器不是一个独立的结构,牵扯到很多控制结构和其他分析器。不过我会尽快给出一个方案让算符优先分析尽可能地独立出来进行测试,以减少语法分析模块整体的测试难度。