Closed13

parser gemとパーセントリテラルのバグ

ピン留めされたアイテム
pockepocke
  • ^@とかがデリミタとしてparser gemだと通らない(Rubyだと通る
  • 1とかがデリミタとしてparser gemだと通る(Rubyだと通らない

軽く調査したらwhitequark/parserにレポートする

pockepocke
# gen.rb
puts "%Q\0foo\0"
$ ruby gen.rb > test.rb

$ ruby-parse test.rb 
warning: parser/current is loading parser/ruby31, which recognizes
warning: 3.1.0-dev-compliant syntax, but you are running 3.1.0.
warning: please see https://github.com/whitequark/parser#compatibility-with-ruby-mri.
test.rb:1:1: fatal: unterminated string meets end of file
test.rb:1: %Q

$ ruby -c test.rb
Syntax OK


$ ruby-parse -e "%w1foo1" 
(array
  (str "foo"))

$ ruby -ce "%w1foo1" 
-e:1: unknown type of %string
%w1foo1
^~~
pockepocke

"\0"をデリミタにすると死ぬ問題の原因はこれ。

パーセントリテラルのデリミタにc_anyというtokenを使っているのだけど、c_anyはanyから^Dとかを抜いた文字であり、それが良くないっぽい。
https://github.com/whitequark/parser/blob/60fe440ec20872d289d7044bc3f5eb6b074bf726/lib/parser/lexer.rl#L514-L516

any - zlenを使うのが正しいのでは?多分zlenはEOFぽく見える。

c_any(とany)は結構いたるところで使われていて、眺めるのがだるい。

%w1foo1が通ってしまうのは別問題として見つけた

pockepocke

openなIssueとPRをざっと見た感じ、該当のIssueはなかった

pockepocke

現実世界の問題としては、hamlit + querlyで問題が起きていた。querlyでHAMLのコードを解析するためにpreprocessorをhamlit compile -に指定していたのだけど、hamlitがヌル文字をデリミタとしてパーセントリテラル文字列を出力していて、それがparser gemによって解釈できていなかった

pockepocke

all-rubyで試した結果。parser gemは1.8からのサポートなので、特にバージョン分岐は考える必要がなさそう。

^Dは実際には制御文字

v0.99から制御文字をデリミタにしたパーセントリテラルは通る

$ docker run -it --rm rubylang/all-ruby ./all-ruby -e 'p %q^Dfoo^D'
ruby-0.49             -e:1: undefined method `(null)' for "nil"(Nil)
                  exit 1
ruby-0.50             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.51             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.54             -e:1:in method `%': undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.55             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
...
ruby-0.76             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.95             -e:1: undefined method `p' for main(Object)
                  exit 1
ruby-0.99.4-961224    "foo"
...
ruby-3.0.2            "foo"

%1foo1は、Ruby 0.99からRuby 1.6.5までは通っていた。それ以降は通らない。

$ docker run -it --rm rubylang/all-ruby ./all-ruby -e 'p %q1foo1'
ruby-0.49             -e:1: undefined method `(null)' for "nil"(Nil)
                  exit 1
ruby-0.50             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.51             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.54             -e:1:in method `%': undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.55             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
...
ruby-0.76             -e:1: undefined method `%' for "nil"(Nil)
                  exit 1
ruby-0.95             -e:1: undefined method `p' for main(Object)
                  exit 1
ruby-0.99.4-961224    "foo"
...
ruby-1.6.5            "foo"
ruby-1.6.6            -e:1: unknown type of %string
                      p %q1foo1
                           ^
                  exit 1
...
ruby-2.4.10           -e:1: unknown type of %string
                      p %q1foo1
                           ^
                  exit 1
ruby-2.5.0-preview1   -e:1: unknown type of %string
                      p %q1foo1
                        ^~~
                  exit 1
...
ruby-2.6.8            -e:1: unknown type of %string
                      p %q1foo1
                        ^~~
                  exit 1
ruby-2.7.0-preview1   -e:1: unknown type of %string
                      p %q1foo1
                        ^~~
                  exit 1
ruby-2.7.0-preview2   -e:1: unknown type of %string
                      p %q1foo1
                        ^~~
                  exit 1
...
ruby-3.0.2            -e:1: unknown type of %string
                      p %q1foo1
                        ^~~
                  exit 1
pockepocke

%q†foo†もだめだった(parser gemでは通ってしまうけどcrubyでは通らない)

pockepocke

test.rb

(0..127).each do |n|
  next if /[a-zA-Z0-9()<>{}\[\]]/ =~ n.chr
  eval "%q#{n.chr}foo#{n.chr}"
end
$ docker run -it --rm -v $(pwd)/test.rb:/tmp/test.rb rubylang/all-ruby env ALL_RUBY_SINCE=1.8 ./all-ruby /tmp/test.rb
ruby-1.8.0
...
ruby-2.0.0-p648
ruby-2.1.0-preview1   (eval):1: warning: encountered \r in middle of line, treated as a mere space
...
ruby-2.7.4            (eval):1: warning: encountered \r in middle of line, treated as a mere space
ruby-3.0.0-preview1
...
ruby-3.0.2

MRIはこれが通る

pockepocke
diff --git a/lib/parser/lexer.rl b/lib/parser/lexer.rl
index 8574f95..c266f1e 100644
--- a/lib/parser/lexer.rl
+++ b/lib/parser/lexer.rl
@@ -518,7 +518,8 @@ class Parser::Lexer
   c_nl_zlen  = c_nl | zlen;
   c_line     = any - c_nl_zlen;
 
-  c_unicode  = c_any - 0x00..0x7f;
+  c_ascii    = 0x00..0x7f;
+  c_unicode  = c_any - c_ascii;
   c_upper    = [A-Z];
   c_lower    = [a-z_]  | c_unicode;
   c_alpha    = c_lower | c_upper;
@@ -1406,7 +1407,7 @@ class Parser::Lexer
       ':'
       => { fhold; fgoto expr_beg; };
 
-      '%s' c_any
+      '%s' c_ascii - c_alnum
       => {
         if version?(23)
           type, delimiter = tok[0..-2], tok[-1].chr
@@ -1758,14 +1759,14 @@ class Parser::Lexer
       };
 
       # %<string>
-      '%' ( any - [A-Za-z] )
+      '%' ( c_ascii - c_alnum )
       => {
         type, delimiter = @source_buffer.slice(@ts).chr, tok[-1].chr
         fgoto *push_literal(type, delimiter, @ts);
       };
 
       # %w(we are the people)
-      '%' [A-Za-z]+ c_any
+      '%' [A-Za-z] c_ascii - c_alnum
       => {
         type, delimiter = tok[0..-2], tok[-1].chr
         fgoto *push_literal(type, delimiter, @ts);

こんな感じのパッチで良いのでは。テスト書くかー。

pockepocke

'%' [A-Za-z]+ c_any+がちょっと謎い。多分正規表現の+と同じ意味。これ消してもいいと思うけどよくわからんのでPRで聞くか

このスクラップは2021/07/10にクローズされました