一、为什么要写X语言解析器
因为之前在《数学表达式计算器》中提到过,我最想尝试的挑战就是写一个C语言解析器,所以我今天抽空开始写了一点点,但是写的不是C语言的解析器。
二、为什么要不写C语言解析器
,为什么呢?因为一方面我觉得我不一定知道C语言中的所有语言特性,就算勉强写出来也不见得能称为C语言解析器;另外一方面是C语言中可能有一些特性比较复杂,现在很多现代语言为了更高效的实现解析工作,也摒弃了一些古老特性,或者说这些新语言强制我们按照一种新习惯来处理,比如必须换行无须分号;分割,比如函数{}左右耳朵也可以不用有。
不管怎么说,我想抛弃一些比较复杂的特性,用更简单的方式达到相同目的即可,比如一行内不允许多条语句或表达式。
三、动力所在
重起炉灶,按照自己的想法来,随意发挥,完全按自己想法来实现,自己发现开发过程中的难点,自己为这个难题提出自己的解决方案,所有的东西都应是自己发现、自己想出来解决方案。尽管实现可能不够完整、性能不够好,目标还是达成的。所以,重起炉灶造X语言解析器。
四、为什么叫X语言
为什么叫X语言,因为我不知道叫什么名称好,不好叫高大尚的,也不想叫低矮戳的,所以选择一个X,我喜欢把X当作未知来理解,因为未知,所以充满无数变数,这就是我选择X的原因。
五、先写代码后补构思
在今天之前,我稍微构思过整个语言的实现思路,不过我发现,有很多细节我也想不到的。之前我还想把我的构思梳理出来,但是心里确实没底,还是不要丢出来了,所以我今天马上抽空先写,写得很挫也没关系,主要我想快速验证我会遇到哪些问题,等我把问题弄清楚了,解决了,我再回来补充即可。
六、初步架构设想
先通过代码看看我的初步架构设想:
/**
* X language interpreter entry
* @Author Rizhong Li
* @Date 2024-01-10
*/
#include
#include
#include <xlang/Interpreter.h>
int main(int argc, const char* argv[]) {
int nr;
try {
auto *ir = new xlang::Interpreter();
if ((nr=ir->parse_cmd_line(argc, argv)) == 0) {
if ((nr=ir->initialize()) == 0) {
nr = ir->start();
}
}
} catch (const std::exception& e) {
nr = -1;
std::cerr << "Main exception: " << e.what() << ".\n";
} catch (...) {
nr = -1;
std::cerr << "Main unknown error" << ".\n";
}
std::cout << "Interpreter finished with exit code " << nr << ".\n";
return nr;
}
Interpreter就整个程序最外层的数据结构,通过它来解析命令行参数(parse_cmd_line),解析完命令行参数后,再通过它来初始化(initialize),我的意图是可以初始化X语言解析器自己要使用的配置参数(比如php解析器也有自己的配置文件),还有就是脚本程序的命令行参数,还有就是我把X设计成支持模块形式,所以类似php/go/python,也有一个模块元数据文件也要解析。
比如如下运行xlang解析器的命令行:
xlang -c /Users/codebook/xlang.toml /Users/codebook/CLionProjects/xtest/main.x 1 2 3 4 5 6 7 8 9 10
1、xlang是我写代码编译生成的xlang解析器可执行文件;
2、-c
/Users/codebook/xlang.toml这个意图是做xlang自己需要的配置文件,目前我没去解析这个文件,留了空函数对应数据结构存在,目前也没设计依赖什么参数,如果有,也会在代码里面写死一套,等有空回过头再回去解析xlang.toml配置文件;
3、
/Users/codebook/CLionProjects/xtest/main.x 这个就是xlang语言写的程序代码,类似python main.py或者php main.php或者node index.js一样,就是启动这个xlang脚本程序;
4、1 2 3 4 5 6 7 8 9 10 这个就是传给脚本程序main.x的命令参数;
目前,我只有到
/Users/codebook/CLionProjects/xtest/main.x,其他的是计划实现,设计好框架和数据结构,解析配置文件工作没写,优先级最低,尽快写代码发现问题、解决问题后再补充回来。下面当前的代码目录结构:
main.cpp就不用说,Interpreter就是外层的数据结构,内部包含Parser作为成员变量,而Parser内部又包含Lexer作为成员变量。
Lexcer主要做的工作就是词法分析,生成token给到Parser,后面可能增加一个Grammar来做语法分析处理,也有可能由Parser直接做了。反正就是很多东西要写了之后才更清晰。
目前写的代码核心代码主要在Lexcer,即词法分析工作,写得很乱,不过也贴出来看看吧,后面肯定会修改的。
/**
* xlang lexer
* @Author Rizhong Li
* @Date 2024-01-10
*/
#include
#include
#include "xlang/Lexer.h"
namespace xlang {
Lexer::Lexer() : _fin() {
}
int Lexer::load(std::filesystem::path &filepath) {
reset();
_fin.open(filepath.c_str());
if (!_fin.is_open()) {
std::cerr << "Failed to open source file:" << filepath << "\n";
return -1;
}
return 0;
}
void Lexer::reset() {
_fin.clear();
_fin.close();
}
int Lexer::is_new_line(char c) {
return (c == '\n' || c == '\r');
}
int Lexer::is_space(char c) {
return (c == '\t' || c == ' ' || c == '\v' || c == '\f');//``\t''``\n''``\v''``\f''``\r''`` ''
}
void Lexer::trim_left(char *&ps, char *&pe) {
while (ps <= pe isspaceps ps void lexer::trim_rightchar ps char pe while pe>= ps && isspace(*pe)) --pe;
}
int Lexer::get_line(std::string &line, size_t &line_no, char *&ps, char *&pe) {
while (true) {
std::getline(_fin, line);
if (_fin.bad()) {
//TODO:
return -1;/* Failed to read file */
}
if (line.empty()) {
if (_fin.eof()) {
return 1;/* End of file */
}
++line_no;
} else {
++line_no;
ps = line.data();
pe = line.data() + line.length() - 1;
trim_left(ps, pe);
trim_right(ps, pe);
if (ps > pe) {
continue;
}
break;
}
}
return 0;
}
int Lexer::generate_token() {
int nr;
bool read_new = false; /* Ignore \ to concat next line, so read_new is always true now */
char *ts = nullptr;
char *te = nullptr;
char *ps = nullptr;
char *pe = nullptr;
size_t line_no = 0;
size_t mlc_beg_line_no = 0;
size_t mlc_end_line_no = 0;
std::string line = "";
std::string token = "";
ESentenceType sen_state = SEN_NONE;
ETokenType token_state = TOKEN_NONE;
/* Read one line from file to process */
nr = get_line(line, line_no, ps, pe);
if (nr != 0) {
return nr;
}
while (true) {
read_new = false;
while (ps <= pe if sen_state='= SEN_NONE)' if isspaceps here ignore the space break if ps='= '/')' token_state='TOKEN_SLASH;' sen_state='SEN_COMMENT;' else if ps='= '+')' token_state='TOKEN_PLUS;' sen_state='SEN_PRE_INCR_P;' else if ps='= '-')' token_state='TOKEN_MINUS;' sen_state='SEN_PRE_DECR_OP;' else if ps='= '_')' token_state='TOKEN_UNDERSCORE;' sen_state='SEN_VARIABLE;' else if ps>= 'a' && *ps <= z ps>= 'A' && *ps <= 'Z')) {
token_state = TOKEN_LETTER;
sen_state = SEN_VARIABLE;
}
} else if (sen_state == SEN_COMMENT) {
if (*ps != '/' && *ps != '*') {
std::cout << "Unrecognized line:" << line << std::endl;
return -1;//TODO:
}
if (*ps == '/') {
std::cout << "Found single line comment [" << line_no << "]:" << line << std::endl;
token_state = TOKEN_NONE;
sen_state = SEN_NONE;
read_new = true;
break;
} else {
mlc_beg_line_no = line_no;
token_state = TOKEN_MLC_SLASH_STAR;
sen_state = SEN_ML_COMMENT;
}
} else if (sen_state == SEN_ML_COMMENT) {
if (*ps == '/') {
if (token_state == TOKEN_MLC_SLASH_STAR_STAR) {
mlc_end_line_no = line_no;
std::cout << "Found multi line comment [" << mlc_beg_line_no << "~" << mlc_end_line_no << "]:" << line << std::endl;
token_state = TOKEN_NONE;
sen_state = SEN_NONE;
// read_new = true;//Maybe wo can do better, now simplify the process
// break;
//Do better on this way
ps++;
trim_left(ps, pe);
continue;
} else {
token_state == TOKEN_MLC_SLASH_STAR;
}
} else if (*ps == '*') {
token_state = TOKEN_MLC_SLASH_STAR_STAR;
} else {
token_state == TOKEN_MLC_SLASH_STAR;
}
} else if (sen_state == SEN_PRE_INCR_P) {
if (*ps != '+') {
std::cerr << "Unrecognized line:" << line << std::endl else token_state='TOKEN_PRE_INCR_PP_VAR;' sen_state='SEN_PRE_INCR_PP;' token.clear ps trim_leftps pe if ps> pe) {
std::cout << "Unrecognized ++var [" << line_no << "]:" << line << std::endl;
return -1;
}
continue;
}
} else if (sen_state == SEN_PRE_INCR_PP) {
if (isspace(*ps)) {
token_state = TOKEN_NONE;
sen_state = SEN_NONE;
std::cout << "Found ++var1 [" << line_no << "]:" << line << std::endl else if token.empty if ps='= '_'' ps>= 'A' && *ps <= z ps>= 'a' && *ps <= 'z'))) {
std::cout << "Unrecognized ++var [" << line_no << "]:" << line << std::endl return -1 else token else if ps='= '_'' ps>= '0' && *ps <= 9 ps>= 'A' && *ps <= z ps>= 'a' && *ps <= 'z'))) {
std::cout << "Unrecognized ++var [" << line_no << "]:" << line << std::endl;
return -1;
} else {
token += *ps;
if (ps == pe) {
token_state = TOKEN_NONE;
sen_state = SEN_NONE;
std::cout << "Found ++var2 [" << line_no << "]:" << line << std::endl;
}
}
}
}
}
ps++;
}
/* Read one line from file to process */
nr = get_line(line, line_no, ps, pe);
if (nr != 0) {
return nr;
}
}
return 0;
}
}
七、词法分析整体思路
整体思路也是只遍历一遍,由状态机推进,目前写了两个状态进行维护,后续优化一下可能只使用一个状态就可以。目前的代码主要是为了能够解析注释,包括单行注释和多行注释,还有就是变量前置自增。
测试的main.x代码如下:
// this is an single line comment
//this is also an single line comment
/**/ /**/ //
/**
* this is comment
* ok
* llll
*/
++ abc
目前上面的代码,都能正常解析,后面会增加加新代码,一个功能点一个功能点的往里面接,正常已有代码不会影响目前解析注释的代码。