解析器组合子

Background

在构建一个自顶向下的语法分析器时,很多情况下需要处理复杂的模式匹配来完成解析。比如对于文法:

E ::= '[' A ']' | '(' B ')'

需要写出如下 OCaml 代码:

let parseE input = 
  match input with 
  | '[' :: rest -> 
      let (a, rest') = parseA rest in
      (match rest' with 
       | ']' :: rest'' -> (E_A a, rest'')
       | _ -> failwith "Expected ']'")
  | '(' :: rest -> 
      let (b, rest') = parseB rest in
      (match rest' with 
       | ')' :: rest'' -> (E_B b, rest'')
       | _ -> failwith "Expected ')'")

或者更简单一些:

let parseE input =
  if String.length input >= 2 then
    match input.[0], input.[String.length input - 1] with
    | '[', ']' -> E_A (parseA (String.sub input 1 (String.length input - 2)))
    | '(', ')' -> E_B (parseB (String.sub input 1 (String.length input - 2)))
    | _ -> failwith "Expected '[' or '('"
  else
    failwith "Input too short"

但是,对于包含更多的终结符的文法,这个模式匹配只会更加复杂,严重影响代码的可维护性。可以使用解析器组合子来处理这个问题。

Parser combinator

解析器组合子的核心思想是:利用小的解析器“组合”成大的解析器。

假设词法单元的类型已经被定义为 token

type token = 
  | Ident of string
  | Number of int
  | Bool of bool
  | Bracket of char
  | Symbol of string

那么给出下面的解析器的定义:

type ['a] parser = token list -> ('a * token list) option

类型 parser 代表一类满足下面描述的函数:接受一个 token 列表,如果解析成功,返回一个解析结果和剩余的令牌列表,否则返回 None

为了接下来可以通过链式调用的方式把小的解析器组合起来,使用 OCaml 的 object 来包装一下 parser

class ['a] parser_obj (p : 'a parser) = object (self)
  method parse = p
end

可以定义一个函数 ptoken,函数接受一个函数 predicate 作为函数参数,返回一个解析器:如果 token 列表里面的第一个 token 满足 predicate(也就是 predicate 返回 true),那么就认为解析成功

method ptoken predicate = 
    let token_parser input =
      match input with
      | hd::tl when predicate hd -> Some (hd, tl)
      | _ -> None
    in
    new parser_obj token_parser

还可以定义组合子 map,它的作用是对解析器的结果进行变换。比如说,如果你有一个解析器能解析出一个 token,你可以用 map 把它变成你想要的任何类型(通常表现为丢掉某些解析结果或者把 token 变成字面量)。'b 是要转化的目标类型,'b. 表示该方法对任意类型 'b 都适用,('a -> 'b) 表示给出的转化函数 f 的类型,最后返回一个 'b parser_obj

method map : 'b. ('a -> 'b) -> 'b parser_obj =
  fun f ->
    let mapped_parser input =
      match self#parse input with
      | Some (a, rest) -> Some (f a, rest)
      | None -> None
    in
    new parser_obj mapped_parser

combine 组合子的作用是把两个解析器顺序组合起来,先用第一个解析器解析输入,如果成功,再用第二个解析器解析剩下的输入。最终返回两个解析结果的元组:

method combine : 'b. 'b parser_obj -> ('a * 'b) parser_obj =
  fun (other : 'b parser_obj) ->
    let combined_parser input =
      match self#parse input with
      | Some (a, rest1) -> (
          match other#parse rest1 with
          | Some (b, rest2) -> Some ((a, b), rest2)
          | None -> None
        )
      | None -> None
    in
    new parser_obj combined_parser

如果你想要“尝试”多个解析器,只要有一个成功就算成功,可以用 either 组合子:

method either : 'a parser_obj -> 'a parser_obj =
  fun (other : 'a parser_obj) ->
    let either_parser input =
      match self#parse input with
      | Some _ as res -> res
      | None -> other#parse input
    in
    new parser_obj either_parser

最后,many 组合子重复使用同一个解析器,直到不能再解析为止,返回所有解析结果的列表:

method many : 'a list parser_obj =
  let rec many_parser input =
    match self#parse input with
    | Some (a, rest) -> (
        match many_parser rest with
        | Some (lst, rest') -> Some (a :: lst, rest')
        | None -> Some ([a], rest)
      )
    | None -> Some ([], input)
  in
  new parser_obj many_parser

Usage

比如对于文法

E = E + T | T
T = T * F | F
F = ( E ) | number
type expr =
  | Add of expr * expr
  | Mul of expr * expr
  | Num of int
  | Paren of expr

let parser = new parser_obj

let pnumber =
  parser#ptoken (function Number _ -> true | _ -> false)
  #map (function Number n -> n | _ -> failwith "not a number")
  #map (fun n -> Num n)

let lparen = parser#ptoken (function Bracket '(' -> true | _ -> false)
let rparen = parser#ptoken (function Bracket ')' -> true | _ -> false)
let plus   = parser#ptoken (function Symbol "+" -> true | _ -> false)
let star   = parser#ptoken (function Symbol "*" -> true | _ -> false)

let rec pE tokens = pE_obj#parse tokens
and pT tokens = pT_obj#parse tokens
and pF tokens = pF_obj#parse tokens

and pE_obj =
  let rec_e_plus_t =
    (new parser_obj pE)
    #combine plus
    #combine (new parser_obj pT)
    #map (fun ((e, _), t) -> Add (e, t))
  in
  rec_e_plus_t#either (new parser_obj pT)

and pT_obj =
  let rec_t_mul_f =
    (new parser_obj pT)
    #combine star
    #combine (new parser_obj pF)
    #map (fun ((t, _), f) -> Mul (t, f))
  in
  rec_t_mul_f#either (new parser_obj pF)

and pF_obj =
  (lparen#combine (new parser_obj pE)#combine rparen
    #map (fun ((_, e), _) -> Paren e))
  #either pnumber

let parse_expr tokens = pE_obj#parse tokens
posted @ 2025-08-23 08:59  sysss  阅读(10)  评论(0)    收藏  举报