框架中部分实现的解释

虽然现有的语义检查只是针对decaf的基本语法的,但是了解一下其中比较tricky的实现也是有必要的,因为新语法有可能需要修改和拓展原有的语义检查。

SymbolPass::var_def

这个函数处理了类的字段,函数参数和局部变量的定义。

fn var_def(&mut self, v: &'a VarDef<'a>) {
  v.ty.set(self.ty(&v.syn_ty, false));
  if v.ty.get() == Ty::void() { self.issue(v.loc, VoidVar(v.name)) }
  let ok = if let Some((sym, owner)) = self.scopes.lookup(v.name) {
    // 在类中定义变量可能产生两种错误,分别为:OverrideVar(与父类定义的变量重名)和ConflictDeclaration(重名的所有其它情形))
    // 在函数参数和局部变量中定义时,只要发现旧的定义是参数或局部变量中定义的,就一定是一个ConflictDeclaration
    // 这是因为decaf并不允许块级别的variable shadowing(我个人觉得这并不合理,如果允许,写程序会更方便一些,像rust一样的同一个语句块也可以shadow就更方便了)
    // 上面说的这些在新语法中都未必成立,所以你可以酌情考虑利用这段代码,但不保证不需要修改
    match (self.scopes.cur_owner(), owner) {
      (ScopeOwner::Class(c1), ScopeOwner::Class(c2)) if Ref(c1) != Ref(c2) && sym.is_var() =>
        self.issue(sym.loc(), OverrideVar(v.name)),
      (ScopeOwner::Class(_), ScopeOwner::Class(_)) | (_, ScopeOwner::Param(_)) | (_, ScopeOwner::Local(_)) =>
        self.issue(v.loc, ConflictDeclaration { prev: sym.loc(), name: v.name }),
      _ => true,
    }
  } else { true };
  if ok {
    v.owner.set(Some(self.scopes.cur_owner()));
    self.scopes.declare(Symbol::Var(v));
  }
}

TypeCk::cur_var_def

当当前语句是一条VarDef时,会将self.cur_var_def设置成这条语句。目前,它存在的唯一意义是用于帮助拒绝这样的代码:

int a = a;

对此我们的编译器会汇报一个"undeclared variable"的错误。

但是,因为全局就只有这一个self.cur_var_def,容易发现假如VarDef的右端项中也有VarDef时,这个方法可能不能奏效。基础的框架中不可能有这样的情形,但是在引入lambda表达式之后,这样的情形是可能存在的,例如:

var f = fun() {
  var a = f; // 应该汇报undeclared variable,但是cur_var_def已经被设置成本条语句,所以会认为f已经被定义
};

为了在新语法中解决这个问题,我个人推荐的方案是:

  1. VarDef中额外维护一个finish_loc,表示这条语句的结束位置

    • 提示:按照现在的实现,在lalr1的lalr(1)模式下,语法动作中你可以访问变量lexer,它包含了当前的行号和列号信息

    • 如果你不想维护pa1b的结果的话,可以不维护,只要保证编译通过即可

  2. 修改ScopeStack::lookup_before,使用上面定义的finish_loc来过滤符号

  3. 删除cur_var_def及其相关操作,因为它已经没用了

并非要求一定要这么实现,如果你觉得有更简单优雅的方法(我也的确觉得这个不太优雅),可以随意发挥。

符号表的输出

java/scala版的实验框架中保存了很多无用的信息,这些信息在我看来就是纯粹为输出服务的。我不想在我的框架中保存这些信息,例如Scope就是最简化的定义:

pub type Scope<'a> = HashMap<&'a str, Symbol<'a>>;

信息已经不能再简化了。与之相反,java版的框架中的LocalScope中保存了所有嵌套在它里面的作用域:

private List<LocalScope> nested = new ArrayList<>();

我读过完整的代码了,这个就是只用来输出的。我不喜欢这样的设计,这不符合"Don't pay for whay you don't use."的原则。在我的设计中,为了在输出时找出所有嵌套在它里面的作用域,我会遍历一遍所有的语句。

大家可以按照自己喜欢的方式实现新特性的符号表的输出,不过因为我目前的设计,我相信实现起来一定是会比其它版本难度要大一些的,希望大家理解。

Last updated