【编译器实现笔记】2. 解析器(parser)

原文地址:https://lisperator.net/pltut/

解析器的作用

解析器在分词器之上,直接操作 token 流,不用处理单个字符,把代码解析成一个个对象

lambda 解析器

解析标记流的过程中,当遇到 lambda 关键字则会调用parse_lambda函数

fib = lambda (n) if n < 2 then n else fib(n - 1) + fib(n - 2);
function parse_lambda() {
  return {
    type: 'lambda',
    vars: delimited('(', ')', ',', parse_varname),
    body: parse_expression(),
  };
}

delimited 函数:获取形参列表

// parser 是一个 function,负责解析 start 和 stop 之间的 token
function delimited(start, stop, separator, parser) {
  var a = [],
    first = true;

  // skip_punc(token):当前 token 是否是给定的符号,若是,将其从输入流中丢弃并继续,否则抛出异常
  skip_punc(start);
  while (!input.eof()) {
    // is_punc(token):若当前 token 是给定的符号,返回 true(不消耗掉当前 token)
    if (is_punc(stop)) break;

    // first 标识当前 token 是否是第一个
    // 因为参数的格式是这样的(arg1, arg2, arg3...)
    // 除去第一个参数之外,每次读一个新参数之前都要先把","给读走
    if (first) first = false;
    else skip_punc(separator);

    // 没有和之前的重复
    // 加上这个的原因是防止(arg1, arg2, arg3,)的情况,多了一个逗号
    // while 开头的 is_punc(stop) 就拦截不下来了,而是继续掠过","读下一个参数,当然这个时候是读到是")",出问题了
    if (is_punc(stop)) break;

    // 解析出参数名
    a.push(parser());
  }

  skip_punc(stop);
  return a;
}

parse_expression 函数:解析表达式

尽可能地向右扩展一个表达式

function parse_expression() {
  return maybe_call(function () {
    return maybe_binary(parse_atom(), 0);
  });
}

有两种可能性:

  1. 表达式为 f(a); 类型,调用函数
  2. 表达式为 c = a + b; 类型,就是普通的表达式

maybe_call 函数:如果是后面是调用函数,就拿一个 call 类型的对象把它包裹起来;如果不是就直接返回表达式本身

function maybe_call(expr) {
    // expr 是 maybe_binary() 的返回值
    expr = expr();

    // 如果在那个疑似二元表达式之后,有一个 "(" 那就是说明调用函数型表达式,交给 parse_call(expr) 处理
    // 如果不是,说明是普通表达式,直接返回就好了
    // 例:f(a);
    // expr 传进去的是 function() { f }
    // f 后面跟着一个 (,所以是调用函数型表达式
    return is_punc("(") ? parse_call(expr) : expr;
}// 处理调用函数型表达式:用一个 call 类型的对象包起来
function parse_call(func) {
    return {
        type: "call",
        func: func,
        args: delimited("(", ")", ",", parse_expression)
    };
}

maybe_binary:如果后面跟的是一个二元表达式,那就用一个结点(可能是 binary 类型,也可能是 assign 类型)包裹住它;如果不是就直接返回

谈到二元表达式就避不开操作符优先级的话题,这个用一个 PRECEDENCE 对象解决

// 定义操作符优先级,越大优先级越高
var PRECEDENCE = {
  '=': 1,
  '||': 2,
  '&&': 3,
  '<': 7,
  '>': 7,
  '<=': 7,
  '>=': 7,
  '==': 7,
  '!=': 7,
  '+': 10,
  '-': 10,
  '*': 20,
  '/': 20,
  '%': 20,
};

实现思路:

1 + 2 * 3;

初始化:

  1. 读一个原子表达式(1)
  2. 取当前运行时的优先级({INIT,0}):my_prec 初始化为 0
  3. 调用 maybe_binary(left, my_prec),左边的是表达式,右边的是运行时当前的优先级

maybe_binary 将会解析紧跟着原子表达式的内容:

  1. 紧跟的不是运算符,直接原样返回左参数(1)
  2. 是运算符,但优先级低于 my_prec,返回左参数(1)
  3. 是运算符且优先级更高({+,10} > {INIT,0}),
    1. 将左参数(1)包裹到一个新的二元表达式 “binary” 节点中
    2. 递归调用 maybe_binary,找出右参数(2…)的具体值:
      1. 读一个原子表达式(2)
      2. 取当前运行时的优先级({+,10})
      3. 调用 maybe_binary(left, my_prec),左边的是表达式,右边的是运行时当前的优先级

递归进去:maybe_binary(2, 10) 将会解析紧跟着原子表达式(2)的内容(*)

  1. 紧跟的不是运算符,直接原样返回左参数(2)
  2. 是运算符,但优先级低于 my_prec,返回左参数(2)
  3. 是运算符且优先级更高({*,20} > {+,10}),
    1. 将左参数包裹到一个新的二元表达式 “binary” 节点中
    2. 递归调用 maybe_binary,找出右参数(3…)的具体值:
      1. 读一个原子表达式(3)
      2. 取当前运行时的优先级({*,20})
      3. 调用 maybe_binary(left, my_prec),左边的是表达式,右边的是运行时当前的优先级

递归进去:maybe_binary(3, 20) 将会解析紧跟着原子表达式(2)的内容(*)

  1. 后面跟的是";",不是运算符,直接原样返回左参数(3)
// my_prec 初始化为 0
function maybe_binary(left, my_prec) {
  var tok = is_op();
  if (tok) {
    var his_prec = PRECEDENCE[tok.value];
    if (his_prec > my_prec) {
      input.next();
      var right = maybe_binary(parse_atom(), his_prec);
      var binary = {
        type: tok.value == '=' ? 'assign' : 'binary',
        operator: tok.value,
        left: left,
        right: right,
      };

      // 为什么上面递归过了还要再递归一次?直接 return binary 不行吗?
      // 原因:以 a * b + c * d 为例:
      // 第一层调用:a,返回
      // {
      //   left: a,
      //   right: maybe_binary(b,*)
      // }
      // 第二层调用:b,返回 b
      // 然后就断了
      // 返回 maybe_binary(binary, my_prec) 是为了让这个过程继续进行下去,以现有被分析好的
      // {
      //   left: a,
      //   right: b
      // }
      // 为左参数,接着向右拓展
      return maybe_binary(binary, my_prec);
    }
  }
  return left;
}

parse_atom:解析原子表达式

parse_atom() 依据当前的 token 进行调度

function parse_atom() {
  return maybe_call(function(){
    // 如果解析到了一个"(",则其必定是一个括号表达式 — 因此首先会跳过开括号,然后调用 parse_expression(),然后跳过")"
    if (is_punc("(")) {
      input.next();
      var exp = parse_expression();
      skip_punc(")");
      return exp;
    }// 如果解析到了某个关键字,则会调用对应关键字的解析函数
    if (is_punc("{")) return parse_prog();
    // is_kw 和 is_punc 是一样的,只是一个是针对字符串一个是针对字符,若当前 token 是给定的符号,返回 true(不消耗掉当前 token)
    if (is_kw("if")) return parse_if();
    if (is_kw("true") || is_kw("false")) return parse_bool();
    if (is_kw("lambda") || is_kw("λ")) {
      input.next();
      return parse_lambda();
    }

    // 如果解析到了一个常量或者标识符,则会原样返回 token
    var tok = input.next();
    if (tok.type == "var" || tok.type == "num" || tok.type == "str")
      return tok;

    // 如果所有情况都未满足,则会调用 unexpected() 抛出一个错误。
    unexpected();
  });
}
parse_prog:解析语句序列

当期望是一个原子表达式但解析到 { 的情况,调用 parse_prog 来解析整个序列的表达式,这里有一个优化,如果只有一个表达式就直接返回那个表达式,不再套一层了

var FALSE = { type: 'bool', value: false };

function parse_prog() {
  var prog = delimited('{', '}', ';', parse_expression);

  // 如果 prog 节点为空,则直接返回 FALSE
  if (prog.length == 0) return FALSE;

  // 如果程序只包含一个表达式,则返回表达式的解析结果
  if (prog.length == 1) return prog[0];

  // 否则返回一个包含表达式的 "prog" 节点
  return { type: 'prog', prog: prog };
}
parse_if:解析 if 语句
if a <= b then {             # 这里的 then 是可选的
  print(a);
  if a + 1 <= b {
    print(", ");
    print-range(a + 1, b);
  } else println("");        # newline
};
function parse_if() {
  // 类似 skip_punc
  skip_kw('if');

  // cond 是条件
  var cond = parse_expression();

  // 如果条件之后不是直接跟着 "{",那肯定是跟着 "then" 了
  if (!is_punc('{')) skip_kw('then');

  // then 是当条件为 true 是要处理的表达式
  var then = parse_expression();

  // 用一个 if 类型的对象把 cond 和 then 包起来
  var ret = { type: 'if', cond: cond, then: then };

  // 如果有 else 的话把 else 也包起来
  if (is_kw('else')) {
    input.next();
    ret.else = parse_expression();
  }
  return ret;
}

以上这些函数似乎在互相调用:

  1. parse_atom() 函数基于当前的 token 来调用其它函数,如 parse_if()
  2. parse_if()调用 parse_expression()
  3. parse_expression()会再次调用 parse_atom()

之所以没有发生死循环,是因为每步处理中,每个函数都会至少消费掉一个 token。

上述类型的解析器叫做 “递归下降解析器”(recursive descent parser),也可能算是可以手写实现的最简单类型。

整体程序(prog 节点)解析器

通过不停地调用 parse_expression() 函数来读取输入流中的表达式(expression)

function parse_toplevel() {
  var prog = [];
  while (!input.eof()) {
    prog.push(parse_expression());

    // 表达式以分号分隔,跳过分号再进行下一个expression的解析
    if (!input.eof()) skip_punc(';');
  }
  return { type: 'prog', prog: prog };
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值