接下來的內(nèi)容將更加硬核,我們距離創(chuàng)造自己的編程語言更近一步——實現(xiàn)一個初步的Pascal解釋器。
Pascal
好吧這是一門古老的語言,在很久很久以前,我還是個初中生的時候稍微接觸過一點,語法上跟同樣古老的Basic差不多,不過因為它足夠古老,不會有太多花里胡哨的特性,相對容易實現(xiàn)一些,一段簡單的Pascal程序代碼如下:
BEGIN
BEGIN
number := 2;
a := number;
b := 10 * a + 10 * number / 4;
c := a - - b
END;
x := 11;
END.
從一個計算器解釋器跳轉(zhuǎn)到一個簡單的編程語言解釋器步子似乎有點大,我們一步一步來。
文法分析
可以看到,Pascal的語句都用"BEGIN ... END"對包裹起來,用"."號作為整個代碼段的結(jié)尾,好,第一步,依舊是創(chuàng)建文法,第一條應(yīng)該是一個program,它包含一個被BEGIN END對包裹的compound_statement和一個DOT:
program : compound_statement DOT
然后我們定義compound_statement:
compound_statement : BEGIN statement_list END
如上所述,一個compound_statement就是被BEGIN END對包裹的statement_list,然后我們再定義statement_list:
statement_list : statement | statement SEMI statement_list
請注意這里定義了一個terminal:SEMI,它代表一個分號,Pascal的分號不同于C/C++,分號只用來分隔語句,而不是作為語句的結(jié)束符號,如下Pascal代碼是合法的:
BEGIN
x := 1;
y := 2
END.
所以對于我們的文法來說,只有statement后面又有statement_list時,才需要SEMI。
接下來我們定義具體的statement,在我們的示例代碼中,一個statement只有三種可能:空語句、賦值語句、被BEGIN END包起來的compound_statement:
statement : empty | assignment_statement | compound_statement
空語句比較簡單,就是什么都沒有:
empty :
compound_statement再前面已經(jīng)定義過了,所以我們需要定義賦值語句assignment_statement:
assignment_statement : variable ASSIGN expr
對于變量名variable,我們定義一個terminal:ID,不用進一步展開:
variable : ID
然后定義了一個terminal操作符,,ASSIGN,代表:=,expr我們很熟悉了,它就是我們在前面的章節(jié)實現(xiàn)過的文法,這里不作贅述,需要注意的是factory需要對應(yīng)的升級,因為variable也可能作為一個factor參與計算:
factor : (PLUS | MINUS) factor | INTEGER | LPAREN expr RPAREN | variable
完整的文法如下:
program : compound_statement DOT
compound_statement : BEGIN statement_list END
statement_list : statement
| statement SEMI statement_list
statement : compound_statement
| assignment_statement
| empty
assignment_statement : variable ASSIGN expr
empty :
expr: term ((PLUS | MINUS) term)*
term: factor ((MUL | DIV) factor)*
factor : (PLUS | MINUS) factor
| INTEGER
| LPAREN expr RPAREN
| variable
variable: ID
其中,statement_list和statement可以合并成如下形式:
statement : ( compound_statement | assignment_statement | empty ) (SEMI ( compound_statement | assignment_statement | empty ))*
但是這顯然過于啰嗦,而且將來當我們使用文法分析器如PLY時可能不支持*的語法,類似的,(PLUS | MINUS) factor,也可以拆分成PLUS factor | MINUS factor。
升級詞法分析器
確認了文法以后,開干,第一步,升級詞法分析器,我們需要完成:
- 增加Token類型,識別BEGIN END DOT ID ASSIGN SEMI這些關(guān)鍵字;
- 添加一個peek函數(shù)以解決歧義;
- 添加一個id函數(shù)用來判斷和獲取變量名;
- 升級getNextTokenPtr函數(shù);
擴展Token.h中的TokenType:
enum class TokenType {
TT_INTEGER,
TT_PLUS,
TT_MINUS,
TT_MULTI,
TT_DIV,
TT_LPAREN,
TT_RPAREN,
TT_BEGIN,
TT_END,
TT_SEMI,
TT_DOT,
TT_ASSIGN,
TT_ID,
TT_EOF
};
在編程語言中,我們需要分辨同樣起始字符的不同符號如:=和::,==和=>,Pascal的保留字和變量名等等,為此在Lexer中實現(xiàn)一個peek函數(shù),區(qū)別于advance,它返回當前字符的下一個字符,但不使pos+1:
char Lexer::peek()
{
if (currentPos + 1 >= strlen(text.c_str()))
{
return NULL;
}
return text.c_str()[currentPos + 1];
}
實現(xiàn)一個id函數(shù),在讀取到字母時判斷它是保留字還是變量名,并返回相應(yīng)的Token:
static const std::map<std::string, TokenPtr> REVERSED_WORDS =
{
{"BEGIN",make_shared<Token>(TokenType::TT_BEGIN,"BEGIN")},
{"END",make_shared<Token>(TokenType::TT_END,"END") },
};
TokenPtr Lexer::id()
{
std::string result = "";
while (isalnum(currentChar))
{
result += currentChar;
advance();
}
if (REVERSED_WORDS.find(result) == REVERSED_WORDS.end())
{
return make_shared<Token>(TokenType::TT_ID,result);
}
else
{
return REVERSED_WORDS.at(result);
}
}
最后,升級一下getNextTokenPtr,加入新的Tokens的判斷:
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());
}
if (isalpha(currentChar))
{
return id();
}
switch (currentChar)
{
case '.':
tokenType = TokenType::TT_DOT;
tokenValue = ".";
break;
case ':':
if (peek() == '=')
{
advance();
tokenType = TokenType::TT_ASSIGN;
tokenValue = ":=";
}
else
{
raiseError();
}
break;
case ';':
tokenType = TokenType::TT_SEMI;
tokenValue = ";";
break;
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;
case '(':
tokenType = TokenType::TT_LPAREN;
tokenValue = "(";
break;
case ')':
tokenType = TokenType::TT_RPAREN;
tokenValue = ")";
break;
default:
raiseError();
break;
}
advance();
return make_shared<Token>(tokenType, tokenValue);
}
return make_shared<Token>(TokenType::TT_EOF, NULL);
}
OK,至此,詞法分析器升級完成,我們寫一段代碼來測試:
int main()
{
std::string text = "BEGIN x:=1 END.";
Lexer lexer(text);
TokenPtr tokenPtr = lexer.getNextTokenPtr();
while (tokenPtr->getType() != TokenType::TT_EOF)
{
std::visit([](auto&& arg) { std::cout << arg << std::endl; }, tokenPtr->getValue());
tokenPtr = lexer.getNextTokenPtr();
}
return 0;
}
這里利用std::visit來更快捷地訪問TokenValue的值,順利的話,輸出內(nèi)容應(yīng)該如下:
BEGIN
x
:=
1
END
.
可以看到原始string已經(jīng)被順利解析成Tokens了。
升級語法分析器
往上一層,對于ASTParser來說,我們需要升級以下功能:
- 定義新的ASTNodes;
- 完成所有那些新的文法對應(yīng)的函數(shù)實現(xiàn);
- 升級parse和factor函數(shù);
同樣的,我們對文法的所需要的AST節(jié)點進行定義,首先,定義CompoundNode:
#pragma once
#include "AbstractSyntaxTreeNodeBase.h"
#include <vector>
class CompoundNode : public AbstractSyntaxTreeNodeBase
{
private:
std::vector<ASTNodePtr> children;
public:
GET_CLASS_NAME(CompoundNode)
};
AssginNode:
#pragma once
#include "AbstractSyntaxTreeNodeBase.h"
class AssignNode : public AbstractSyntaxTreeNodeBase
{
private:
ASTNodePtr left;
ASTNodePtr right;
TokenPtr opTokenPtr;
public:
AssignNode(ASTNodePtr l, TokenPtr op, ASTNodePtr r);
GET_CLASS_NAME(AssignNode)
};
VarNode:
#pragma once
#include "AbstractSyntaxTreeNodeBase.h"
#include "Tokenizer/Token.h"
class VarNode : public AbstractSyntaxTreeNodeBase
{
private:
TokenPtr tokenPtr;
TokenValue varValue;
public:
VarNode(TokenPtr ptr);
GET_CLASS_NAME(VarNode)
};
varNode需要一個額外的varValue成員來存儲它的值。
以及特殊的NoOpNode:
#pragma once
#include "AbstractSyntaxTreeNodeBase.h"
class NoOpNode : public AbstractSyntaxTreeNodeBase
{
public:
GET_CLASS_NAME(NoOpNode)
};
對應(yīng)文法中的empty,是一個空的節(jié)點。
OK,完成了新的nodes,我們開始升級Parser,每有一條文法都有一個對應(yīng)的函數(shù),所以我們先在ASTParser.h中添加如下聲明
#pragma once
#include <string>
#include "ASTNodes/BinOpNode.h"
#include "ASTNodes/NumNode.h"
#include "Tokenizer/Lexer.h"
class ASTParser
{
private:
LexerPtr lexerPtr;
TokenPtr currentTokenPtr;
private:
void raiseError();
void eat(TokenType type);
ASTNodePtr program();
ASTNodePtr compoundStatement();
std::vector<ASTNodePtr> statementList();
ASTNodePtr statement();
ASTNodePtr assignStatement();
ASTNodePtr variable();
ASTNodePtr empty();
ASTNodePtr expr();
ASTNodePtr term();
ASTNodePtr factory();
public:
ASTParser(std::string txt);
ASTNodePtr parse();
};
typedef std::shared_ptr<ASTParser> ASTParserPtr;
然后實現(xiàn)這些新增的函數(shù):
ASTNodePtr ASTParser::program()
{
ASTNodePtr node = compoundStatement();
eat(TokenType::TT_DOT);
return node;
}
ASTNodePtr ASTParser::compoundStatement()
{
eat(TokenType::TT_BEGIN);
ASTNodePtr root = make_shared<CompoundNode>(statementList());
eat(TokenType::TT_END);
return root;
}
std::vector<ASTNodePtr> ASTParser::statementList()
{
ASTNodePtr node = statement();
std::vector<ASTNodePtr> result = { node };
while (currentTokenPtr->getType() == TokenType::TT_SEMI)
{
eat(TokenType::TT_SEMI);
result.push_back(statement());
}
if (currentTokenPtr->getType() != TokenType::TT_END)
{
raiseError();
}
return result;
}
ASTNodePtr ASTParser::statement()
{
if (currentTokenPtr->getType() == TokenType::TT_BEGIN)
{
return compoundStatement();
}
if (currentTokenPtr->getType() == TokenType::TT_ID)
{
return variable();
}
return empty();
}
ASTNodePtr ASTParser::assignStatement()
{
ASTNodePtr left = variable();
TokenPtr opToken = currentTokenPtr;
eat(TokenType::TT_ASSIGN);
ASTNodePtr right = expr();
return make_shared<AssignNode>(left,opToken,right);
}
ASTNodePtr ASTParser::variable()
{
ASTNodePtr varNode = make_shared<VarNode>(currentTokenPtr);
eat(TokenType::TT_ID);
return varNode;
}
ASTNodePtr ASTParser::empty()
{
return make_shared<NoOpNode>();
}
factor函數(shù)新增了一種可能:
ASTNodePtr ASTParser::factory()
{
TokenPtr tokenPtr = currentTokenPtr;
...
if (tokenPtr->getType() == TokenType::TT_ID)
{
return variable();
}
...
更新parse函數(shù),這次我們parse的輸出是一個program了:
ASTNodePtr ASTParser::parse()
{
ASTNodePtr programNode = program();
if (currentTokenPtr->getType() != TokenType::TT_EOF)
{
raiseError();
}
return programNode;
}
至此,語法分析器更新完畢,下一步,我們需要更新解釋器的visitor,另外,為了variable實現(xiàn)一張變量表。
更新解釋器
首先,我們添加新增的node的visitor函數(shù)聲明:
#pragma once
#include "NodeVisitor.h"
#include "Parser/ASTParser.h"
#include "ASTNodes/CompoundNode.h"
#include "ASTNodes/NoOpNode.h"
#include "ASTNodes/AssignNode.h"
#include "ASTNodes/VarNode.h"
#include <string>
#define DECLARE_VISITOR(NODE_NAME) TokenValue visit##NODE_NAME(ASTNodePtr nodePtr)
class Interpreter : public NodeVisitor
{
private:
ASTParserPtr astParserPtr;
private:
void raiseError(std::string content);
public:
std::map<std::string,TokenValue> globalVariables;
public:
DECLARE_VISITOR(NumNode);
DECLARE_VISITOR(BinOpNode);
DECLARE_VISITOR(UnaryOpNode);
DECLARE_VISITOR(CompoundNode);
DECLARE_VISITOR(NoOpNode);
DECLARE_VISITOR(AssignNode);
DECLARE_VISITOR(VarNode);
Interpreter(std::string text);
TokenValue interpret();
};
注意這里新增了一個globalVariables成員變量,用來儲存我們要解釋的代碼段的變量名和值。
然后,實現(xiàn)相應(yīng)的visitor函數(shù):
DEFINE_VISITOR(CompoundNode)
{
auto children = dynamic_pointer_cast<CompoundNode>(nodePtr)->getChildren();
for (ASTNodePtr child : children)
{
this->visit(child);
}
return NULL;
}
DEFINE_VISITOR(NoOpNode)
{
return NULL;
}
DEFINE_VISITOR(AssignNode)
{
auto node = dynamic_pointer_cast<AssignNode>(nodePtr);
auto varNode = dynamic_pointer_cast<VarNode>(node->getLeftPtr());
globalVariables[get<std::string>(varNode->getTokenPtr()->getValue())] = this->visit(node->getRightPtr());
return NULL;
}
DEFINE_VISITOR(VarNode)
{
auto varNode = dynamic_pointer_cast<VarNode>(nodePtr);
auto varName = get<string>(varNode->getTokenPtr()->getValue());
if (globalVariables.find(varName) == globalVariables.end())
{
raiseError("Name error");
}
return globalVariables[varName];
}
在AssignNode的visitor中,我們將它的left的值作為key,right的值作為value放進globalVariables。
然后,在構(gòu)造函數(shù)中添加一下函數(shù)指針的map:
Interpreter::Interpreter(std::string text)
{
ADD_TO_VISIT_MAP(NumNode);
ADD_TO_VISIT_MAP(BinOpNode);
ADD_TO_VISIT_MAP(UnaryOpNode);
ADD_TO_VISIT_MAP(CompoundNode);
ADD_TO_VISIT_MAP(AssignNode);
ADD_TO_VISIT_MAP(NoOpNode);
ADD_TO_VISIT_MAP(VarNode);
astParserPtr = make_shared<ASTParser>(text);
}
OK,就這么簡單,我們完成了Pascal語言的初步解釋,現(xiàn)在解釋器可以識別賦值語句并存儲到變量表中了。
寫一段測試代碼:
int main()
{
std::string text = R"(
BEGIN
x:=1;
BEGIN
y:=2*(2+3)
z:=x+y
END;
END.
)";
Interpreter inter(text);
inter.interpret();
for (const auto& pair : inter.globalVariables)
{
cout << pair.first << "=" << std::get<int>(pair.second) << endl;
}
return 0;
}
順利的話,可以看到如下輸出:
x=1
y=10
z=11
Cool!試試隨意修改這段代碼看看吧!