90分钟实现一门编程语言(极简解释器教程)

2019-12-30 14:54:22王冬梅

语法树生成

得到了词素之后,接下来就是进行语法分析。不过由于 Lisp 类语言的程序即是语法树,所以语法分析可以直接跳过。

以下面的程序为例:

程序即语法树


;
(def x (if (> a 1) a 1))
; 换一个角度看的话:
(
  def
  x
  (
    if
    (
      >
      a
      1
    )
    a
    1
  )
)

更加直观的图片:

c#,极简解释器

这使得抽象语法树(Abstract Syntax Tree)的构建变得极其简单(无需考虑操作符优先级等问题),我们使用 SExpression 类型定义 iScheme 的语法树(事实上S Expression也是Lisp表达式的名字)。

抽象语法树的定义


public class SExpression {
  public String Value { get; private set; }
  public List<SExpression> Children { get; private set; }
  public SExpression Parent { get; private set; }
  public SExpression(String value, SExpression parent) {
    this.Value = value;
    this.Children = new List<SExpression>();
    this.Parent = parent;
  }
  public override String ToString() {
    if (this.Value == "(") {
      return "(" + " ".Join(Children) + ")";
    } else {
      return this.Value;
    }
  }
}

然后用下面的步骤构建语法树:

    碰到左括号,创建一个新的节点到当前节点( current ),然后重设当前节点。 碰到右括号,回退到当前节点的父节点。 否则把为当前词素创建节点,添加到当前节点中。

抽象语法树的构建过程


public static SExpression ParseAsIScheme(this String code) {
  SExpression program = new SExpression(value: "", parent: null);
  SExpression current = program;
  foreach (var lex in Tokenize(code)) {
    if (lex == "(") {
      SExpression newNode = new SExpression(value: "(", parent: current);
      current.Children.Add(newNode);
      current = newNode;
    } else if (lex == ")") {
      current = current.Parent;
    } else {
      current.Children.Add(new SExpression(value: lex, parent: current));
    }
  }
  return program.Children[0];
}

注意

使用 自动属性(Auto Property),从而避免重复编写样版代码(Boilerplate Code)。 使用 命名参数(Named Parameters)提高代码可读性: new SExpression(value: "", parent: null) 比 new SExpression("", null) 可读。 使用 扩展方法 提高代码流畅性: code.Tokenize().ParseAsIScheme 比 ParseAsIScheme(Tokenize(code)) 流畅。 大多数编程语言的语法分析不会这么简单!如果打算实现一个类似C#的编程语言,你需要更强大的语法分析技术: 如果打算手写语法分析器,可以参考 LL(k), Precedence Climbing 和Top Down Operator Precedence。 如果打算生成语法分析器,可以参考 ANTLR 或 Bison。