第二步:Token到Expr
经过上一步的处理,我们已经获得了代码切割后的Token序列,然而Token中只是简单地存放切割得到的代码片段,解释器仍无法直接执行这样的代码片段,因此接下来我们要做的就是如何把Token转换为解释器能够执行求解的语法树。
青语言是一门动态类型的编程语言,所有的语法元素统一使用Expr表示,这其中既包括单个的值,例如整数、小数、字符串等,也包括实现语法的各种语句。
之前在基础类定义中我们已经给出了Expr的定义,其中Tp属性用来标记Expr的类型,Val通常用来存放单个值,而如果是集合或者语句,需要包括多个语法元素,那么就存放在List里面。由于List属性本身就是Expr列表,所以Expr本身就满足树的节点的定义,通过Expr我们就可以构建青语言的语法树。
Token到Expr的转换由函数MakeExpr实现:
public Expr MakeExpr(List<Token> tks, ref int idx, Ctx ctx, bool checkOp = true) {
……
}
这个函数需要传入上一步得到的Token序列,另外使用一个整数类型的引用idx作为转换过程中位序的标记。转换的过程可能要考虑转换时所处的语境Ctx,这个概念我们在后面的环节再介绍。
另外,转换中可能遇到中缀表达式,而且有可能是连续的中缀表达,这种情况下我们要优先处理中缀表达式,而在构建中缀表达式本身的时候,我们又不希望对中缀符号进行检查,所以我们使用了一个默认为true的checkOp参数,表示是否检查后面的中缀符。
MakeExpr函数的基本逻辑非常简单,逐个遍历传入的Token序列,然后根据Token中的代码片段,对照语法要求,转换为Expr。
例如在函数的开头部分:
/*首先是一些固定字面值的解析*/
if (tk.Str == "空") {
expr = new Expr(TP.None, 0, tk.Line, tk.Src);
idx++;
}
if (tk.Str == "真") {
expr = new Expr(TP.Bool, true, tk.Line, tk.Src);
idx++;
}
if (tk.Str == "假") {
expr = new Expr(TP.Bool, false, tk.Line, tk.Src);
idx++;
}
这些是语法中的固定值的字面表达,如果我们发现Token中的代码片段符合这样的字面表达,那么就可以直接转换为相应的值类型的Expr。
另一种情况下,我们需要构建语句,例如:
/*如果语句*/
if (tk.Str == "如果") {
idx++;
/*向后取一个Expr作为条件判断表达式*/
Expr cond = MakeExpr(tks, ref idx, ctx);
/*再向后取一个Expr作为条件为真的执行体*/
Expr body = MakeExpr(tks, ref idx, ctx);
if (body.Tp != TP.Brace) {
throw new ParseException("如果语句语法错误", tk.Line, tk.Src);
}
expr = new Expr(TP.If, "如果", new List<Expr>(), tk.Line, tk.Src);
expr.List!.Add(cond);
expr.List!.Add(body);
}
这里我们发现Token中的代码片段对应的是语法中的关键字如果,说明这应该是一个如果语句。语法中定义,如果语句接收一个逻辑表达式,然后再接一个代码块(同样用Expr表示)。所以这里其实我们需要做的事情就是向后取两个Expr,又由于生成Expr的函数就是MakeExpr本身,而且作为位序标记的idx是一个引用,所以我们可以直接递归调用MakeExpr向后获取Expr,并且能够保证idx正确的向后移动。
在如果语句这个例子里面,由于任意的表达式的值我们都可以隐式地转换为逻辑值,所以获取的条件表达式我们没有做限定。但是再往后获取到的Expr,根据语法要求就必须是代码块,所以我们做了类型检查,如果不是代码块类型,那么就抛出一个解析异常,直接终止执行。
其他的值表达式和语句表达式也都采用这样的方式来构建,然而接下来我们将遇到转换过程中的一大麻烦——中缀表达式!
与值表达式和语句表达式不同的是,中缀表达式的长度是可变的,可能出现连续的长中缀表达式,所以首先我们需要确定中缀表达式的边界。这里我们使用到的是SkipOpExpr函数:
public void SkipOpExpr(List<Token> tks, ref int idx) {
……
}
这里会对连续的中缀表达式进行检查,一直找到中缀表达式的结尾。
确定边界后,我们再通过BuileOpExpr函数来构建一个中缀表达式的语法树:
public Expr BuileOpExpr(List<Token> tks, ref int idx, Expr expr, Ctx ctx) {
……
}
这个函数的逻辑是,先把所有的Token都转换为单个值类型的Expr,放到一个List中,那么此时这个List中偶数位应该对应的就是中缀运算符,然后构建出这些中缀运算符的优先级列表。
接下来是循环的过程,每次从这个优先级列表中查找优先级最高的位置,然后找到这个位置的中缀Expr,左右两个值Expr放到它的List中,这样相当于把3个Expr合并成了1个Expr,并且是树的结构。之后再从之前的Expr序列中删除左右两个值Expr。
重复以上步骤,最终只剩下1个Expr时,就是我们要构建的中缀表达式的语法树。
以上就是Token转换为Expr的过程,由于程序代码通常包含多个表达式,我们只需要在一个循环中,每次构建1个Expr,那么就可以得到整段代码对应的Expr序列,对应的函数为:
public List<Expr> MakeExprs(List<Token> tks, Ctx ctx) {
……
}
public List<Expr> MakeExprs(List<Token> tks, int start, int end, Ctx ctx) {
……
}
这里有两种方式,第二种可以支持转换的起止位置,默认是从头到尾全部转换。
另外,有些语句也是不定长的,例如如果语句可能出现零至多个再则子句,以及零至一个否则子句。对于这种不定长语句的处理我们也在MakeExprs函数中进行处理。
MakeExprs函数在构建Expr序列的过程中,会记录上一次产生的Expr,那么假设我们这一次构建出一个否则子句,并且上一个Expr是如果语句,那么我们就应该把否则子句加到如果语句的Expr的List中。
以上就是从Token序列转换到Expr序列的主要逻辑。