《Crafting Interpreters》阅读笔记(二)

《Crafting Interpreters》中 jlox(Part I 中用 java 实现的 Lox 语言)用了 recursive descent parsing 去实现 parser。为了偷懒,下面用 RD 代指 recursive descent parsing 这个方法。

设计简易计算器

假如我们现在要实现一个支持加减乘除的计算器,那 parser 应该怎么设计?

  • 并不是所有的输入都是支持的,比如输入“1 +/ 2”
  • 不同运算符优先级不一样,考虑“1 + 2 * 3”,应该先算乘法,再算加法
  • 当运算符优先级一样时,应该优先算靠左边的

上面三条都是在定义或者限制计算器的语法规则,我们将语法规则用一套符号语言去表示。

Expr -> Expr + Term     (1)
      | Expr - Term     (2)
      | Term            (3)
Term -> Term * Factor   (4)
      | Term / Factor   (5)
      | Factor          (6)
Factor -> Number        (7)

每一行表示一条语法规则,为了方便描述,给每条规则都标了号,这里一共有 7 条规则。上面的这套符号应该怎么理解呢?箭头左边的表达式可以转换成右边的表达式,“|”表示逻辑或,比如说 Expr 可以展开成 Expr + Term, Expr - Term 和 Term 中的任何一个, 以"1 + 2 * 3" 为例列出每一步展开的过程。

0.               Expr
1. 应用规则1      Expr + Term
2. 应用规则4      Expr + Term * Factor
3. 应用规则7      Expr + Term * Number
4. 应用规则6      Expr + Factor * Number
5. 应用规则7      Expr + Number * Number
6. 应用规则3      Term + Number * Number
7. 应用规则6      Factor + Number * Number
8. 应用规则7      Number + Number * Number

不同运算符的优先级能在上述语法规则中体现吗?可以的,箭头左边的表达式,从上往下优先级递增。

上述语法真的能 work 吗?这里其实有一个在 RD 中非常出名的问题——Left Recursion

回到我们展开“1 + 2 _ 3”这里例子,在第 4 行的时候我们应用规则 6,将 Term 展开成 Factor,得到了 Expr + Factor _ Number。Term 展开成了 Factor,为什么不是 Term _ Factor,不是 Term / Factor 呢?这里我们站在上帝视角,知道应该走 Term → Factor → Number 路径才是正确的,但是根据我们的语法规则定义,由“|”连接起来的都是平等的。好,现在放弃上帝视角,Term 展开成 Term _ Factor,Term _ Factor 中的 Term 又可以展开成 Term _ Factor,现在变成了 Term _ Term _ Factor,以此类推,将无线递归下去。stack overflow!!!

怎么解决这个问题呢?其实很简单,表达式展开的时候不要把自身写在最左边

Expr -> Term + Expr
      | Term - Expr
      | Term
Term -> Factor * Term
      | Factor / Term
      | Factor
Factor -> Number

写成这样的语法规则能达到我们最开始设计时的那三点要求吗?oh,我们好像还没讨论运算符优先级相同时的情况。

考虑“1 + 2 + 3”会被 parse 成什么?答案是“(1 + (2 + 3))”,竟然是先算右边的,你可能会觉得先算右边好像也没啥大不了嘛,结果都一样,为啥要强调得先算左边呢?那试试“1 - 2 - 3”呢?为什么会先算右边呢?你可以尝试根据语法规则一步步展开推一下。感兴趣的话,这儿有一份用 python 实现的 parser,语法比我们这个略微复杂一点,(代码不是我写的,我甚至都没有看过,我只看了大佬的博客)。

刚刚讨论的其实是 parser 中一个非常重要的问题,叫 Asscociativity,像“+”,“-”,“*”,“/”都是 left-associative,“=”是一个典型的 right-associative,“a = b = c”应该被 parse 为 “a = (b = c)”。

Expr -> Term {+ Term}
      | Term {- Term}
      | Term
Term -> Factor {* Factor}
      | Factor {/ Term}
      | Factor
Factor -> Number

“{}”表示括号内可以重复一次或多次。(忘了“{}”这个叫中括号还是大括号了。。。)

这次它真的可以解决 left-associative 的问题,不信你可以推一下(狗头)。

优缺点

Recursive descent parsers are fast, robust, and can support sophisticated error handling. In fact, GCC, V8 (the JavaScript VM in Chrome), Roslyn (the C# compiler written in C#) and many other heavyweight production language implementations use recursive descent.

这是《Crafting Interpreters》中说的,又快有稳定,GCC 都在用。但是下面两篇文章都指出 RD 有性能问题,每个优先级在语法规则中都会有一个单独的 level,直白点,代码中每个优先级会对应一个函数,优先级层数越多,对应的函数也就越多。即使输入只有一个 token “1”,也需要先后调用 expr,term,factor 函数。

https://web.archive.org/web/20191231231734/www.engr.mun.ca/~theo/Misc/exp_parsing.htm

https://eli.thegreenplace.net/2009/03/14/some-problems-of-recursive-descent-parsers/

这一块还没有怎么调研,先挖个坑吧。

参考

贴一下我觉得有用的文章,有用程度和先后顺序有关

https://eli.thegreenplace.net/2009/03/14/some-problems-of-recursive-descent-parsers/

https://web.archive.org/web/20191231231734/www.engr.mun.ca/~theo/Misc/exp_parsing.htm

《Engineering A Compiler (second edition)》3.3 left recursion

http://craftinginterpreters.com/parsing-expressions.html#ambiguity-and-the-parsing-game

Implementing it in Rust

最后用 rust 实现了最终版的 RD,手动 lexer,哈哈哈

/*
Expr -> Term {+ Term}
      | Term {- Term}
      | Term
Term -> Factor {* Factor}
      | Factor {/ Factor}
      | Factor
Factor -> Number

 */
use std::fmt;
use std::iter::Peekable;

#[derive(Debug, Clone)]
enum Token {
    Number(u32),
    Plus,
    Minus,
    Star,
    Slash,
}

impl fmt::Display for Token {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Token::Number(n) => write!(f, "{}", n),
            Token::Plus => write!(f, "+"),
            Token::Minus => write!(f, "-"),
            Token::Star => write!(f, "*"),
            Token::Slash => write!(f, "/"),
        }
    }
}

#[derive(Debug)]
enum Expr {
    Number(u32),
    Binary(Box<Expr>, Token, Box<Expr>),
}

impl fmt::Display for Expr {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            Expr::Number(n) => write!(f, "{}", n),
            Expr::Binary(lhs, token, rhs) => {
                write!(f, "({} ", lhs)?;
                write!(f, "{} ", token)?;
                write!(f, "{})", rhs)
            }
        }
    }
}

fn expr<I: Iterator<Item = Token>>(token_iter: &mut Peekable<I>) -> Expr {
    term(token_iter)
}

fn term<I: Iterator<Item = Token>>(token_iter: &mut Peekable<I>) -> Expr {
    let mut lhs = factor(token_iter);
    while let Some(token) = token_iter.peek().cloned() {
        match token {
            Token::Plus | Token::Minus => {
                token_iter.next();
                let rhs = factor(token_iter);
                lhs = Expr::Binary(Box::new(lhs), token, Box::new(rhs));
            }
            _ => break,
        }
    }
    lhs
}

fn factor<I: Iterator<Item = Token>>(token_iter: &mut Peekable<I>) -> Expr {
    let mut lhs = primary(token_iter);
    while let Some(token) = token_iter.peek().cloned() {
        match token {
            Token::Slash | Token::Star => {
                token_iter.next();
                let rhs = primary(token_iter);
                lhs = Expr::Binary(Box::new(lhs), token, Box::new(rhs));
            }
            _ => break,
        }
    }
    lhs
}

fn primary<I: Iterator<Item = Token>>(token_iter: &mut Peekable<I>) -> Expr {
    if let Some(Token::Number(n)) = token_iter.peek().cloned() {
        token_iter.next();
        return Expr::Number(n);
    }
    panic!("No more tokens left")
}

#[test]
fn tests() {
    let tokens = vec![
        Token::Number(1),
        Token::Plus,
        Token::Number(2),
        Token::Slash,
        Token::Number(3),
    ];
    let s = expr(&mut tokens.into_iter().peekable());
    assert_eq!(s.to_string(), "(1 + (2 / 3))");

    let tokens = vec![
        Token::Number(1),
        Token::Minus,
        Token::Number(2),
        Token::Plus,
        Token::Number(3),
    ];
    let s = expr(&mut tokens.into_iter().peekable());
    assert_eq!(s.to_string(), "((1 - 2) + 3)");
}