[公告] rsec-0.0.8

night_stalker 2010-02-13
See English document in github

rsec 是一个 Ruby 的动态解析器生成器(parser generator),可以用来构建解析器(parser)。只支持 Ruby 1.9 (不支持 1.8 及 jruby 等)。

注:Antlr、YACC 等工具为静态解析器生成器,Parsec、Boost::spirit 等为动态解析器生成器。一般静态的解析器生成器速度稍快,应用广泛,但是动态生成器可以在运行时变化,使用起来灵活得多。另外引入了 packrat parsing 等手段后,速度已经不慢了。

和 rparsec 的一个区别是底层为 StringScanner,没有 char() parser。

安装:
gem install rsec


helloworld 中的 helloworld: (最简单的解析器:匹配一个字符串)
require 'rsec'
parser = 'abcd'.r # String#r 方法将它转变成解析器
parser.parse 'abc'  #=> 不匹配,我们失败了。
parser.parse 'abcd' #=> "abcd" 我们成功了!


复杂的解析器都是从简单的解析器慢慢成长起来的。通过 parser combinator,我们可以极大简化构建解析器的过程。

synopsis: (helloworld 真达人)
# coding:utf-8
# 四则运算,永远的 helloworld
require "rsec"
include Rsec::Helpers

def arithmetic
  calculate = proc do |(p, *ps)|
    ps.each_slice(2).inject(p) do |left, (op, right)|
      left.send op.strip, right
    end
  end

  num    = /[+-]?[1-9]\d*(\.\d+)?/.r.map &:to_f
  bra    = /\(\s*/.r.skip
  ket    = /\s*\)/.r.skip
  expr   = nil # declare for lazy
  paren  = bra >> lazy{expr} << ket
  factor = num | paren
  term   = factor.join(/\s*[\*\/]\s*/).map &calculate
  expr   = term.join(/\s*[\+\-]\s*/).map &calculate
  expr.eof
end

str = '1+2-3*4*5/((2*3)+6)-7'
print str, ' = '
puts arithmetic.parse str #=> -9
puts "ruby gets #{eval str}, too!" #=> 如果我没算错的话,应该也是 -9


tricks
1. lazy{expr}:在 paren 规则调用 expr 的时候, expr 还没定义,所以要用 lazy 去包起它,等到 parse 的时候再找 expr。
用 lazy 之前需要先声明 expr,避免 not defined variable or methods。
2. 二元操作符 combinator 的右边可以直接用 String 或者 Regexp,可以自动转换成对应的 parser。
3. 在 map 中返回 Rsec::SKIP 也能产生 skip parser 的效果。

源代码(欢迎添加 issue):
http://github.com/luikore/rsec
night_stalker 2010-02-14
parser 方法汇总

String#rRegexp#r 产生原子 parser。
原子 parser 调用各种方法或者运算符可以产生新的 parser。
最后调用 parser 的 parse 方法就 OK 了,相当的简单呢。
(注:parse 解析失败返回 Rsec::INVALID。
parse! 会对语法错误产出异常。异常类型为 Rsec::ParseError,其 to_s 方法可以打印错误的行列和出错位置附近的代码。)

下面假设 parser 都是一个 parser,那么以下方法都能产生一个新的 parser。


基本组合子
--------------------------------------------------------
[par, ser, par].r   顺序 parser,也就是先匹配 par,然后匹配 ser,再匹配 par,输出的结果是一个数组,对应 [par, ser, par]

[par, ser].r skip: /\ /   顺序 parser,元素之间的空格会被跳过。

par >> ser  先匹配 par,然后匹配 ser,输出 ser 的匹配结果。

par << ser  先匹配 par,然后匹配 ser,输出 par 的匹配结果。

par | ser  如果 par 失败,便尝试 ser

par._?  出现 0 次或一次 (不出现时返回 Rsec::SKIP)

par * n   par 连续出现 n 次。

par * (n..m)  par 连续出现 n.abs 到 m 次。
如果 m < 0,par 至少匹配 n 次。(即匹配 n 到无穷次)

par ** n  至少匹配 n 次的另一种写法,相当于 par * (n..-1)


更多的组合子
--------------------------------------------------------
par.eof  如果解析完输入还没结束,返回 nil (不匹配)

par.map{|node| ... }  这是一个映射 parser,将 par 的解析结果赋给 node,传给 block,然后 block 返回的结果作为 parse 结果。SDT 主要靠这个求值。根据 parser 的类型不同,node 可能是一个数组或者字符串。

par.on{|node| ... } 将 par 的解析结果传给 block,不改变 parse 结果。
技巧:可以在 on 之中给 node 添加属性或者方法,例如:
par.on{|node|
  def node.sum
    inject(&:+)
  end
end

par.fail('oh no!') 设定 fail 信息,par 失败时弹出一个包含此信息的异常(默认是 "syntax error")。可以用来定制语法错误。

par.skip  成功就跳过。相当于 par.map{Rsec::SKIP},不过会针对 pattern parser 作优化。

par & ser  正向环视(lookahead)。就是说 par 后面是 ser,但是 parse 结果不包括 ser。

par ^ ser  negative lookahead。par 后面不是 ser 才通过。


join 系列
--------------------------------------------------------
par.join ser  1 到任意个 par 之间以 ser 为间隔串在一起,方便进行二元运算符的解析。譬如 '1'.r.join ',' 可以 parse '1,1,1,1'。结果是一个数组 ['1',',','1',',','1',',','1']。
如果 ser 是一个 skip parser(也就是成功时输出 Rsec::SKIP 的 parser),join 的输出结果就会忽略掉 ser 的内容。假如上面的字符串如果被 '1'.r.join ','.r.skip 解析,结果就是数组 ['1','1','1','1']

par.join ser, /\s*/  跳过可选空格 /\s*/


Pattern parser (原子 parser) 的专有方法
--------------------------------------------------------
pattern.until  匹配以 pattern 结尾的字符串。


Sequence parser (顺序 parser) 的专有方法
--------------------------------------------------------
par[n]  依然匹配原序列,但是产生的不是数组,而是其中一个子元素的 parse 结果。如:
par = %w[( b )].r
par.parse '(b)' #=> ['(', 'b', ')']
par[1].parse '(b)' #=> 'b'


include Rsec::Helpers 之后的额外方法
--------------------------------------------------------
lazy{ par }  有时一个 parser 还没定义,但又想引用它 —— 就用 lazy 吧。
注: 如果 par 是局部变量,应该在 lazy 之前加上 "par = nil" 声明一下。

dynamic{ par }  类似于 lazy,但每次调用的时候都会 call block,所以可以动态更改 parser 成分。

bol()  返回一个匹配行首的 parser。

value(1234)  不做任何事情,只返回值 1234。常用的手段是和 >> 或者 << 组合:
/\w+/.r >> value(1234) # 匹配多个单词字符,如果匹配,输出 1234


skip_n(n)  跳过 n 个字符(可以为负)。如果越界,返回 nil,否则返回 Rsec::SKIP


和 PEG 的对应表
--------------------------------------------------------
PEG meaning rsec
ε the empty string parser.eof
a terminal (a ∈ Σ) 'a'.r
A non-terminal (A ∈ N) parser
e1 e2 sequence [e1, e2].r
e1 / e2 prioritized choice e1 ▏e2
e? optional e._?
e* zero-or-more e ** 0
e+ one-or-more e ** 1
&e, !e syntactic predicates &e, ^e
night_stalker 2010-02-14
关于速度的讨论

1. vs treetop

和 treetop 作了一下性能比较:
rsec result:    32918.38
  0.266000   0.000000   0.266000 (  0.273437)

treetop result: 32918.38
  2.171000   0.000000   2.171000 (  2.175781)

treetop without calculation
  1.782000   0.000000   1.782000 (  1.774414)


比 treetop 快呢 ……

代码见 http://github.com/luikore/rsec/tree/master/bench/

假设有个递归定义的规则 s :
s = ss / a
ss = a '+' s
a = 'xyz'


那么在解析 'xyz' 的时候,规则应用的顺序是:
s (左)
ss (左)
a -> 'xyz' 匹配
回到 ss (中) -> '+' 不匹配
回到 s (右)
a -> 'xyz' 匹配


规则 a 在同一位置解析了两遍,导致了解析器的重复劳动。一种解决方法是对规则 a 进行缓存,如 Treetop 的 packrat parsing。它的缺点是:内存消耗变大,而且当定义是非递归的时候,缓存反而降低了解析速度。

另外还可以得到结论:
应该只针对 Or parser(例如 a | b | c 产生的结果) 的选择支中的公共元素进行缓存,其它 parser 是确定的,缓存也没有用。

对下面 3 个版本的 arithmetic 作了一下性能测试

递归定义的版本:
def arithmetic1
  calc = proc do |left, op, right|
    left.send op, right
  end

  num    = /[+-]?[1-9]\d*(\.\d+)?/.r.map(&:to_f)
  bra    = /\(\s*/.r.skip
  ket    = /\s*\)/.r.skip

  expr, term = nil

  # paren = ( expr )
  paren  = bra >> lazy{expr} << ket
  
  # factor = num | paren
  factor = num | paren

  # term = factor */ term | factor
  term  = [factor, /[\*\/]/, lazy{term}].r(skip: /\s*/).map(&calc) | factor
  
  # expr = term +- expr | term
  expr  = [term, /[+-]/, lazy{expr}].r(skip: /\s*/).map(&calc) | term
  expr.eof
end


使用了缓存的递归版本:
def arithmetic2
  calc = proc do |left, op, right|
    left.send op, right
  end

  num    = /[+-]?[1-9]\d*(\.\d+)?/.r.map(&:to_f)
  bra    = /\(\s*/.r.skip
  ket    = /\s*\)/.r.skip
  
  expr, term = nil

  # paren = ( expr )
  paren  = bra >> lazy{expr} << ket
  
  # factor = num | paren
  factor = num | paren

  # term = factor */ term | factor
  _term  = [factor,/[\*\/]/,lazy{term}].r(skip: /\s*/).map(&calc) | factor
  term   = _term.cached
  
  # expr = term +- expr | term
  _expr  = [term,/[+-]/,lazy{expr}].r(skip: /\s*/).map(&calc) | term
  expr   = _expr.cached
  expr.eof
end


利用 join combinator 消除了大部分递归,缓存完全派不上用场的版本:
def arithmetic3
  calculate = proc do |(p, *ps)|
    ps.each_slice(2).inject(p) do |left, (op, right)|
      left.send op, right
    end
  end

  num    = /[+-]?[1-9]\d*(\.\d+)?/.r.map(&:to_f)
  bra    = /\(\s*/.r.skip
  ket    = /\s*\)/.r.skip
  
  expr   = nil # declare for lazy
  paren  = bra >> lazy{expr} << ket
  factor = num | paren
  term   = factor.join(/[\*\/]/, /\s*/).map &calculate
  expr   = term.join(/[\+\-]/, /\s*/).map &calculate
  expr.eof
end


测试的结果: arithmetic3 完胜,arithmetic2 比起 arithmetic1 有巨大的进步,但还是比 arithmetic3 慢一些。

arithmetic3 的性能和下面的 Shunting-Yard 算法(转换成逆波兰表达式)的结果相近,只比 operator table 略差一点:
# rpn: reverse polish notation
def arithmetic_rpn
  float  = /[\+\-]?\d+(?:\.\d+)?/.r.map(&:to_f)
  bra    = /\(\s*/.r.skip
  ket    = /\s*\)/.r.skip

  expr   = nil # declare for lazy
  term = float | bra >> lazy{expr} << ket

  # op[s] is a parser, it parses s and returns s.to_proc
  op = proc {|s|
    s.to_s.r >> value(s.to_proc)
  }
  # build operator table
  expr = term.join_infix_operators(
    calculate: true,
    left: {
      op[:+] => 5,
      op[:-] => 5,
      op[:**] => 15,
      op[:*] => 10,
      op[:/] => 10,
      op[:%] => 10
    }
  )
  expr.eof
end


-------------------------------------------------------------------------

2. vs haskell

下面和 ssword 童鞋的 FParsec 做一下比较:

下载 FParsec
git clone git://github.com/Fleurer/FParser.git


计算器和 FFI 代码 Arithmetic.hs
module Arithmetic where
 
import Control.Monad
import Data.Maybe
 
import Internal
import Combinator
import Lexer
import Foreign.C.String

expr = term `chain` addop
term = factor `chain` mulop
factor = (parens expr) <|> number
mulop =  do { sym "*"; return (*); }
     <|> do { sym "/"; return (div);}
addop =  do { sym "+"; return (+); }
     <|> do { sym "-"; return (-);}

calculate :: CString -> IO Int
calculate cs = do
    s <- peekCString cs
    (Right x, _) <- return (parse expr s)
    return (fromIntegral x)

donothing :: CString -> IO Int
donothing cs = return 0

foreign export stdcall calculate :: CString -> IO Int
foreign export stdcall donothing :: CString -> IO Int


在 windows 下编译成 dll 需要创建一个 dllmain.c,内容如下:(linux 就不需要了)
#include <windows.h>
#include <Rts.h>
extern void __stginit_Arithmetic(void);
static char* args[] = { "ghcDll", NULL };
BOOL APIENTRY DllMain(HANDLE hModule, DWORD reason, void* reserved)
{
  if (reason == DLL_PROCESS_ATTACH) {
    startupHaskell(1, args, __stginit_Arithmetic);
    return TRUE;
  }
  return TRUE;
}


编译:
ghc -c Type.hs Internal.hs Combinator.hs -fglasgow-exts -O2
ghc -c Lexer.hs Arithmetic.hs -fglasgow-exts -O2
ghc -c dllmain.c -O2
ghc -shared *.o -o Arithmetic.so -O2


测试代码
require 'dl/import'
require 'dl/types'
module Arithmetic
  extend DL::Importer
  dlload 'Arithmetic.so'
  extern "long calculate(char *)", :stdcall
  extern "long donothing(char *)", :stdcall
end

require "benchmark"
s = '(3+24/5)/10-3*4+((82321+12-3)-3*4+(82321+12-3))/5'

print "calc result: ", Arithmetic.calculate(s), "\n"

puts Benchmark.measure {
  1000.times {
    Arithmetic.calculate s
  }
}

# 对照组,因为 dl 做类型转换时有些消耗,所以上述时间减去对照组要准确些
puts Benchmark.measure {
  1000.times {
    Arithmetic.donothing s
  }
}


测试结果
calc result: 32917
  0.156000   0.000000   0.156000 (  0.155273)
  0.000000   0.016000   0.016000 (  0.016601)


最后还是 haskell 快呢。不过 0.27: 0.14,只慢一倍相对于总是很慢的 ruby 来说已经不错了 ……

ps - 为什么要绕那么一大圈在 ruby 里作比较: 因为这种 repeat 类型的测试对 haskell 基本行不通,相同的调用会被自动 cache,对结果没影响的调用会被忽略 ……
night_stalker 2010-02-17
添加了个 BNF 的例子:
http://github.com/luikore/rsec/blob/master/examples/bnf.rb

下面可以写个 EBNF --> rsec 的东西?

Edit

添加了 C-Minus 的例子(有点粗):
http://github.com/luikore/rsec/blob/master/examples/c_minus.rb
RednaxelaFX 2010-02-17
话说我想起这么一个课件,PEGs, Packrats and Parser Combinators
NS要不要考虑一下到底rsec是用来支持CFG还是用来支持PEG?如果支持的是PEG的话,就不太可能轻松的做“EBNF -> rsec”的转换了,因为EBNF是CFG的表示方式,而CFG与PEG表达的语言范围是不完全一样的
night_stalker 2010-02-17
它主要是 PEG,因为 | 组合子是确定的、会短路的。(手动用 map 处理成非确定的也可以 ……)

处理 EBNF 的一个小小的子集应该是可以的 ……
night_stalker 2010-02-23
更新

  • API 痛改了一番。
  • .cached 方法可以号称 packrat parsing 了。
  • .join_infix_operators 用了 Shunting-Yard 算法(的一小部分) 处理二元操作符,也没快多少嘛。(以上二条详见上面速度比较的回帖)
  • 添加了一个 C-Minus 的例子
ssword 2010-03-04
还没有实现try...
这样parse ((sym "aabc") <|> (sym "abc")) $ "abc"
会跟着第一个字符'a'进入第一个分支然后报错~囧

Global site tag (gtag.js) - Google Analytics