Try   HackMD

Zuby 下書き

ずびしいず

チケット下書き


background

  • hogefuga

  • reachabilty判定の導入によって改善されるもの

    • ("b".."aa") のような単純な辞書順比較によって正しく評価されていなかったrangeが正しく評価されるようになる
      • #5607 ("b".."aa").to_a => [] になる問題もこれで解決できる
    • Range#include?

algorithm

string contain with alunm(s)

  • "a" is reachable to "a", "z", "aa", "abc"

  • "b" is reachable to "z", "aa", "abc", unreachable to "a"

  • "0" is reachable to "9", 10", "10298"

  • "0" is unreachable to ":", "a", "0a"

  • "aa00" is reachalbe to "ab00", "abcasdfv84"

  • "." is reachable to "10982"

  • 書くのめんどくせえ

string with no alunm

  • '\0' <= a[-1] <= 'z'
    • "//" is reachable to "/10"
    • "//" is unreachable to "/010"
    • "//" is unreachable to "/?"
    • "@@" is reachable to "@AA"
    • "@@" is unreachable to "@_"
    • "``" is reachable to "`aa"
    • "``" is unreachable to "`~"
  • a =~ /[^\x2f\x40\x60\x7f]\x7f*[\x7b-\x7f]$/
    • "{\x7f{" is reachable to "{\x7f~"
    • "{\x7f{" is reachable to "{\x00!"
    • "{\x7f{" is reachable to "|\x0010"
  • a =~ /[\x2f\x40\x60]\x7f*[\x7b-\x7f]$/
    • "@@\x7f{" is reachable to "@A\x00\x00"
    • "@@\x7f{" is unreachable to "@A\x00\x01"
    • "@@\x7f{" is reachable to "@AA\x00\x00"
  • a =~ /^\x7f*[\x7b-\x7f]$/
    • "\x7f{" is reachable to "\x01\x00\x00"
    • "\x7f{" is reachable to "\x01\x0010"

発表スライド

https://docs.google.com/presentation/d/156xZN9OrN6NPyof3E_1K-oOeCf0J-Hh8vtcuvoxjcto/edit?usp=sharing

CRuby の Rangeの改善

  • 上に関連して,Rubyについて,rangeの始端もしくは終端にUTF-8文字が含まれると非自明な挙動をする

    • ("a".."あ").to_a #=> ["a", ... , "zzy", "zzz"]
      • str_upto_each はsuccで到達できないものについても処理を書いているので把握済み?
      • https://github.com/ruby/ruby/blob/master/string.c#L4457
      • 本当は始端のsucc* が終端にならないものはそもそもrangeとして受け付けないでほしい(個人的に)
        • これは例えばFloatのrangeを受け入れている以上現実的ではなさそうという気持ちになった
    • [*"z".."aa"] #=> []
      • [*"a"..."z"] + [*"z".."aa"] == [*"a".."aa"] になるのが自然だが、[*"z".."aa"] が空になるためこれは一致しない
      • https://bugs.ruby-lang.org/issues/5607
      • 関連: https://bugs.ruby-lang.org/issues/2323
      • これは string.c#L4444 の判定が誤っていて、例えば(c..a)のようなものにイテレーションを走らせないようにした結果,このように到達できるものもはじいてしまっている
      • 「『a が辞書順で b より大きい』ならば『a は b に unreachable である』」は偽
      • issue でも言われている通り、ASCII に絞った対応なら現実的&需要があるのでは?
    • ("aa"..).include?("b") #=> true
      • 上とは逆に unreachable なものを reachable 判定してしまう
      • これも辞書順比較だけで済ませてしまっているため
      • 単にbegin/endlessの実装ミスっぽい
  • rubyのRange#include のstringが特殊なケースを除いてString#uptoを呼び出しているため遅い

    • Range#includeは始端終端がstringの場合 rb_str_include_range_p@string.c を呼び出す
    • https://github.com/ruby/ruby/blob/master/string.c#L4518
    • 始端終端がasciiかつ1文字の場合のみ単純な比較を行い,それ以外はrb_str_upto_each を呼び出している
  • (0..1).include?(0.5) #=> true

  • String#succ のアルゴリズム

    • str_succ
      • 末尾から見ていき alnum があればそこを次の文字にする
        • 次の文字にするのは enc_succ_alnum_char を使う
        • 次の文字にできない場合は「繰り上がり」のように振る舞う
          • 一個前の alnum を次の文字にする
        • 先頭まで来てしまったら文字を前方向に伸ばす
      • 一個も alnum がなかったら、単に文字として似たことをやる
        • enc_succ_char を使う
    • enc_succ_alnum_char
      • alnum 判定は、Unicode Character Properties に基づくと思われる
      • 非 alnum を1回までなら超えられる(この回数は max_gaps によって定められる)
      • だめなら巻き戻す(巻き戻しは非 alnum を超えられない)
    • enc_succ_char
      • エンコーディングによって場合分けがされている
        • バイト長に関する処理?
    • enc_pred_char
      • だいたい enc_succ_char の逆
    • enum neighbor_char
      • NEIGHBOR_NOT_CHAR
        • 「次(前)が文字として無効だよ」みたいな意味
      • NEIGHBOR_FOUND
        • 「次(前)が文字として有効だよ」みたいな意味
      • NEIGHBOR_WRAPPED
        • enc_succ_alnum_char だと「次が文字として無効だったから巻き戻せるところまで巻き戻したよ」みたいな意味
        • enc_succ_char だとバイト長が変わったみたいな意味?
  • メモ

    • String#uptoがString#succに依存しない例
    ​​​​(".".."13").to_a
    ​​​​=> [".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12", "13"]
    ​​​​(".".."=").to_a
    ​​​​=> [".", "/", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", ":", ";", "<", "="]
    
話の流れを切ってしまい申し訳ありません.

CRubyのRange#include? について,現状の実装だとRangeの始終端がStringの場合,[rb_str_include_range_p](https://github.com/ruby/ruby/blob/master/string.c#L4518)が呼び出されるんですが,これは始端終端がasciiかつ1文字の場合以外については`rb_str_upto_each`を呼び出しているため重く例えば`("a".."zzzzz").include?("zzzzz")` は(手元の環境で)2秒程度かかっています.
上のように(他の例として"1".."100","あ".."を"など)始端がsuccを辿って終端に到達可能なrangeについては文字列の長さ及び文字列の各文字の比較によって`rb_str_upto_each`で1個ずつ辿ること無く高速にincludeかを判定できるため,そのような実装をRange#include?に追加することを考えているのですがこのアイデアについてご意見いただければと思います.
現状気になっているいる点は
・始端と終端が`rb_str_upto_each`を使わず比較可能かは「文字種」が始端終端で一致しているかを確認すればよいと考えていて,具体的には始端終端が { 数字を表すStringの場合比較可能(例: "1".."10")/ そうでない場合ascii文字列ならば比較可能(例: "a".."aaaaa") / そうでない場合比較が難しいもしくは不可能なのでuptoを呼び出す必要がある} で可能と考えているのですが,
・("a".."あ")のような終端が始端からunreachableなrangeについては今までと同様に`rb_str_upto_each` を呼び出す他ないように思えるんですが,そもそもこのようなrangeは許容・想定されているのでしょうか ( str_upto の[ここ](https://github.com/ruby/ruby/blob/master/string.c#L4457 )を見る限り想定はされているのかなという所感ではあります(これにより例えば("a".."あ").each は "zzz"で止まります), blameや過去のチケットを軽く眺めてみたのですが関連する議論は見つけられませんでした)
$ time ruby -e '("a".."zzzzz").include?("zzzzz")'
ruby -e '("a".."zzzzz").include?("zzzzz")'  2.19s user 0.10s system 89% cpu 2.543 total
  • 到達可能性判定アルゴリズム?
ASCII = /[[:ascii:]]/ ALNUM = /[[:alnum:]]/ ALPHA = /[[:alpha:]]/ UPPER = /[[:upper:]]/ LOWER = /[[:lower:]]/ DIGIT = /[[:digit:]]/ def ascii_str_reachable?(a, b) raise unless a.ascii_only? raise unless b.ascii_only? return true if a == "" && b == "" return false if a == "" || b == "" # succ によって文字数が減ることはないので弾ける return false if a.size > b.size # assert a.size <= b.size # 参考: string.c str_succ L4241-L4264 # 後ろから見ていき、任意回の succ によって変更されうる始点を見つける # 非 alnum を超えるには {digit / alpha} が一致している必要がある ...(1) last_alnum_index = nil neighbor_is_alnum = true i = a.size - 1 while i >= 0 if !neighbor_is_alnum && last_alnum_index case a[i] when DIGIT break if a[last_alnum_index] =~ ALPHA when ALPHA break if a[last_alnum_index] =~ DIGIT end end if a[i] =~ ALNUM last_alnum_index = i neighbor_is_alnum = true else neighbor_is_alnum = false end i -= 1 end if last_alnum_index # last_alnum_index より以前は変化しないので一致している必要がある for i in 0...last_alnum_index return false if a[i] != b[i] end # 後ろから見て {digit / upper / lower / else} が一致している必要がある # else なら文字自体が一致している必要がある # b の超過分については a[last_alnum_index] のものと一致している必要がある ...(2) # (ルール(1)と(2)によって、任意回の succ をしても last_alnum_index の位置が変わらないことが言える) i = a.size - 1 j = b.size - 1 prepend = false while j >= last_alnum_index case a[i] when DIGIT return false if b[j] !~ DIGIT return false if prepend && b[j] == "0" when UPPER return false if b[j] !~ UPPER when LOWER return false if b[j] !~ LOWER else return false if a[i] != b[j] end if i > last_alnum_index i -= 1 else prepend = true end j -= 1 end # あとは普通の自然数の大小比較と同じ case when a.size < b.size return true when a.size == b.size i = last_alnum_index while i < a.size if a[i] =~ ALNUM case when a[i] > b[i] return false when a[i] < b[i] return true end end i += 1 end return true end else # TODO: alnum を含まない場合 # 参考: string.c str_succ L4265-L4297 # UTF-8の場合は \x7f の次が \x00 になって繰り上がる # (ASCII-8bitの場合 \x7f.succ == \x80) # 十分な回数やると alnum ありにたどり着く # a[-1] を succ していき、 # - '0', 'A', 'a' に突入するパターン # - 突入するまでに b と一致するパターン # - 突入し、繰り上がりが起こるなどして b と一致するパターン # - '\x7f' を超えるパターン # - 超えるまでに b と一致するパターン # - 繰り上がって a[-1] 自身が '0' に突入するパターン # - 突入するまでに b と一致するパターン # - 突入し、繰り上がりが起こるなどして b と一致するパターン # - 繰り上げられた文字が '0', 'A', 'a' に突入するパターン # - 繰り上がって先頭に '\x01' がつき a[-1] が '0' に突入するパターン # - 突入するまでに b と一致するパターン # - 突入し、繰り上がりが起こるなどして b と一致するパターン # [wrap_start, wrap_end) ... a[-1] から到達しうる ['0', '9'), ['A', 'Z'), ['a', 'z'), ['\x80', ) などの区間 # [carry_pos, carry_pos + carry_len) ... a から succ していって alnum を含み始めたとき b のどの部分に含みうるか wrap_start = nil wrap_end = nil carry_pos = nil carry_len = nil ch = a[-1].ord ch += 1 while ch.chr =~ ASCII && ch.chr !~ ALNUM wrap_start = ch.chr if wrap_start =~ ASCII return false if a[...(a.size - 1)] != b[...(a.size - 1)] return true if a.size == b.size && a[-1] <= b[-1] && b[-1] <= wrap_start carry_pos = a.size - 1 carry_len = b.size - carry_pos else return true if a.size == b.size && a[...(a.size - 1)] != b[...(a.size - 1)] && a[-1] <= b[-1] && b[-1] < wrap_start carry_pos = a.size - 2 carry_pos -= 1 while carry_pos >= 0 && (a[carry_pos].ord + 1).chr !~ ASCII if carry_pos >= 0 return false if a[...carry_pos] != b[...carry_pos] if (a[carry_pos].ord + 1).chr !~ ALNUM return false if (a[carry_pos].ord + 1).chr != b[carry_pos] for j in (carry_pos + 1)...(a.size - 1) return false if b[j] != "\0" end ch = "\0".ord ch += 1 while ch.chr !~ ALNUM wrap_start = ch.chr return true if a.size == b.size && b[-1] < wrap_start carry_pos = a.size - 1 carry_len = b.size - carry_pos else wrap_start = (a[carry_pos].ord + 1).chr carry_len = 1 + (b.size - a.size) for j in (carry_pos + carry_len)...b.size return false if b[j] != "\0" end end else return false if b[0] != "\x01" for j in 1...a.size return false if b[j] != "\0" end ch = "\0".ord ch += 1 while ch.chr !~ ALNUM wrap_start = ch.chr return true if a.size + 1 == b.size && b[-1] < wrap_start carry_pos = a.size carry_len = b.size - a.size end end ch = wrap_start.ord ch += 1 while ch.chr =~ ALNUM wrap_end = ch.chr if wrap_start =~ DIGIT return false if carry_len > 1 && b[carry_pos] == wrap_start end for j in carry_pos...(carry_pos + carry_len) return false if b[j] < wrap_start || wrap_end <= b[j] end return true end end
  • reachabilty判定の導入によって改善されるもの

    • ("b".."aa") のような単純な辞書順比較によって正しく評価されていなかったrangeが正しく評価されるようになる
      • #5607 ("b".."aa").to_a => [] になる問題もこれで解決できる
    • Range#include?
      • String#upto で全列挙していた実装の代わりに本methodを使うことで判定が高速に行えるようになる
      • ("b".."aa").include?("z"), ("b"..).include?("aa") などの辞書順比較を用いていたことによるバグが直る
  • TODO

    • C移植
      • /string.c, /internal/string.h
        • ascii_str_reachable_p
        • rb_str_upto_each
        • rb_str_include_range_p
      • /range.c
        • range_include_internal
    • テスト
    • ベンチマーク
    • PR
    • 本家の方で相談
  • memo

mrubyへのendless/beginless rangeの追加

  • https://github.com/mruby/mruby/issues/5085
  • 進捗状況
    • parserの変更
    • Range#eachのInteger(FIXNUM)の場合の実装
    • Range#eachのStringの場合の実装
      • ("a"..).each{|c| p c}
      • 本家ではC側で処理していて通常の String#upto に対応するrb_str_uptoおよびこれのメインロジックに相当するrb_str_upto_eachとは別にendless用にrb_str_upto_endless_eachを用意している.
      • mrubyではRuby側で処理していて String#upto を使っている
      • ここにendless range用の処理を組み込む場合どうするか
        • String#upto の第一引数endに nil を渡すことを許容する
          • 本家と挙動が異なってしまう / 元々のuptoインターフェースを破壊してしまう
        • Ruby側でStringクラスにendless range用のメソッドを新たに用意する
          • どうやって隠蔽?
          • String#__sub_replace という前例が存在
            • 隠蔽はできてない
        • each内でStringにパッチをあてるなりしてupto_endlessを生やす / インラインでベタ書きしちゃう
          • String#upto は拡張として切り分けられているのにまあまあな量の処理を拡張でないところに入れてしまうのは
        • C側で生やす
          • Ruby側から使うにはメソッドなりなんなりで登録する必要があり結局隠蔽ができない.
        • mrubyの他のコードで__ prefixつきのメソッドをprivateとして実装しているものがあるためこれを踏襲する(使えてはしまうが)
          • __sub_replace@string.rb など
          • _の数は1個だったり2個だったりする(え?)
      • とりあえずアンスコでの実装を選択,完了
    • [1,2,3][0..]の実装(mrb_range_beg_len()@range.c)
    • EXCLUSIVE[0]への対応(mrb_range_beg_len()@range.c)
    • Range#to_s, Range#inspect
      • nil.to_s は空文字になるので結果的に Range#to_s 改変の必要はない
    • Range#{include?(member), #min, #===, #cover?}
    • Range#{max, last}
    • Range#sizeについて
      • 本家RubyではFloat::INFINITYを返しているがmrubyはFloat無しの環境もありえる.この場合に何を返すか.
      • https://github.com/ruby/ruby/commit/342619300ce26f9a134249002270c5d38d5993b3
      • とりあえず nil を返すようにして PR 時に相談
      • そもそも MRB_WITHOUT_FLOAT 下での動作確認が十分になされていなさそう
      • 始端終端がNumericじゃない場合はnilらしい
        • ``('a'..'z').size # => nil`
    • eachで初期値がFIXNUM::MAXのときどうするか
    • Range#to_a
    • テストの追加
      • /test/t/range.rb, /mrbgems/mruby-range-ext/test/range.rb
        • Range
          • Range#Hashのテストが無かったり,Float skipがあったりなかったりする
      • /test/t/array.rb
        • Array#[], Array#[]=
      • /test/t/string.rb
        • String#[] with Range
        • Ruby では本来 ここ にテストが書かれるべきだが、忘れられている?
        • mruby にそもそも String#[]= with Range がなかったので生やした
      • /mrbgems/mruby-string-ext/test/range.rb
        • Range の要素が String のときのテスト
      • /mrbgems/mruby-enum-ext/test/enum.rb
        • Enumerable#take など各種メソッド
        • 変更が増えるので見送り
    • Rubyの変更コードとの比較(この部分はいる・いらない選択, 最終確認)
    • あと、関係ないけどもとのコードにあった冗長な部分(lenの二重代入とか、TODOでメモったやつ)

副産物として見つけたバグなど

  • [BETTER]Range#eachでlenが2重代入されている
  • [BETTER]Range#Hashのテストが無い
    • 書いた
  • [BETTER]RangeのテストでFloat skipがあったりなかったりする
    • 多分MRB_WITHOUT_FLOATすると落ちる
    • 書いた
  • [BETTER] RubyのString#[]にendless/beginless rangeのテストが無い (endless mergeされた後)
  • [BETTER]Range#minのテストミス
  • (1)[BUG] mrubyで ('b'..'d').include?('ba') # => true
  • [BUG?] 上に関連して,Rubyについて,rangeの始端もしくは終端にUTF-8文字が含まれると非自明な挙動をする
  • [BETTER] rubyのRange#include のstringが特殊なケースを除いてString#uptoを呼び出しているため遅い
    • Range#includeは始端終端がstringの場合 rb_str_include_range_p@string.c を呼び出す
    • https://github.com/ruby/ruby/blob/master/string.c#L4518
    • 始端終端がasciiかつ1文字の場合のみ単純な比較を行い,それ以外はrb_str_upto_each を呼び出している
  • [BUG] mrubyで(9223372036854775807..9223372036854775808).each が壊れる
  • [BUG] WITHOUT_FLOAT でコンパイルが通らない
  • [FEATURE] beginless range

PR下書き

(TITLE) Introduce endless range. (a part of https://github.com/mruby/mruby/issues/5085)

We introduced endless range feature, which is proposed in https://github.com/mruby/mruby/issues/5085 and already supported by CRuby.

Current Behavior

> Range.new(1,nil)
bad value for range (ArgumentError)

Behavior after this commit

# mruby 
> Range.new(1,nil)
 => 1..
> (1...)
 => 1...
> (1..).each {|n| p n}
1
2
3
(snip)
> ('a'...).each {|c| p c; break if c>='f'}
"a"
"b"
"c"
"d"
"e"
"f"
 => nil
 > "Ruby"[1..]
 => "uby"
 > a = [0, 1, 2, 3, 4]; a[1..] = 5
 => 5
 > a
 => [0, 5]

# CRuby(2.6.6)
2.6.6 :001 > Range.new(1,nil)
 => 1.. 
2.6.6 :002 > (1...)
 => 1... 
2.6.6 :001 > (1..).each {|n| p n}
1
2
3
(snip)
2.6.6 :011 > ('a'...).each {|c| p c; break if c>='f'}
"a"
"b"
"c"
"d"
"e"
"f"
 => nil
2.6.6 :001 > "Ruby"[1..]
 => "uby" 
2.6.6 :002 > a = [0, 1, 2, 3, 4]; a[1..] = 5
2.6.6 :003 > a
 => [0, 5]

For other changes, please see the diff.

Things to notice

  • Range#size returns Float::INFINITY when it is endless. (This behavior is based on CRuby.) Under MRB_WITHOUT_FLOAT environment, we implemented to return nil for now.
# CRuby(2.6.6)
2.6.6 :015 > (1..).size
 => Infinity
 
# mruby
> (1..).size
=> Infinity
# mruby (built with MRB_WITHOUT_FLOAT flag)
> (1..).size
=> nil
  • Range#last
    • According to ISO, Range#last is an alias of Range#end (15.2.14.4.10), which is actually not the case in current Ruby anymore (e.g. Range#last can take 0 or 1 argument, but Range#end does not take arguments.). Since the core of mruby basically needs to follow ISO, those features in CRuby have been implemeted as extension(mruby-range-ext) in current mruby.
    • In CRuby, Range#last raises a RangeError when it is endless. (while Range#end returns nil). So we implemented this feature to extension(mruby-range-ext)'s Range#last following that implementation.
# CRuby(2.6.6)
2.6.6 :002 > (1..).last
Traceback (most recent call last):
        5: from /home/wataru/ruby/bin/irb:23:in `<main>'
        4: from /home/wataru/ruby/bin/irb:23:in `load'
        3: from /home/wataru/.rvm/gems/ruby-2.6.6/gems/irb-1.2.4/exe/irb:11:in `<top (required)>'
        2: from (irb):2
        1: from (irb):2:in `last'
RangeError (cannot get the last element of endless range)
# mruby (core)
> (1..).last
 => nil
# mruby (with mruby-range-ext)
> (1..).last
cannot get the last element of endless range (RangeError)
  • __upto_endless
    • To implement Range#each, we need to prepare upto method of String corresponding to endless range separately from the existing Stinrg#upto (CRuby also takes the same steps). According to the other implementation (like [TODO]) in mruby, we implement Stinrg#__upto_endless for private in use.

PR下書きオワリ -

> (1..).min
 => 1
> (1..).max
cannot get the maximum of endless range (RangeError)

> (1..).to_s
 => "1.."
> (1..).to_a
cannot get the last element of endless range (RangeError)

> (1..).first
 => 1

2.6.6 :003 > (1..).min
 => 1
2.6.6 :004 > (1..).max
Traceback (most recent call last):
        5: from /home/wataru/ruby/bin/irb:23:in `<main>'
        4: from /home/wataru/ruby/bin/irb:23:in `load'
        3: from /home/wataru/.rvm/gems/ruby-2.6.6/gems/irb-1.2.4/exe/irb:11:in `<top (required)>'
        2: from (irb):4
        1: from (irb):4:in `max'
RangeError (cannot get the maximum of endless range)
2.6.6 :013 > (1..).to_s
=> "1.."
2.6.6 :014 > (1..).to_a
Traceback (most recent call last):
        5: from /home/wataru/ruby/bin/irb:23:in `<main>'
        4: from /home/wataru/ruby/bin/irb:23:in `load'
        3: from /home/wataru/.rvm/gems/ruby-2.6.6/gems/irb-1.2.4/exe/irb:11:in `<top (required)>'
        2: from (irb):14
        1: from (irb):14:in `to_a'
RangeError (cannot convert endless range to an array)

2.6.6 :001 > (1..).first
 => 1 

memo

  • MRB_WITHOUT_FLOAT を定義してコンパイルすることで Float を排除できる
  • Range#stepはmrubyには未実装
  • nil判定
    • C: mrb_nil_p(a)
    • Ruby: a.nil?
  • コーディングスタイル
    • C
      • else の前は改行
      • ​​​​​​​​​​if (a) {
        ​​​​​​​​​​  b
        ​​​​​​​​​​}
        ​​​​​​​​​​else {
        ​​​​​​​​​​  c
        ​​​​​​​​​​}
        
      • 1行でない if 文を使うときは波括弧をつける
      • 必要に合わせて = の位置を揃える場合と揃えない場合があるぞい
      • コメントで // は使わない、/* */ のみ使う
    • Ruby
      • 引数があるときのメソッド呼び出しは括弧をつける場合とつけない場合があるぞい

案出し

参考リンクなど