如何手写一个简单的 parser

前一阵子,收到烨兄的私聊,他忽然要解决这样一个任务:html

作以下格式的表达式转换:前端

  • Multi(a, Multi(b, c)) --> a * (b * c)
  • Divide(a, Sub(b, c)) --> a / (b - c)

支持的运算符有:git

  • Add: +
  • Sub: -
  • Multi: *
  • Divide: /

并且好死不死的须要用他没怎么用过的 C++ 来写。我发现这是一个 parser 的问题,第一反应是推荐他用 flex/bison,但想到为了这么大点任务大费周章不太合适,又开始想手写这样一个表达式的 parser 难不难。最后得出的结论是,不难。程序员

了解编译原理的人都知道什么是 parser。Parser 中文名(语法)分析器,是每一个编译器的前端都会有的一个东西。不过,从编译原理的视角来看,“语言”的范畴要比咱们理解的编程语言要广义得多,任何有必定规则的字符串构成方式,均可以当作是语言,例如上面的那个任务里用 AddSub 这样的函数描述的表达式。github

那么,要解决上面这个任务,只须要对表达式的字符串进行语法分析,获得一个中间表示(通常是分析树或抽象语法树),再将中间表示输出为所需的格式便可。也就是说咱们须要为表达式提供一个 parser,这个任务的任何解决方式,本质上均可以当作是写了一个 parser。编程

在平时,咱们彻底没有任何须要去手写一个 parser,由于这东西已经有工具能够为咱们生成。感谢几十年前伟大的程序员就已经发明了这样的工具。我用过的有 C/C++ 的 flex/bison,以及 Java 的 ANTLR。你只须要提供一个文法描述,这些工具就能够为你自动生成对应的语法分析器。若是要手写分析器,会很复杂,也很容易出错,不是一个明智的选择。数组

不过,面对上面举例的这种小任务,使用自动生成 parser 的工具备时候显得过重了,这时候也许手写一个 parser 是更好的选择。并且在这样的任务场景下,咱们的 parser 有两个地方起码是能够获得大大简化的:app

第一,咱们要处理的语言应该不会像通用编程语言那样,有很复杂的状态转移。一般状况下,应该能看到当前的字符串就知道下面要分析什么类型的内容。通常标记语言都会是这种风格的,好比:编程语言

  • XML/HTML:看到 <tag> 就知道是一个标签的开始,直到 </tag> 为止
  • CSS:选择器后的声明,老是用花括号括起来,每一条声明以 ; 分隔
  • Markdown:一行以 # 开头就是标题,以 1. 开头就是有序列表项

第二,咱们不须要进行复杂的语法错误处理,只须要报“语法错误”就行了,而不须要费力说明到底发生了什么错误。ide

有了这两个前提,咱们开始思考如何手写一个语法分析器。固然,我已经思考好了,下面是我给出的一个简单的分析器的实现。我是用 Java 实现的,用到了一点 lambda 表达式的语法,不过不难理解。由于 parser 的主要工做是作字符串比较,因此用任何语言都差很少。后面我会考虑再用其余语言实现。

在实现上咱们再作一点简化:咱们把要分析的字符串做为字符数组保存下来,而不是从所谓“字符流”中读入。这样咱们没必要考虑读 (get) 了字符却不用掉 (consume) 的状况下,这些是输入模块要考虑的部分,咱们专一于 parser 自己。

首先,咱们的 SimpleParser 是这样定义的:

public class SimpleParser {

    private char[] input;
    private int pos;

    public SimpleParser(String source) {
        this.input = source.toCharArray();
        this.pos = 0;
    }
}
复制代码

咱们将输入保存为字符数组,pos 是一个指向待读取的下一个字符的指针。将 pos 加一,就至关于从读入了一个字符。

下面,咱们添加一些脚手架函数:

private void consumeWhitespace() {
    consumeWhile(Character::isWhitespace);
}

private String consumeWhile(Predicate<Character> test) {
    StringBuilder sb = new StringBuilder();
    while (!eof() && test.test(nextChar())) {
        sb.append(consumeChar());
    }
    return sb.toString();
}

private char consumeChar() {
    return input[pos++];
}

private boolean startsWith(String s) {
    return new String(input, pos, input.length - pos).startsWith(s);
}

private char nextChar() {
    return input[pos];
}

private boolean eof() {
    return pos >= input.length;
}
复制代码

这些函数的来源于我以前看过的一个系列文章:Let's build a browser engine!(原文是用 Rust 语言的)。咱们来看一下这几个函数:

其中,nextChar, startsWith 这两个函数是用来“向后看”,判断后面输入的状态。这实际上已经和编译原理中说的语法分析不太同样了(回忆一下,编译原理中说的语法分析方法只会向后看一个字符),可是由于咱们只是判断是否是等于一个固定的字符串,因此也不是太大的问题。

consume... 开头的几个函数就是真正的读取输入的函数了。其中,consumeWhile 是一个通用的函数,consumeWhitespace 也是基于其实现的。相似地,咱们还能够基于其实现解析变量名的函数:

private String parseVariableName() {
    return consumeWhile(Character::isAlphabetic);
}
复制代码

注意到这实际上就是在解析咱们任务中的变量名了,以此为思路,后面的实现其实很简单。咱们一上来会以为手写 parser 会很复杂,其实是由于没找到入手点。因此这几个脚手架函数特别重要,先有了他们,后面就能够一步一步写出整个 parser 的功能了。

那么咱们接下来能够这么写:

// 解析由单个变量组成的表达式
private VariableExpression parseVariableExpression() {
    String name = parseVariableName();
    // VariableExpression 的定义略
    return new VariableExpression(name);
}
复制代码
// 解析加减乘除表达式
private CompoundExpression parseCompoundExpression(String name) {
    for (char c : name.toCharArray()) {
        checkState(c == consumeChar());
    }
    checkState('(' == consumeChar());
    // 递归解析
    Expression left = parseExpression();
    checkState(',' == consumeChar());
    consumeWhitespace();
    Expression right = parseExpression();
    checkState(')' == consumeChar());
    // CompoundExpression 的定义略
    return new CompoundExpression(name, left, right);
}

// VariableExpression 和 CompoundExpression 都是 Expression
private Expression parseExpression() {
    if (startsWith("Add")) {
        return parseCompoundExpression("Add");
    } else if (startsWith("Sub")) {
        return parseCompoundExpression("Sub");
    } else if (startsWith("Multi")) {
        return parseCompoundExpression("Multi");
    } else if (startsWith("Divide")) {
        return parseCompoundExpression("Divide");
    } else {
        return parseVariableExpression();
    }
}
复制代码

写到这里,咱们 parser 的主要工做已经作完了,接下来的任务就很是简单了。彷佛咱们的任务有点太简单了?在这种场景下,手写 parser 确实不难,接下来能够手写一个 Markdown 的 parser 练习一下了😜。

P.S. 烨兄后来并无作这个任务,我也是到如今才想起来把这个 parser 实现出来,只是我本身以为好玩想了这件事。

文章中的 parser 的完整代码,能够到个人 GitHub 上查看:simpleparser

相关文章
相关标签/搜索