On semantic check, types, namespaces and control flows in Rx

Created: September 16, 2025 1:01 PM

提示:文中含有大量对当前 Rx 语言的简化。如果你已经完成其中的一些特性,完全不需要删除你的实现。你可以和助教讨论你的方案,并且在 cr 时展示。我们会给一定的加分,计入 bonus。

What to do

在语义分析阶段,你的编译器需要识别一些常规的错误如下:

错误类型 描述
Type Mismatch 操作符两侧表达式类型不匹配;
函数参数类型不匹配、返回值类型不匹配;
let 语句类型不匹配;
if 语句不同分支类型不同,或者单分支类型不是 unit
borrow / dereference 见后面相应的部分
Invalid Type 非法的类型使用,如 condition 非 bool
main 函数返回类型不是 unit
对非数组类型使用 indexing expression ,或者在 indexing 中使用非 usize 类型;
对非引用类型使用解引用 *
在非 associated item 中使用 Self / self
loop / while 块中有尾表达式且该表达式类型不是 unit
const 环境中使用非 const expression
Undefined Name 找不到变量 / 函数 / struct / enum / enum variant / struct 字段或关联 item / trait / const item
Missing Return 在返回类型不是 unit 且没有尾表达式作为返回值的函数中,存在缺失返回语句的控制流
Invalid Control Flow break / continue 不在循环体内;
return 不在函数体内(比如,在 const 定义中出现);
main 函数最后一条 statement 以外的地方调用 exit
Immutable Variable Mutated 对不可变变量及其字段进行修改
Multiple Definition 同一个 struct 中出现了重名的字段/关联函数/关联常量;
同一个 scope 下出现重名的 item
测试点中保证,局部变量不会遮蔽它可见的同名函数、常量,也不会遮蔽同名类方法和常量;如果有,请向助教反映
Trait Item Unimplemented impl xx for xx 块中,没有实现 trait 中省略的关联函数体 / 关联 const
Syntax Error 在词法分析或语法分析阶段检查出的错误

说明:

How to check

假设你已经成功建了一棵 AST。

在 AST 上,有一类结点非常重要,即 BlockExpression 。在 AST 中,假设你已经将 BlockExpression 中的 Item 和其他 Statement 分开记录下来。

BlockExpression代表一个全新的 scope ,在名称解析的时候,如果当前 scope 找不到符号,那么就会回溯到上一个 scope。这样的 scope 自然地形成一棵树,而不是用完就扔。

这里你需要考虑一些细节,例如助教的做法是为函数、traitimplLoopExpression 也维护 scope,因为你可以在函数 scope 中写一些关于函数参数的信息;或者在 impl Scope 中记录 Self 类型。还有一点好处是,当你寻找一个变量的时候,不断向上回溯,一旦遇到函数的 scope 就可以停下来(因为我们不允许变量捕获(这是闭包干的事情))。

具体来说,以下每一步都需要遍历一遍 AST:

以上内容仅作参考。有一些步骤你可以自由调换顺序,只要逻辑通顺并且正确完成语法检查的任务。

如果你好奇上面的步骤为什么这样安排,注意几个关键的先后顺序:

Types in Rx

这一节说明一些类型逻辑,帮助你推断表达式的类型。我们在这里使用 rust 中的 never 类型 ! 作为类型推导的中间结果,并且保证这种类型不会显式出现。

Namespaces in Rx

我们有时候可以知道某个符号是干什么用的,去 scope 里面找对应的东西就好了。比如代码调用了一个函数,就去函数列表里面找;如果用到了一个 struct expression,就去 struct 列表里面找。

对于个别特例,我们不知道某个符号指代的是什么。例如,在 PathExpression 中,第一个 Segment 可能是 struct, 可能是 enum; 第二个 Segment 可能是 const item, 可能是关联函数,也可能是 enum variant.

因此你可以实现(也可以不实现)类似 rustnamespace 规则,将每个 scopenamespace 分成 type namespacevalue namespace 。如果不实现,在 scope 中把 struct / 函数 / enum / const 的名字分开来,按需寻找,也可以满足语法检查的要求。

Bonus: Control flow in Rx

控制流检查不应该依赖类型检查。实际上 Rust 也是这么做的,看几个例子:

fn test3() -> i32 {
    {
        if (3 > 4) {
            return 3;
        } else {
            return 7;
        }
        1
    };
}

这里 BlockExpression 的类型被推断成 i32,这和我们的规则相符。编译器也知道这一段代码总会返回。

fn test4() -> i32 {
    if ({
        return 1;
        1
    } == 1)
    {
        3;
    };
}

这里 IfExpression 的类型被推断成 i32。实际上除了 return 1 之外,没有表达式被推断成 ! ,但是编译器知道这一段代码总会返回。

你可以在以上两个例子中插入 let y = 以测试类型推断的结果。

上面两个例子说明了,如果用 Rust! 类型来处理控制流,逻辑将会非常复杂。因此我们鼓励大家自己想办法来解决控制流的问题(没有想象中那么难!)

比如,你可以为每个 Expression做“一定返回”的标记,表示执行这个 Expression 的过程中,函数无论经过哪一条控制流,都会返回。这种标记需要遵循一些逻辑,例如 if 表达式。最终如果一个返回类型非 unit 函数的函数体没有标记,报错。

以上仅作参考,可自由选择实现方式。

由于保证没有 dead code,控制流分析被极大地简化了。如果对奇怪的案例感兴趣,可以自行摸索/实现。

Auto Dereferencing / Implicit Borrow

在 Rust 中,有时候即使类型看起来不完全匹配,代码依然能运行,也无需手动添加繁琐的 *(解引用)或 &(引用)符号。这来自于编译器为了提升代码可读性和编写效率而提供的两个特性:解引用转换 (Deref Coercion)点运算符自动引用 (Dot Operator Auto-referencing)

在 Rx 中也是如此,但相较于 Rust 大幅简化。Rx 的自动转换机制暂时只需要考虑 点运算符的自动校准方法调用与数组索引

总结(TL;DR)

rust

Anyway,祝大家 Semantic 愉快!

Upd at September 28, 2025 1:17 AM.

Constants and Constant Context in Rx

Rx 有三种常量环境(constant contexts):

Rx 中,只有在常量环境中,你才需要在编译期确定表达式的值。在上面语法检查过程的第三步,你需要确定所有常量的值,这是为了能顺利地比较类型,为第四步类型检查做准备。为了简化我们的常量求值,我们规定常量环境中只能出现以下表达式:

以下两项可选择实现,作为 Bonus,不会出现在常规数据点中:

也就是说,以下几种表达式不会出现在常量环境中:

说明.

  1. 关于 ArrayExpr. 个人认为,类似 [Expression ; Expression] 这类数组常量似乎(?)没有太大意义. 目前测试点中确实没有数组类型的常量,不过 [Expression ( , Expression )* ,?] 还是有用(例如模拟随机种子、常量打表或者字符串). 故将这两种数组形式留作 Bonus.