逃 Error

“逃 ERROR”,顾名思义。本页记录了一些 Abandon 的一些错误。不包含语法错误,包含好不容易 de 掉的 编译器/连接器 错误、运行时错误、逻辑错误.

include 的顺序

src/include.h下包含了这个 project 的所有头文件,其中有标准库的,也有 Abandon 中某个部分的头文件,之后让其他的每个编译单元都 include 这个文件。如果该文件的某个顺序错了,那就会让编译器 g++ 报一些某某符号 undefined 的错.
我甚至为此写一个注释:

// pay attention to the order!! (to myself)

不要在头文件中定义变量!

Once upon a time, 我在某个头文件中加入了类似以下的代码

PointerManager pmTree;

然而,因为上面提过的原因,每个编译单元都 include“主头文件”,而“主头文件”会包含每个其他的头文件,所以就会产生多个定义.
当然,在编译阶段没报错,但链接时就会报一大堆多重定义的错:multiple definition of pmTree
我刚开始想到把它放到实现文件(或 源文件)中,但也不行,这样编译器就报错了.
Emmmm, 我又想起了一个词—— extern .
于是,解决这个问题也很简单, 在头文件的定义前加上 extern,把定义(Definition)变为声明(Declaration),在把定义放在实现文件中.

extern PointerManager pmTree;

这就是空指针鸭

“我要写 parser 辣”:我用一个写了一个类 Tree用来存储抽象语法树(Abstract Syntax Tree, AST ),在我的 Parser类中,我有一些用于解析非终结符的成员函数,每个函数返回一个指向 new 过的一个树的指针,so ,
我在遇到错误的语法时会返回 nullptr,而在这之后我又直接引用了这个空指针,
于是,make 过了,运行时系统输出了类似这样的错:

[1]    2841 segmentation fault (core dumped)  ./abandon hello.abn

并且当前目录还多了个名为core.2841的文件,现请教 GPT-4o 得知,

在Linux系统中,出现“segmentation fault (core dumped)”错误时,系统通常会生成一个核心转储文件(core dump file)。这个文件包含程序在崩溃时的内存映像和寄存器状态,帮助开发者进行调试和分析。
具体到你的例子中:
● [1] 2841 表示进程ID为2841的程序在后台运行。
● segmentation fault (core dumped) 表示程序由于非法访问内存地址而崩溃,并且系统生成了一个核心转储文件。
生成的文件 core.2841 是核心转储文件,其中 2841 对应的是崩溃的进程ID。该文件包含了程序崩溃时的内存状态、寄存器状态、堆栈内容等信息。开发者可以使用调试工具,如gdb(GNU Debugger),来分析这个核心转储文件,以确定崩溃的原因。

(我这时使用的是一个云 space,运行的 Linux)
这明显是一个比较明显的问题,但当时这个明显的问题显得不那么明显,
就是说“不知庐山真面目,只缘身在此山中”,我当时想不到这上面,害的我开始埋怨别的函数。
最后才领悟“啊,我 return 了个空指针,我又引用了这个空指针”.
解决办法就是不用 nullptr 来表示语法错误,于是我又定义了一个全局变量来表示:

Tree* noneTreeClass = createTree(treeTypeNode_None);

不要吞我的 token

在试写 parser 后,我的输入是 3+7,而输出的树是

Node Expr
├── Node Term
│   ├── Node Factor
│   │   └── Token Int [3]
│   └── Node ε
└── Node ε

其文法是

Expr   -> Term Expr'
Expr'  -> + Term Expr' | - Term Expr' | ε
Term   -> Factor Term'
Term'  -> * Factor Term' | / Factor Term' | ε
Factor -> ( Expr ) | Number

哎,我加号呢?我 7 呢? debug 后,发现原来是“吞 token”了.

按照我的规定,parser 里的函数都是先使用现成的 token,后为下一个函数获取新 token; 每个函数最后必须有且只能有一个未使用的 token

而我的一些函数因为多读了造成了 bug (我称之为“吞 token”,不知道是否有这个名词)
比如以下代码,为 Expr' 写的一个函数 (含有 bug 的)

Tree* Parser::parse_Expr_() {
    Token tk = this->lx->current;   // 获取当前 token
    this->lx->getNextToken();       // 为下一个 token 提前读
    
    Tree* tr = createTree(treeTypeNode_Expr_);  // 构建一棵 Expr' 的树 (作为返回值)
    Tree* tr_Term;
    Tree* tr_Expr_;
    if (tk.matchSign("+") || tk.matchSign("-")) {   // 当前 token 是 + 或 - 的符号
        tr->add(createTree(tk));    // 将当前 token 封装为树加入到 tr 的子结点
    } else {
        return epsilonTreeClass; // 返回 ε
    }

    tr_Term = this->parse_Term();   // 继续解析 Term
    if (tr_Term == noneTreeClass) return noneTreeClass; // 如果 Term 解析失败, 返回失败
    tr->add(tr_Term);   // 否则将 Term 的树加入到 tr 的子结点

    tr_Expr_ = this->parse_Expr_(); // 继续解析 Expr'
    if (tr_Expr_ == noneTreeClass) return noneTreeClass;    // 如果 Expr' 解析失败, 返回失败
    tr->add(tr_Expr_);  // 否则将 Expr' 的树加入到 tr 的子结点
    return tr;  // 返回解析过后的 Expr' 树
}

不难发现, 如果输入是 ε 的, 那么它会依次执行

Token tk = this->lx->current; // 未使用的第一个 token (先标记为 token1)
this->lx->getNextToken();   // 未使用的第二个 token
Tree* tr = createTree(treeTypeNode_Expr_);
return epsilonTreeClass;
/* 因为定义不会被执行,且对应该示例无用,故弃之*/

这样它就会一下子读了两个 token,但却都没有使用,会导致其他代码看不到 token1
解决方案:
将代码改为只有返回的不是 ε 时,才 this->lx->getNextToken()
如下

Tree* Parser::parse_Expr_() {
    Token tk = this->lx->current;
    // 减去了 this->lx->getNextToken();
    
    Tree* tr = createTree(treeTypeNode_Expr_);
    Tree* tr_Term;
    Tree* tr_Expr_;
    if (tk.matchSign("+") || tk.matchSign("-")) {
        tr->add(createTree(tk));
        this->lx->getNextToken(); // 增加了
    } else {
        return epsilonTreeClass; 
    }

    tr_Term = this->parse_Term();
    if (tr_Term == noneTreeClass) return noneTreeClass;
    tr->add(tr_Term);

    tr_Expr_ = this->parse_Expr_();
    if (tr_Expr_ == noneTreeClass) return noneTreeClass;
    tr->add(tr_Expr_);
    return tr;
}

其实也就是把 this->lx->getNextToken() 换了个位置,
其他函数也有这个 bug,解决方法亦同