`
NeuronR
  • 浏览: 59843 次
  • 性别: Icon_minigender_1
  • 来自: 武汉
社区版块
存档分类
最新评论

[语法分析]无需产生式的分析器

阅读更多

    算符优先分析用来处理表达式非常便捷,甚至你可以忘记算术运算相关的一切产生式。对于算符优先分析来说最重要的东西有三:运算符的优先级、元和结合方式。优先级在任务布置时已经讲过了;元很简单,一般运算符都是二元的,只有正号和负号是一元的;结合方式一般分两种,普通的运算都是左结合的,赋值和乘幂运算是右结合的,而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再分开分析,即OperationAnalyserconsumeToken函数可以实现为一个中转函数:

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 {
        // 报错
    }
} 

这里有个小问题,就是将MINUSPLUS作为一元运算符入栈时,栈内存放的是什么?如果是一个Token*,那么一来信息太少,以后从栈中取出操作符时就无从知道它到底是一元操作符还是二元的,另外,对于提供Token*的函数,它必须知道这个Token*是否还能重用,比如如果压入的是数,那么重用是没问题的,因为Token*已经被变成了IntegerNodeRealNode,而符号就不一样,这样增加了词法的复杂度。因此,需要另一种数据结构。

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;
}

就行了,而如果该分析器没有正常返回——就把错误处理给那个分析器做吧。

 

    这一篇文章中很多代码都是未完成的,因为分析器不是一个独立的结构,牵扯到很多控制结构和其他分析器。不过我会尽快给出一个方案让算符优先分析尽可能地独立出来进行测试,以减少语法分析模块整体的测试难度。

分享到:
评论
7 楼 NeuronR 2009-01-27  
lwwin 写道

那取栈顶的代码是不是还没有^^?

        int push = 0;  
        struct Operator* topOp = (struct Operator*)  
                                         (self->opStack->getTop(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->getTop(self->opStack));  
            push |= (priority < topOp->priority);  
            push |= (priority == topOp->priority && topOp->rightCombination);  
        }  
        self->opStack->push(self->opStack, newOperator(token->type,  
                                                       priority,  
                                                       binaryOperate));

一切都在这个循环里面。
循环开头第一句
topOp = (struct Operator*)(self->opStack->pop(self->opStack));
弹出栈顶操作符,接着运算。而第三句取栈顶
topOp = (struct Operator*)(self->opStack->getTop(self->opStack));
但是不弹出它,这样做是为了确定push的值。
6 楼 lwwin 2009-01-24  
那取栈顶的代码是不是还没有^^?
5 楼 NeuronR 2009-01-24  
lwwin 写道

// 将一个左括号压入栈顶
这句话的意思明白了,提个建议,为实现的用类似于VC的//TODO:标记,这样不会费解^^,当时我把这句注释加了问,因为不解^^

第一个问题仔细看明白你!PUSH的意思了,应该是对高优先级的算符进行结合为非终结符并且被压入数字栈(才注意到有二个栈在操作)

现在只剩一个问题,就是超前看一位的呈现还不明朗,出现二元算符的时候,
例如 a + b * c,其中看到*的时候,根据优先级判断 push == 0
此时构造 binary ( MUL, b, c ) 但是c是不是还没有读入呢?

我再看看,这次修补的东西多了点^^

好,以后用 // TODO

至于运算符栈的操作,对于二元运算符,确实是在没有读入右侧操作数时就判定是否该进行运算。但是,这里的运算并不是指当前读入的符号,而是当前栈顶的符号,如你给的例子,在读入
a + b *
时,刚刚读入的 * 号还未入栈。即使判定此时应该运算,也不是建立一个binary ( MUL, b, c ),而是 binary ( ADD, a, b )
4 楼 lwwin 2009-01-23  
// 将一个左括号压入栈顶
这句话的意思明白了,提个建议,为实现的用类似于VC的//TODO:标记,这样不会费解^^,当时我把这句注释加了问,因为不解^^

第一个问题仔细看明白你!PUSH的意思了,应该是对高优先级的算符进行结合为非终结符并且被压入数字栈(才注意到有二个栈在操作)

现在只剩一个问题,就是超前看一位的呈现还不明朗,出现二元算符的时候,
例如 a + b * c,其中看到*的时候,根据优先级判断 push == 0
此时构造 binary ( MUL, b, c ) 但是c是不是还没有读入呢?

我再看看,这次修补的东西多了点^^
3 楼 NeuronR 2009-01-22  
lwwin 写道

还有一个,不知道算法分析例程是单个( )括号处理还是多个,感觉可以支持多个的
就是(a + (b + c))的情况,但是这句我(注2那句)
if(0 == self->opStack->getSize(self->opStack)) {
的意思是只支持单个配对么?

注意到第一块代码中有一行注释:压入一个正括号到栈顶。实际上表达式不会总被正括号包起来——没哪个程序员会有这样的风格而且甚至在Java里面这样会被认为是语法错误。

压入一个正括号是一种策略,首先,因为有一个正括号“垫底”,那么不必每次传入运算符时都去判断符号栈是否为空;第二,当程序书写发生错误时,特别是多出一个反括号时,它会与这个虚假的正括号匹配,这时符号栈为空,就认为错误发生了。

支持括号嵌套是必须的,这一点要坚持不动摇。我想你是不是在问这一点在哪里实现。其实,当出现反括号时,符号栈里面的符号会顺序弹出,依次运算,直到遇到一个正括号为止,但是,词法分析一次只会返回一个符号,即使反括号连续出现,一次也只会匹配一个正括号。如你给的例子
(a + (b + c))
输入到
(a + (b + c
时,符号栈(第一个左括号是垫底左括号)和数栈情况如下:
( ( + ( +
a b c
这时读入第一个反括号,于是弹出栈顶加法进行运算,结果假设为x,压回栈顶,再弹出一个符号,发现它是左括号,于是运算终止,算符分析函数返回,栈里变成这样:
( ( +
a x
这时读入第二个反括号,于是又弹出加号,运算一次(设结果为y),再弹出正括号,又一次匹配结束返回:
(
y
这时加入后面紧跟了一个反括号(也就是多余的反括号),它会匹配掉栈底的正括号。
注意,准确地说,这时仍然没有错误产生,因为if语句或者while语句的条件有一个反括号结尾,如
if( a == b )

这个额外的反括号实际上不应该让算符优先分析器来处理,因此接下来不应该是简单的报错,而应该这样:
-  弹出分析器栈栈顶(就是该算符优先分析器本身)
-  将算符优先分析器此时数栈栈顶元素 num 传递给现在的分析器栈栈顶的 consumeNonTerminal 函数
-  将反括号传递给现在的分析器栈顶的 consumeToken 函数

lwwin 写道

感觉用OBJC看起来还是比较吃力啊= =不如C++的VIRTUAL来的轻松

另外:比如分析 a + b - c的-的时候,应该是优先级相同且左结合的时候应该归并,
于是可能是:
priority == topOp->priority &amp;&amp; !topOp->rightCombination
而不是
priority == topOp->priority &amp;&amp; topOp->rightCombination
吧?


well... push为1的意思是把当前符号压入栈,或许你理解错了以为是让栈顶符号进行运算?

很感谢你对我的支持,同时很抱歉,最近的两篇文章——这一篇,以及之前的那一篇[语法分析]语法树及分析器数据结构——里面有一些错误,它们是:
*  这一篇文章中的优先级表
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,
    4, 4, 4,
    0, 0, 0x7fffffff, 0, 0, 0, 0, 0
};

与或非对应的优先数曾经是一样的,现在它们已经被修改正确了。
*  这一篇文章中的
consumeFactor

曾经的实现中并没有提到将NOT(即符号 ! )也考虑为因子的开头,现在这一情况已经被加入了(这样多出一个if-else分支,导致代码变得更加狰狞了:-(),此外对应的文字说明也有了修改。
*  上一篇文章中,由于我贴的代码是我曾经的一个很蹩脚的代码版本(sorry, 我写这玩意时没有用版本控制,导致现在手头上代码版本只有3个:词法分析完成版,全部完成版,以及刚才说的那个错误版本),而导致有两个数据结构被遗忘了,它们是
BinaryOperationNode
UnaryOperationNode
而那个错误的就是
OperationNode
实际上这三个数据结构之间并不构成任何继承关系,现在最后那个已经被删除并被前两个替代了。对应了,那些实现代码也都改变了。

如果你在我的代码中还看到任何有问题的地方或含糊不清的地方(我写注释很懒的),请及时回复我,或者发送邮件联系我。
lene13 @ gmail . com
2 楼 lwwin 2009-01-22  
还有一个,不知道算法分析例程是单个( )括号处理还是多个,感觉可以支持多个的
就是(a + (b + c))的情况,但是这句我(注2那句)
if(0 == self->opStack->getSize(self->opStack)) {
的意思是只支持单个配对么?
1 楼 lwwin 2009-01-22  
感觉用OBJC看起来还是比较吃力啊= =不如C++的VIRTUAL来的轻松

另外:比如分析 a + b - c的-的时候,应该是优先级相同且左结合的时候应该归并,
于是可能是:
priority == topOp->priority && !topOp->rightCombination
而不是
priority == topOp->priority && topOp->rightCombination
吧?

相关推荐

    lR语法分析器设计.pdf

    LR分析器从左向右扫描输入字符串,并自底向上地进行语法分析,旨在判断输入字符串是否符合文法的语句。以下是关于LR分析器设计的一些关键知识点: 1. **LR分析法的基本思想**: LR分析器的核心在于LR(k)分析器,它...

    LL1语法分析器3(WINDOW).rar

    如果对于某个组合,有多个产生式可选,则该文法不是LL(1)文法,无法构建LL1分析器。 5. **测试与优化**:分析表可能会包含冲突,即对于特定状态和输入,可能有多个解析动作。需要消除这些冲突,以确保分析器能正确...

    语法分析程序

    CFG由一组产生式规则构成,每个规则描述了一个非终结符如何可以被其他非终结符和/或终结符替换。 6. **抽象语法树(AST)**:语法分析的结果通常是一个抽象语法树,它是源代码语法结构的树形表示。AST使得后续的...

    ll预测分析程序(语法分析)

    这个表包含了每个非终结符在当前输入符号下应该采取的动作,要么进行 shift 操作(即读入下一个输入符号),要么进行 reduce 操作(根据产生式合并符号)。这种分析方法简洁且效率高,适用于许多编程语言的解析。 ...

    编译原理LL1语法分析

    这里的“1”意味着分析器只需要查看输入串的下一个符号就能决定接下来的行动,无需看更多。这种解析方法简单、高效,但对文法的要求较高,只能处理无回溯的递归下降文法。 LL(1)解析的关键在于构造解析表。这个表...

    编译原理报告二LR分析器参考.pdf

    LR分析器的关键在于,它可以根据栈中的“历史”(已经扫描过的符号)和“展望”(未来可能出现的符号)来确定下一步操作,无需回溯,这提高了分析效率。 LR分析器的工作流程通常包括以下步骤: 1. 初始化:分析器...

    编译原理课程设计_算术表达式的语法分析及语义分析程序设计.doc

    任务是设计并实现一个针对算术表达式的语法及语义分析程序,该程序需能够进行词法检查、语法分析,并生成逆波兰式中间代码。 首先,我们需要定义算术表达式的文法。这里采用一种简单的上下文无关文法(Context-Free...

    电脑基础知识LL文法及其分析程序PPT课件.pptx

    LL(1)文法是一种特殊的自上而下的分析方法,其中"1"表示分析器只需查看输入序列的一个字符(即"1"个输入符号的预测),就能确定应使用哪个产生式。它要求: - **每个产生式的右部要么由终结符开始**,保证分析过程...

    LR(0)算法分析文档

    1. **编译器设计**:编译器中的词法分析和语法分析阶段常使用LR(0)算法。 2. **程序语言设计**:在设计新的编程语言时,为了保证语言的高效性和简洁性,LR(0)算法可以用来验证语法的正确性。 3. **代码生成与优化**...

    赋值语句的LR分析方法翻译程序设计

    LR分析法(Lookahead Rightmost Derivation)基于上下文无关文法,通过分析栈和ACTION表、GOTO表来决定是移进(Shift)输入符号还是归约(Reduce)产生式。LR(0)分析器不需考虑输入符号的前瞻,而更复杂的LR(1)、...

    数据结构课设报告

    LR(0)分析法是一种编译原理中用于语法分析的方法,它基于一种特殊的有限状态自动机,能够根据当前分析栈中的信息决定分析器的动作,而无需查看输入串的后续符号。 设计目的主要在于理解LR(0)分析法的核心概念,并...

    编译原理小抄

    语法制导翻译是在语法分析过程中,根据每个产生式所对应的语义子程序进行翻译的方法。这种方法将语法分析和语义处理紧密结合起来,使得编译器在识别程序结构的同时,能够立即执行相应的语义处理动作,如类型检查、...

    编译原理-LR分析

    2. **输入任意文法**:用户可以输入自定义的上下文无关文法,这通常涉及到文法规则的定义,例如非终结符、终结符和产生式规则。这种灵活性使得该工具能够分析各种各样的语法结构,而不局限于预设的特定语言。 3. **...

    编译原理递归下降分析法

    - **空产生式**:无需特殊处理。 - **终结符号与非终结符号**:终结符号匹配后移动到下一个符号,非终结符号调用相应函数。 **实验过程和指导** 1. **准备阶段**:阅读相关教材,设计解决方案,规划模块结构,并...

    编译原理期末考试题目及答案.doc

    5. **词法分析器输出**:词法分析器的输出是单词的种别编码和自身值,提供给后续的语法分析阶段。 6. **正规式与等价**:正规式M1和M2等价意味着它们识别的语言集合相同。正规形式的等价性是正则表达式理论的基础。...

    烟台大学文经学院2018.12编译原理考试

    LL(1) 文法可以通过简单的预测分析算法来实现语法分析,且无需回溯。判断一个文法是否为LL(1)的关键在于检查是否存在冲突。 - **LR(0) 文法**: LR(0) 文法是一类能够通过简单的自下而上分析技术来解析的语言。LR(0) ...

    编译原理课程设计及报告

    在分析过程中,如果当前输入符号与栈顶的非终结符形成了一个右部为单个符号的产生式,那么就会进行归约。这种技术使得编译器能够正确处理复杂的语法结构,确保源代码符合语法规则。 在课程设计中,学生很可能使用了...

    基于java实现的C语言编译器(编译原理大作业)同时包含课件、往年真题【100012213】

    3. **语法设计**:利用产生式(BNF文法)来描述C语言的语法规则,这是语法分析阶段,确保输入的源代码符合语言的结构规则。 4. **解释器编写**:通过递归下降解析技术,编写语言的解释器。解释器直接执行源代码,而...

    yy.rar_C Builder

    语法分析阶段,编译器依据语法规则,将词法分析产生的标记流组织成抽象语法树(Abstract Syntax Tree, AST)。对于布尔表达式,例如 "A AND (B OR NOT C)",抽象语法树会反映出其结构,便于进一步的处理。C++ ...

Global site tag (gtag.js) - Google Analytics