算符优先分析用来处理表达式非常便捷,甚至你可以忘记算术运算相关的一切产生式。对于算符优先分析来说最重要的东西有三:运算符的优先级、元和结合方式。优先级在任务布置时已经讲过了;元很简单,一般运算符都是二元的,只有正号和负号是一元的;结合方式一般分两种,普通的运算都是左结合的,赋值和乘幂运算是右结合的,而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;
}
就行了,而如果该分析器没有正常返回——就把错误处理给那个分析器做吧。
这一篇文章中很多代码都是未完成的,因为分析器不是一个独立的结构,牵扯到很多控制结构和其他分析器。不过我会尽快给出一个方案让算符优先分析尽可能地独立出来进行测试,以减少语法分析模块整体的测试难度。
分享到:
相关推荐
LR分析器从左向右扫描输入字符串,并自底向上地进行语法分析,旨在判断输入字符串是否符合文法的语句。以下是关于LR分析器设计的一些关键知识点: 1. **LR分析法的基本思想**: LR分析器的核心在于LR(k)分析器,它...
如果对于某个组合,有多个产生式可选,则该文法不是LL(1)文法,无法构建LL1分析器。 5. **测试与优化**:分析表可能会包含冲突,即对于特定状态和输入,可能有多个解析动作。需要消除这些冲突,以确保分析器能正确...
CFG由一组产生式规则构成,每个规则描述了一个非终结符如何可以被其他非终结符和/或终结符替换。 6. **抽象语法树(AST)**:语法分析的结果通常是一个抽象语法树,它是源代码语法结构的树形表示。AST使得后续的...
这个表包含了每个非终结符在当前输入符号下应该采取的动作,要么进行 shift 操作(即读入下一个输入符号),要么进行 reduce 操作(根据产生式合并符号)。这种分析方法简洁且效率高,适用于许多编程语言的解析。 ...
这里的“1”意味着分析器只需要查看输入串的下一个符号就能决定接下来的行动,无需看更多。这种解析方法简单、高效,但对文法的要求较高,只能处理无回溯的递归下降文法。 LL(1)解析的关键在于构造解析表。这个表...
LR分析器的关键在于,它可以根据栈中的“历史”(已经扫描过的符号)和“展望”(未来可能出现的符号)来确定下一步操作,无需回溯,这提高了分析效率。 LR分析器的工作流程通常包括以下步骤: 1. 初始化:分析器...
任务是设计并实现一个针对算术表达式的语法及语义分析程序,该程序需能够进行词法检查、语法分析,并生成逆波兰式中间代码。 首先,我们需要定义算术表达式的文法。这里采用一种简单的上下文无关文法(Context-Free...
LL(1)文法是一种特殊的自上而下的分析方法,其中"1"表示分析器只需查看输入序列的一个字符(即"1"个输入符号的预测),就能确定应使用哪个产生式。它要求: - **每个产生式的右部要么由终结符开始**,保证分析过程...
1. **编译器设计**:编译器中的词法分析和语法分析阶段常使用LR(0)算法。 2. **程序语言设计**:在设计新的编程语言时,为了保证语言的高效性和简洁性,LR(0)算法可以用来验证语法的正确性。 3. **代码生成与优化**...
LR分析法(Lookahead Rightmost Derivation)基于上下文无关文法,通过分析栈和ACTION表、GOTO表来决定是移进(Shift)输入符号还是归约(Reduce)产生式。LR(0)分析器不需考虑输入符号的前瞻,而更复杂的LR(1)、...
LR(0)分析法是一种编译原理中用于语法分析的方法,它基于一种特殊的有限状态自动机,能够根据当前分析栈中的信息决定分析器的动作,而无需查看输入串的后续符号。 设计目的主要在于理解LR(0)分析法的核心概念,并...
语法制导翻译是在语法分析过程中,根据每个产生式所对应的语义子程序进行翻译的方法。这种方法将语法分析和语义处理紧密结合起来,使得编译器在识别程序结构的同时,能够立即执行相应的语义处理动作,如类型检查、...
2. **输入任意文法**:用户可以输入自定义的上下文无关文法,这通常涉及到文法规则的定义,例如非终结符、终结符和产生式规则。这种灵活性使得该工具能够分析各种各样的语法结构,而不局限于预设的特定语言。 3. **...
- **空产生式**:无需特殊处理。 - **终结符号与非终结符号**:终结符号匹配后移动到下一个符号,非终结符号调用相应函数。 **实验过程和指导** 1. **准备阶段**:阅读相关教材,设计解决方案,规划模块结构,并...
5. **词法分析器输出**:词法分析器的输出是单词的种别编码和自身值,提供给后续的语法分析阶段。 6. **正规式与等价**:正规式M1和M2等价意味着它们识别的语言集合相同。正规形式的等价性是正则表达式理论的基础。...
LL(1) 文法可以通过简单的预测分析算法来实现语法分析,且无需回溯。判断一个文法是否为LL(1)的关键在于检查是否存在冲突。 - **LR(0) 文法**: LR(0) 文法是一类能够通过简单的自下而上分析技术来解析的语言。LR(0) ...
在分析过程中,如果当前输入符号与栈顶的非终结符形成了一个右部为单个符号的产生式,那么就会进行归约。这种技术使得编译器能够正确处理复杂的语法结构,确保源代码符合语法规则。 在课程设计中,学生很可能使用了...
3. **语法设计**:利用产生式(BNF文法)来描述C语言的语法规则,这是语法分析阶段,确保输入的源代码符合语言的结构规则。 4. **解释器编写**:通过递归下降解析技术,编写语言的解释器。解释器直接执行源代码,而...
语法分析阶段,编译器依据语法规则,将词法分析产生的标记流组织成抽象语法树(Abstract Syntax Tree, AST)。对于布尔表达式,例如 "A AND (B OR NOT C)",抽象语法树会反映出其结构,便于进一步的处理。C++ ...