Skip to content

Latest commit

 

History

History
96 lines (71 loc) · 5.11 KB

2.md

File metadata and controls

96 lines (71 loc) · 5.11 KB

第二阶段:foo 与 bar

本阶段的任务是简单实现变量。暂时不考虑变量的类型。

语法

与变量有关的语法有几个:

  1. 声明 - 变量必须被显式定义,同时,被赋予一个默认值。
  2. 赋值 - 变量可以被赋值。
  3. 取值 - 变量的值被取出,以参与计算。
  4. 1 和 2 的结合 - 声明的同时赋值一个变量。
let foo
foo = 2
let bar = foo + 1
bar

需要的 AST 定义如下。最后一个Block并不跟变量直接相关,但是这个阶段需要的,变量相关语法在各个表达式之间有相互联系,Block作为多个表达式的简单容器,组织一个表达式列表。

VarDef(String),
Assign(String, Box<Expr>),
Variable(String),

Block(Vec<Expr>),

开发

代码和文档都在phase2分支上。其他阶段也以此类推。

解析器

解析方面比较简单。虽然增加了挺多解析函数,但原理上没有大的不同。唯一需要提一下的是出现了一个语句需要解析为多个执行概念的情况。def_and_assign_par对应上面的第四中语法“声明的同时赋值一个变量”。这里有两种做法。一种,是为他单独设计一个 AST 节点(表达式)类型,语句直接解析成这种类型,比如叫VarDefAssign,然后在编译阶段,把这种 AST 节点转换为与之等价的VarDef+Assign。另一种,直接把语句解析为VarDef+Assign两个 AST 节点。我这里采用的是第二种。

AST 和编译

编译方面有一个比较大不同。 之前的编译都是以一个表达式(现阶段表达式是 AST 的基本单位,也就是代码中的Expr)为单位,不需要考虑到其他表达式的影响。现在需要考虑。一个表达式声明一个变量,而接下来的表达式对这个变量进行赋值或取值,这两个表达式是有相互关联的。 因为根据我们的设计,没有声明的变量不合法,这里就是一个跨越表达式的关联规则。 我们的代码有如下的改变。除开名字变了,重要的是多了个Compiling对象,这个对象,作为编译过程中保持上下文状态,前后不同的表达式通过此对象进行沟通。 Screen Shot 2022-03-21 at 17.20.46

pub trait Compile {
    fn compile(&self, compiling: Compiling) -> Compiling;
}
...
pub struct Compiling {
    pub instructions: Vec<Instruction>,
    pub locals: Vec<String>,
    pub errors: Vec<Error>,
}

compiling实例,是贯穿整个编译过程的,其中的 instructions、locals、errors 这些数据,就是前后不同表达式之间共享的数据,前面的表达式如果是“定义变量”,就会在 locals 里面增加这个变量的定义,后面的表达式通过 locals,就能知道这个变量之前是否已经定义。

这里采用了传入一个 compiling 对象,再传出一个新对象的风格,这个传出的新对象,融合了传入的对象,并添加了新的内容。如果习惯采用一直修改同一个对象的风格,也是同样的效果。

WASM

编译结果方面,本阶段出现了个新的概念:local,用于处理本地变量。同时也提供了GetLocalSetLocal这样的指令,用于将本地变量和堆栈打通。 在 WASM 中,一个function execution,可以对应一组 local,可以认为是函数对应的一组内存空间,在函数切换的时候,local 对应的内存也会自动切换,始终与当前函数保持对应。 详细介绍这些概念超出了本文范围,但确实需要有一些了解。比如下面这个文档。目前系统全面介绍 WASM 的文档还比较少,特别是中文的,同学们有发现好的也请随时分享一下。 https://github.com/sunfishcode/wasm-reference-manual/blob/master/WebAssembly.md#function-execution

错误处理

这个方面第一阶段没有说明,这里讲一下。Rust 的错误处理机制很好用。 下面这段代码出现在主模块,也就是src/main.rs中,对全部各个阶段可能出现的错误是个概括、总结的作用。Error 的三个值,相当于定义了三个子错误类型。我这里设计的子错误类型,对应的是三个主要的阶段:解析、编译、运行。不同阶段发生的错误放入不同的子错误类型。如果需要进一步信息,可以利用值里面所包含的数据。这里都是String,后续可以进一步精细设计。比如ParseError里面放入 nom 定义的源错误等。

pub enum Error{
    ParseError(String),
    CompileError(String),
    RuntimeError(String),
}

类似于下面这种过程,通过把 Error 返回,可以兼容所有错误子类型,

fn run(s: &str) -> Result<i32, Error> {

Result作为 Rust 中的高等公民,是错误处理的标准模式,标准库和大量的第三方库都遵循这个模式,配合?等小语法糖,我认为非常好用。任意一个错误类型,只要写上一个隐式转换。

impl From<nom::error::Error<&str>> for Error {
    fn from(e: nom::error::Error<&str>) -> Self {
        Error::ParseError(format!("{:?}", e))
    }
}