一个简单的解释器(1)

参考:Let’s Build A Simple Interpreter.
源代码:github

前言

日常使用python写一些自动化脚本,用到多线程的时候才发现,python居然没有真正的多线程?
这一切都得感谢GIL(Global Interpreter Lock),最常用的CPython解释器使用GIL来保证线程安全,也就是说python线程运行的时候是被GIL锁死的,其它线程并不能并发执行,python只是使用轮询来模拟多线程,想要在python上利用多核CPU,只能用多进程,然后通过PIPE或者别的一些恶心玩意来进行进程间通信。
当然也有一些没用GIL的python解释器,但是由于几乎所有python库都默认存在GIL,它们不会对线程安全进行任何处理,所以会使得多线程运行起来非常危险。
当然这是历史原因,在最初的python解释器实现的年代,多核CPU还不是主流,现如今已经是尾大不掉,不好修改了。
当然我并不想自己写一个线程安全的没有GIL的python解释器,这工程未免太大,只是由于以上原因,研究一下解释器的实现原理,实现一个简单的解释器,也许发明一个自己的编程语言,实现语言方面选择C++,足够灵活,C++17标准下也基本可以保证内存安全,不过std::string居然还是没有split,这方面只好效率低一点了。

Let's go

先忘记那些词法分析、语法分析、语法图、有限状态机等等麻烦的玩意,实现一个最简单的语法:

输入两个个位数字相加的表达式,输出结果,其余输入抛出错误,如:输入1+1,输出2

OK,我们先声明一个Token类在Token.h里:

#pragma once
#include <variant>
#include <string>
#include <memory>

enum class TokenType
{
    INTEGER,
    PLUS,
    EOF,
};

typedef std::variant<int,std::string> TokenValue;

class Token
{
private:
    TokenType tokenType;
    TokenValue tokenValue;
public:
    Token(TokenType t, TokenValue v);
    TokenType getType();
    TokenValue getValue();
};

typedef std::shared_ptr<Token> TokenPtr;

感谢C++17,感谢std::varaint,我可以用接近动态类型语言的方式实现TokenValue,而不需要自己去写template或者union,这里可以使用std::shared_ptr来更灵活和安全地使用Token对象。
实现上比较简单,Token.cpp:

#include "Token.h"

Token::Token(TokenType t, TokenValue v)
{
    tokenType = t;
    tokenValue = v;
}

TokenType Token::getType()
{
    return tokenType;
}

TokenValue Token::getValue()
{
    return tokenValue;
}

好了,接下来我们只要实现一个Interpreter,声明如下:

#prama once
#include <string>
#include "Token.h"

class Interpreter
{
private:
    TokenPtr currentTokenPtr;
    std::string inputText;
    size_t currentPos;
private:
    void eat(TokenType type);
    void raiseError();
    TokenPtr getNextTokenPtr();
public:
    Interpreter(std::string text);
    int expr();
};

接下来的功能是核心,我们一步一步来,首先实现构造函数:

Interpreter::Interpreter(std::string text)
{
    inputText = text;
    currentPos = 0;
}

没有任何难度,接下来是真正干活的expr:

int Interpreter::expr()
{
    currentTokenPtr = getNextTokenPtr();
    TokenPtr left = currentTokenPtr;
    eat(TokenType::INTEGER);

    TokenPtr op = currentTokenPtr ;
    eat(TokenType::PLUS);

    TokenPtr right = currentTokenPtr ;
    eat(TokenType::INTEGER);

    return std::get<int>(left->getValue()) + std::get<int>(right->getValue());
}

思路上也很简单,首先获取第一个Token,由于我们的语法是定死的:个位数、加号、个位数,所以第一个Token必须是TokenType::INTEGER的类型,然后我们调用eat,把它吃进去,检测合法性,同时把下一个Token赋予currentTokenPtr,如是三遍,读取所有Token以后,返回结果。
然后实现getNextTokenPtr:

TokenPtr Interpreter::getNextTokenPtr()
{
    const char * textStr = text.c_str();
    if( currentPos >= strlen(textStr) )
    {
        return std::make_shared<Token>(TokenType::EOF,NULL);
    }

    const char currentChar = textStr[currentPos];
    if( currentChar >= '0' && currentChar <= '9' )
    {
        currentPos++;
        return std::make_shared<Token>(TokenType::INTEGER,currentChar - '0');
    }

    if( strcmp( currentChar,'+' ) == 0 )
    {
        currentPos++;
        return std::make_shared<Token>(TokenType::PLUS,"+");
    }

    raiseError();
}

逻辑也非常简单,判断当前的字符,返回对应的TokenPtr,如果条件都不符合,则抛出error。
然后实现一个eat,就大功告成啦:

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

至于raiseError,简单点可以直接throw "Interpreter error.";,或者自己实现一个exception类:

#prama once
#include <exception>
#include <string>

class InterpreterError:public std::exception
{
};

这样raiseError的实现就是:

void Interpreter::raiseError()
{
    throw InterpreterError();
}

这样可以在catch的时候规定更明确的exception类型。
最后实现一个main函数来进行测试:

#include "Interpreter.h"
#include <iostream>
#include <string>

int main()
{
    std::string text;
    while(true)
    {
        try
        {
            std::in >> text;
            Interpreter interpreter(text);
            std::cout << interpreter.expr() << std::endl;
        }
        catch(InterpreterError e)
        {
            return -1;
        }
    }
    return 0;
}

当然,上面的解释器过于原始,不支持多位整数,不支持负数,不支持空格等等等,但这至少是一个开始,接下来,我们将在这个基础上慢慢改进。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念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

推荐阅读更多精彩内容