参考: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;
}
当然,上面的解释器过于原始,不支持多位整数,不支持负数,不支持空格等等等,但这至少是一个开始,接下来,我们将在这个基础上慢慢改进。