一个简单的解释器(4)—— 乘除和运算符优先级

本章,我们将继续扩展我们的解释器,使其支持乘除运算符和运算符优先级,我们将更加深入编译原理,加入文法分析( grammar analysis )。

文法与文法分析

等一下,什么是文法?
OK,国内的编译原理教材是翻译得有点随心所欲以至于当年上学的时候总是云里雾里,还是英文材料更好理解一些,不过中文语境确实很难精确表达syntax跟grammar,简单来说,文法( grammar )是描述语法的语法,是一种元语法( meta syntax ),
文法的描述方式参考BNF,我们不会严格地遵守BNF,而是在这个基础上稍微扩展一些,使用EBNF
对于我们的多位数加减乘除语言,有如下文法:

expr : factor((PLUS | MINUS | MULTI | DIV)factor)*
factor : INTEGER

其中,冒号左侧的符号被称为head,相对的,右侧是body,类似PLUS/MINUS/MULTI/DIV/INTEGER这样的变量被称为终端(terminals),它们类似编程语言的保留字,是不需要进一步展开的,而相对的expr/factor则是非终端(non-terminals),非终端代表了我们的文法还没结束,需要进一步展开,比如factor要展开成INTEGER,文法分析才结束。
expr作为第一行的head,又叫做起始变量(start symbol),它是我们文法所描述的语法的起点,而expr后面,冒号右侧的内容则是推断expr相对应的语法的文法,可以看到它具有类似正则表达式的符号,这里,它表示一个factor后面跟着零个或多个(PLUS | MINUS | MULTI | DIV)factor的组合,而factor本身则由第二行文法定义,它代表一个整数。
现在我们对输入"3"进行文法分析:

expr
factor((PLUS | MINUS | MULTI | DIV)factor)*
factor
INTEGER

最终,我们得到"3"这个输入对应的语法就是一个INTEGER,那么对于3+3这样的呢?

expr
factor((PLUS | MINUS | MULTI | DIV)factor)*
factor PLUS factor((PLUS | MINUS | MULTI | DIV)factor)*
factor PLUS factor
INTEGER PLUS INTEGER

可以看到,文法最终展开成了由terminals构成的语法。

运算符优先级

当我们加入乘除运算以后,运算符优先级就成了我们的解释器所必须的一项特性,对于运算符优先级,我们可以画一张表来表示,这也是文法的一部分,将来我们需要实现文法分析器时,这将至关重要:

precedence level associative operators
2 left +, -
1 left *, /

其中associative代表这个运算符是跟它的左边还是右边的factor相关联的,我们统一使用left,即:

1 / 3 + 3 = ( 1 / 3 ) + 3

如果是right的话就变成:

1 / 3 + 3 = 1 / ( 3 + 3 )

这显然不符合常识。
接下来,我们要把这个表转化成文法,通过以下两条规则:

  • 对于每一个运算符优先级,定义一条非终端( non-terminal )规则,其中包含当前优先级的终端运算符和下一个优先级的非终端head;
  • 为非终端的基本单元创建一条规则,对于我们的解释器来说,就是factor : INTEGER
    最终,我们修改文法为:
expr : term((PLUS | MINUS)term)*
term : factor((MULTI | DIV)factor)*
factor : INTEGER

这里的逻辑在于:解释expr之前,一定要先解释term,所以term中所包含的terminals的优先级是高于expr中的terminals的,以此类推,可以实现多重优先级。

实现

这一次,我们正式实现一个词法分析器Lexer:

#pragma once
#include <string>
#include <memory>
#include "Token.h"

class Lexer
{
private:
    size_t currentPos;
    std::string text;
    char currentChar;
private:
    void advance();
    void skipWhiteSpaces();
    int integer();
    bool isSpace(char c);
    bool isDigit(char c);
    void raiseError();
public:
    Lexer(std::string txt);
    TokenPtr getNextTokenPtr();
};

typedef std::shared_ptr<Lexer> LexerPtr;
#include "Lexer.h"
#include "LexerException.h"
using namespace std;
void Lexer::advance()
{
    currentPos += 1;
    if (currentPos >= strlen(text.c_str())) 
    {
        currentChar = '\0';
        return;
    }
    currentChar = text.c_str()[currentPos];
}

void Lexer::skipWhiteSpaces()
{
    while (isSpace(currentChar))
    {
        advance();
    }
}

int Lexer::integer()
{
    int result = 0;
    while (isDigit(currentChar))
    {
        result *= 10;
        result += currentChar - '0';
        advance();
    }
    return result;
}

bool Lexer::isSpace(char c)
{
    return c == ' ' || c == '\t';
}

bool Lexer::isDigit(char c)
{
    return c >= '0' && c <= '9';
}

void Lexer::raiseError()
{
    throw LexerException();
}

Lexer::Lexer(std::string txt) :text(txt)
{
    currentChar = txt.c_str()[0];
    currentPos = 0;
}

TokenPtr Lexer::getNextTokenPtr()
{
    while (currentChar != '\0' && currentPos < strlen(text.c_str()))
    {
        if (isSpace(currentChar))
        {
            skipWhiteSpaces();
        }

        TokenType tokenType;
        TokenValue tokenValue;

        if (isDigit(currentChar)) 
        {
            return make_shared<Token>(TokenType::TT_INTEGER,integer());
        }

        switch (currentChar)
        {
        case '+':
            tokenType = TokenType::TT_PLUS;
            tokenValue = "+";
            break;
        case '-':
            tokenType = TokenType::TT_MINUS;
            tokenValue = "-";
            break;
        case '*':
            tokenType = TokenType::TT_MULTI;
            tokenValue = "*";
            break;
        case '/':
            tokenType = TokenType::TT_DIV;
            tokenValue = "/";
            break;
        default:
            raiseError();
            break;
        }

        advance();
        return make_shared<Token>(tokenType, tokenValue);
    }
    return make_shared<Token>(TokenType::TT_EOF, NULL);
}

把词法分析的逻辑从之前的interpreter中剥离了出来,这样,Lexer只需要负责解析输入的字符串,并按顺序输出Token,其余的逻辑不需要它考虑。
而interperter则可以更加专注于语法分析:

#pragma once
#include "Token.h"
#include "Lexer.h"

class Interpreter
{
private:
    LexerPtr lexerPtr;
    TokenPtr currentTokenPtr;
private:
    void eat(TokenType type);
    void raiseError(std::string content);
    TokenValue term();
    TokenValue factor();
public:
    Interpreter(std::string txt);
    int expr();
};
#include "Interpreter.h"
#include "InterpreterException.h"
using namespace std;

void Interpreter::eat(TokenType type)
{
    if (currentTokenPtr->getType() != type)
    {
        raiseError("syntax error");
    }
    currentTokenPtr = lexerPtr->getNextTokenPtr();
}


void Interpreter::raiseError(std::string content = "")
{
    throw InterpreterException(content);
}

TokenValue Interpreter::term()
{
    TokenValue result = factor();
    while (currentTokenPtr->getType() == TokenType::TT_MULTI ||
        currentTokenPtr->getType() == TokenType::TT_DIV) 
    {
        TokenPtr tokenPtr = currentTokenPtr;
        eat(tokenPtr->getType());
        switch (tokenPtr->getType())
        {
        case TokenType::TT_MULTI:
            result = get<int>(result) * get<int>(factor());
            break;
        case TokenType::TT_DIV:
            result = get<int>(result) / get<int>(factor());
            break;
        }
    }
    return result;
}

TokenValue Interpreter::factor()
{
    TokenPtr tokenPtr = currentTokenPtr;
    eat(TokenType::TT_INTEGER);
    return tokenPtr->getValue();
}

Interpreter::Interpreter(string txt)
{
    lexerPtr = make_shared<Lexer>(txt);
    currentTokenPtr = lexerPtr->getNextTokenPtr();
}

TokenValue Interpreter::expr()
{
    TokenValue result = term();
    while (currentTokenPtr->getType() == TokenType::TT_PLUS ||
        currentTokenPtr->getType() == TokenType::TT_MINUS) 
    {
        TokenPtr token = currentTokenPtr;
        switch (token->getType())
        {
        case TokenType::TT_PLUS:
            eat(TokenType::TT_PLUS);
            result = get<int>(result) + get<int>(term());
            break;
        case TokenType::TT_MINUS:
            eat(TokenType::TT_MINUS);
            result = get<int>(result) - get<int>(term());
            break;
        default:
            raiseError();
            break;
        }
    }
    return result;
}

上面是根据之前定义的文法所写的语法分析器,可以看到term和factor这种non-terminal,分别实现了对应的函数,最后通过start symbol:expr调用。

括号的文法

OK,让我们向上面的文法中添加一点点东西:

expr : term((PLUS | MINUS)term)*
term : factor((MULTI | DIV)factor)*
factor : INTEGER | LPAREN expr RPAREN

我们添加LPAREN和RPAREN两个terminals,分别代表左右括号,并且仅仅将factor的body扩展了一点,如此,便从文法上实现了括号运算符。
是不是很神奇?
还记得我们怎么实现乘除的优先级吗?
对于任何一条文法规则,其non-terminal符号总是比该规则本身优先级更高,所以对于factor这个规则,当它的模式为LPAREN expr RPAREN时,expr中的内容的优先级总是高于调用factor的non-terminal。
然后,根据我们的手工文法分析,向解释器中添加括号相关的代码即可,它只修改了factor,所以我们的Interpreter也只需要修改factor,除此以外再添加左右括号的词法分析即可。
首先,升级词法分析器,向Lexer.cpp的getNextToken的switch中添加如下cases:

case '(':
    tokenType = TokenType::TT_LPAREN;
    tokenValue = "(";
    break;
case ')':
    tokenType = TokenType::TT_RPAREN;
    tokenValue = ")";
    break;

然后升级Interpreter:

TokenValue Interpreter::factor()
{
    TokenPtr tokenPtr = currentTokenPtr;
    TokenValue result;
    switch (tokenPtr->getType())
    {
    case TokenType::TT_INTEGER:
        eat(TokenType::TT_INTEGER);
        return tokenPtr->getValue();
    case TokenType::TT_LPAREN:
        eat(TokenType::TT_LPAREN);
        result = expr();
        eat(TokenType::TT_RPAREN);
        return result;
    default:
        raiseError("syntax error");
        break;
    }
}

仅修改factor函数,我们成功实现了括号运算符,这就是文法的作用。

这一章,我们了解了文法,并且通过手工文法分析,实现了一个解释器,添加了乘除括号和运算符优先级的特性,把词法分析独立出来从而实现了更清晰的结构。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容