ruby_parser 3.15.0 → 3.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. checksums.yaml +4 -4
  2. checksums.yaml.gz.sig +0 -0
  3. data/History.rdoc +151 -0
  4. data/Manifest.txt +7 -0
  5. data/README.rdoc +9 -6
  6. data/Rakefile +141 -31
  7. data/bin/ruby_parse_extract_error +1 -1
  8. data/compare/normalize.rb +8 -3
  9. data/debugging.md +133 -0
  10. data/gauntlet.md +107 -0
  11. data/lib/rp_extensions.rb +15 -36
  12. data/lib/rp_stringscanner.rb +20 -51
  13. data/lib/ruby20_parser.rb +7544 -3633
  14. data/lib/ruby20_parser.y +335 -257
  15. data/lib/ruby21_parser.rb +7518 -3678
  16. data/lib/ruby21_parser.y +330 -254
  17. data/lib/ruby22_parser.rb +7652 -3689
  18. data/lib/ruby22_parser.y +334 -256
  19. data/lib/ruby23_parser.rb +7659 -3702
  20. data/lib/ruby23_parser.y +334 -256
  21. data/lib/ruby24_parser.rb +7748 -3721
  22. data/lib/ruby24_parser.y +334 -256
  23. data/lib/ruby25_parser.rb +7748 -3721
  24. data/lib/ruby25_parser.y +334 -256
  25. data/lib/ruby26_parser.rb +7755 -3726
  26. data/lib/ruby26_parser.y +334 -255
  27. data/lib/ruby27_parser.rb +10290 -4518
  28. data/lib/ruby27_parser.y +933 -254
  29. data/lib/ruby30_parser.rb +13258 -0
  30. data/lib/ruby30_parser.y +3459 -0
  31. data/lib/ruby31_parser.rb +13638 -0
  32. data/lib/ruby31_parser.y +3493 -0
  33. data/lib/ruby3_parser.yy +3548 -0
  34. data/lib/ruby_lexer.rb +277 -599
  35. data/lib/ruby_lexer.rex +28 -21
  36. data/lib/ruby_lexer.rex.rb +60 -24
  37. data/lib/ruby_lexer_strings.rb +638 -0
  38. data/lib/ruby_parser.rb +4 -0
  39. data/lib/ruby_parser.yy +974 -261
  40. data/lib/ruby_parser_extras.rb +355 -114
  41. data/test/test_ruby_lexer.rb +226 -129
  42. data/test/test_ruby_parser.rb +1653 -267
  43. data/tools/munge.rb +36 -8
  44. data/tools/ripper.rb +15 -10
  45. data.tar.gz.sig +0 -0
  46. metadata +55 -37
  47. metadata.gz.sig +0 -0
data/debugging.md CHANGED
@@ -55,3 +55,136 @@ From there? Good luck. I'm currently trying to backtrack from rule
55
55
  reductions to state change differences. I'd like to figure out a way
56
56
  to go from this sort of diff to a reasonable test that checks state
57
57
  changes but I don't have that set up at this point.
58
+
59
+ ## Adding New Grammar Productions
60
+
61
+ Ruby adds stuff to the parser ALL THE TIME. It's actually hard to keep
62
+ up with, but I've added some tools and shown what a typical workflow
63
+ looks like. Let's say you want to add ruby 2.7's "beginless range" (eg
64
+ `..42`).
65
+
66
+ Whenever there's a language feature missing, I start with comparing
67
+ the parse trees between MRI and RP:
68
+
69
+ ### Structural Comparing
70
+
71
+ There's a bunch of rake tasks `compare27`, `compare26`, etc that try
72
+ to normalize and diff MRI's parse.y parse tree (just the structure of
73
+ the tree in yacc) to ruby\_parser's parse tree (racc). It's the first
74
+ thing I do when I'm adding a new version. Stub out all the version
75
+ differences, and then start to diff the structure and move
76
+ ruby\_parser towards the new changes.
77
+
78
+ Some differences are just gonna be there... but here's an example of a
79
+ real diff between MRI 2.7 and ruby_parser as of today:
80
+
81
+ ```diff
82
+ arg tDOT3 arg
83
+ arg tDOT2
84
+ arg tDOT3
85
+ - tBDOT2 arg
86
+ - tBDOT3 arg
87
+ arg tPLUS arg
88
+ arg tMINUS arg
89
+ arg tSTAR2 arg
90
+ ```
91
+
92
+ This is a new language feature that ruby_parser doesn't handle yet.
93
+ It's in MRI (the left hand side of the diff) but not ruby\_parser (the
94
+ right hand side) so it is a `-` or missing line.
95
+
96
+ Some other diffs will have both `+` and `-` lines. That usually
97
+ happens when MRI has been refactoring the grammar. Sometimes I choose
98
+ to adapt those refactorings and sometimes it starts to get too
99
+ difficult to maintain multiple versions of ruby parsing in a single
100
+ file.
101
+
102
+ But! This structural comparing is always a place you should look when
103
+ ruby_parser is failing to parse something. Maybe it just hasn't been
104
+ implemented yet and the easiest place to look is the diff.
105
+
106
+ ### Starting Test First
107
+
108
+ The next thing I do is to add a parser test to cover that feature. I
109
+ usually start with the parser and work backwards towards the lexer as
110
+ needed, as I find it structures things properly and keeps things goal
111
+ oriented.
112
+
113
+ So, make a new parser test, usually in the versioned section of the
114
+ parser tests.
115
+
116
+ ```
117
+ def test_beginless2
118
+ rb = "..10\n; ..a\n; c"
119
+ pt = s(:block,
120
+ s(:dot2, nil, s(:lit, 0).line(1)).line(1),
121
+ s(:dot2, nil, s(:call, nil, :a).line(2)).line(2),
122
+ s(:call, nil, :c).line(3)).line(1)
123
+
124
+ assert_parse_line rb, pt, 1
125
+
126
+ flunk "not done yet"
127
+ end
128
+ ```
129
+
130
+ (In this case copied and modified the tests for open ranges from 2.6)
131
+ and run it to get my first error:
132
+
133
+ ```
134
+ % rake N=/beginless/
135
+
136
+ ...
137
+
138
+ E
139
+
140
+ Finished in 0.021814s, 45.8421 runs/s, 0.0000 assertions/s.
141
+
142
+ 1) Error:
143
+ TestRubyParserV27#test_whatevs:
144
+ Racc::ParseError: (string):1 :: parse error on value ".." (tDOT2)
145
+ GEMS/2.7.0/gems/racc-1.5.0/lib/racc/parser.rb:538:in `on_error'
146
+ WORK/ruby_parser/dev/lib/ruby_parser_extras.rb:1304:in `on_error'
147
+ (eval):3:in `_racc_do_parse_c'
148
+ (eval):3:in `do_parse'
149
+ WORK/ruby_parser/dev/lib/ruby_parser_extras.rb:1329:in `block in process'
150
+ RUBY/lib/ruby/2.7.0/timeout.rb:95:in `block in timeout'
151
+ RUBY/lib/ruby/2.7.0/timeout.rb:33:in `block in catch'
152
+ RUBY/lib/ruby/2.7.0/timeout.rb:33:in `catch'
153
+ RUBY/lib/ruby/2.7.0/timeout.rb:33:in `catch'
154
+ RUBY/lib/ruby/2.7.0/timeout.rb:110:in `timeout'
155
+ WORK/ruby_parser/dev/lib/ruby_parser_extras.rb:1317:in `process'
156
+ WORK/ruby_parser/dev/test/test_ruby_parser.rb:4198:in `assert_parse'
157
+ WORK/ruby_parser/dev/test/test_ruby_parser.rb:4221:in `assert_parse_line'
158
+ WORK/ruby_parser/dev/test/test_ruby_parser.rb:4451:in `test_whatevs'
159
+ ```
160
+
161
+ For starters, we know the missing production is for `tBDOT2 arg`. It
162
+ is currently blowing up because it is getting `tDOT2` and simply
163
+ doesn't know what to do with it, so it raises the error. As the diff
164
+ suggests, that's the wrong token to begin with, so it is probably time
165
+ to also create a lexer test:
166
+
167
+ ```
168
+ def test_yylex_bdot2
169
+ assert_lex3("..42",
170
+ s(:dot2, nil, s(:lit, 42)),
171
+
172
+ :tBDOT2, "..", EXPR_BEG,
173
+ :tINTEGER, "42", EXPR_NUM)
174
+
175
+ flunk "not done yet"
176
+ end
177
+ ```
178
+
179
+ This one is mostly speculative at this point. It says "if we're lexing
180
+ this string, we should get this sexp if we fully parse it, and the
181
+ lexical stream should look like this"... That last bit is mostly made
182
+ up at this point. Sometimes I don't know exactly what expression state
183
+ things should be in until I start really digging in.
184
+
185
+ At this point, I have 2 failing tests that are directing me in the
186
+ right direction. It's now a matter of digging through
187
+ `compare/parse26.y` to see how the lexer differs and implementing
188
+ it...
189
+
190
+ But this is a good start to the doco for now. I'll add more later.
data/gauntlet.md ADDED
@@ -0,0 +1,107 @@
1
+ # Running the Gauntlet
2
+
3
+ ## Maintaining a Gem Mirror
4
+
5
+ I use rubygems-mirror to keep an archive of all the latest rubygems on
6
+ an external disk. Here is the config:
7
+
8
+ ```
9
+ ---
10
+ - from: https://rubygems.org
11
+ to: /Volumes/StuffA/gauntlet/mirror
12
+ parallelism: 10
13
+ retries: 3
14
+ delete: true
15
+ skiperror: true
16
+ hashdir: true
17
+ ```
18
+
19
+ And I update using rake:
20
+
21
+ ```
22
+ % cd GIT/rubygems/rubygems-mirror
23
+ % git down
24
+ % rake mirror:latest
25
+ % /Volumes/StuffA/gauntlet/bin/cleanup.rb -y -v
26
+ ```
27
+
28
+ This rather quickly updates my mirror to the latest versions of
29
+ everything and then deletes all old versions. I then run a cleanup
30
+ script that fixes the file dates to their publication date and deletes
31
+ any gems that have invalid specs. This can argue with the mirror a
32
+ bit, but it is pretty minimal (currently ~20 bad gems).
33
+
34
+ ## Curating an Archive of Ruby Files
35
+
36
+ Next, I process the gem mirror into a much more digestable structure
37
+ using `unpack_gems.rb`.
38
+
39
+ ```
40
+ % cd RP/gauntlet
41
+ % time caffeinate /Volumes/StuffA/gauntlet/bin/unpack_gems.rb -v [-a] ; say done
42
+ ... waaaait ...
43
+ % DIR=gauntlet.$(today).(all|new).noindex
44
+ % mv hashed.noindex $DIR
45
+ % tar vc -T <(fd -tf . $DIR | sort) | zstd -5 -T0 --long > archives/$DIR.tar.zst ; say done
46
+ % ./bin/sync.sh
47
+ ```
48
+
49
+ This script filters all the newer (< 1 year old) gems (unless `-a` is
50
+ used), unpacks them, finds all the files that look like they're valid
51
+ ruby, ensures they're valid ruby (using the current version of ruby to
52
+ compile them), and then moves them into a SHA dir structure that looks
53
+ something like this:
54
+
55
+ ```
56
+ hashed.noindex/a/b/c/<full_file_sha>.rb
57
+ ```
58
+
59
+ This removes all duplicates and puts everything in a fairly even,
60
+ wide, flat directory layout.
61
+
62
+ This process takes a very long time, even with a lot of
63
+ parallelization. There are currently about 160k gems in the mirror.
64
+ Unpacking, validating, SHA'ing everything is disk and CPU intensive.
65
+ The `.noindex` extension stops spotlight from indexing the continous
66
+ churn of files being unpacked and moved and saves time.
67
+
68
+ Finally, I rename and archive it all up (currently using zstd to
69
+ compress).
70
+
71
+ ### Stats
72
+
73
+ ```
74
+ 9696 % find gauntlet.$(today).noindex -type f | lc
75
+ 561270
76
+ 3.5G gauntlet.2021-08-06.noindex
77
+ 239M gauntlet.2021-08-06.noindex.tar.zst
78
+ ```
79
+
80
+ So I wind up with a little over half a million unique ruby files to
81
+ parse. It's about 3.5g but compresses very nicely down to 240m
82
+
83
+ ## Running the Gauntlet
84
+
85
+ Assuming you're starting from scratch, unpack the archive once:
86
+
87
+ ```
88
+ % zstdcat gauntlet.$(today).noindex.tar.zst | tar x
89
+ ```
90
+
91
+ Then, either run a single process (easier to read):
92
+
93
+ ```
94
+ % ./gauntlet/bin/gauntlet.rb gauntlet/*.noindex/?
95
+ ```
96
+
97
+ Or max out your machine using xargs (note the `-P 16` and choose accordingly):
98
+
99
+ ```
100
+ % ls -d gauntlet/*.noindex/?/? | time xargs -n 1 -P 16 ./gauntlet/bin/gauntlet.rb
101
+ ```
102
+
103
+ In another terminal I usually monitor the progress like so:
104
+
105
+ ```
106
+ % while true ; do clear; fd . -t d -t e gauntlet/*.noindex -X rmdir -p 2> /dev/null ; for D in gauntlet/*.noindex/? ; do echo -n "$D: "; fd .rb $D | wc -l ; done ; echo ; sleep 30 ; done
107
+ ```
data/lib/rp_extensions.rb CHANGED
@@ -12,26 +12,24 @@ class Regexp
12
12
  end
13
13
  # :startdoc:
14
14
 
15
- ############################################################
16
- # HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
17
-
18
- unless "".respond_to?(:grep) then
19
- class String
20
- def grep re
21
- lines.grep re
22
- end
15
+ class Array
16
+ def prepend *vals
17
+ self[0,0] = vals
23
18
  end
24
- end
19
+ end unless [].respond_to?(:prepend)
25
20
 
26
- class String
27
- ##
28
- # This is a hack used by the lexer to sneak in line numbers at the
29
- # identifier level. This should be MUCH smaller than making
30
- # process_token return [value, lineno] and modifying EVERYTHING that
31
- # reduces tIDENTIFIER.
21
+ # :stopdoc:
22
+ class Symbol
23
+ def end_with? o
24
+ self.to_s.end_with? o
25
+ end
26
+ end unless :woot.respond_to?(:end_with?)
27
+ # :startdoc:
32
28
 
33
- attr_accessor :lineno
29
+ ############################################################
30
+ # HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK HACK
34
31
 
32
+ class String
35
33
  def clean_caller
36
34
  self.sub(File.dirname(__FILE__), "./lib").sub(/:in.*/, "")
37
35
  end if $DEBUG
@@ -40,34 +38,15 @@ end
40
38
  require "sexp"
41
39
 
42
40
  class Sexp
43
- attr_writer :paren
41
+ attr_writer :paren # TODO: retire
44
42
 
45
43
  def paren
46
44
  @paren ||= false
47
45
  end
48
46
 
49
- def value
50
- raise "multi item sexp" if size > 2
51
- last
52
- end
53
-
54
- def to_sym
55
- raise "no: #{self.inspect}.to_sym is a bug"
56
- self.value.to_sym
57
- end
58
-
59
- alias :add :<<
60
-
61
- def add_all x
62
- self.concat x.sexp_body
63
- end
64
-
65
47
  def block_pass?
66
48
  any? { |s| Sexp === s && s.sexp_type == :block_pass }
67
49
  end
68
-
69
- alias :node_type :sexp_type
70
- alias :values :sexp_body # TODO: retire
71
50
  end
72
51
 
73
52
  # END HACK
@@ -1,64 +1,33 @@
1
1
  require "strscan"
2
2
 
3
3
  class RPStringScanner < StringScanner
4
- # if ENV['TALLY'] then
5
- # alias :old_getch :getch
6
- # def getch
7
- # warn({:getch => caller[0]}.inspect)
8
- # old_getch
9
- # end
10
- # end
11
-
12
- if "".respond_to? :encoding then
13
- if "".respond_to? :byteslice then
14
- def string_to_pos
15
- string.byteslice(0, pos)
16
- end
17
- else
18
- def string_to_pos
19
- string.bytes.first(pos).pack("c*").force_encoding(string.encoding)
20
- end
21
- end
22
-
23
- def charpos
24
- string_to_pos.length
25
- end
26
- else
27
- alias :charpos :pos
28
-
29
- def string_to_pos
30
- string[0..pos]
31
- end
32
- end
33
-
34
- def unread_many str # TODO: remove this entirely - we should not need it
35
- warn({:unread_many => caller[0]}.inspect) if ENV['TALLY']
36
- begin
37
- string[charpos, 0] = str
38
- rescue IndexError
39
- # HACK -- this is a bandaid on a dirty rag on an open festering wound
40
- end
41
- end
42
-
43
- if ENV['DEBUG'] then
44
- alias :old_getch :getch
4
+ if ENV["DEBUG"] || ENV["TALLY"] then
45
5
  def getch
46
- c = self.old_getch
47
- p :getch => [c, caller.first]
6
+ c = super
7
+ where = caller.drop_while { |s| s =~ /(getch|nextc).$/ }.first
8
+ where = where.split(/:/).first(2).join(":")
9
+ if ENV["TALLY"] then
10
+ d getch:where
11
+ else
12
+ d getch:[c, where]
13
+ end
48
14
  c
49
15
  end
50
16
 
51
- alias :old_scan :scan
52
17
  def scan re
53
- s = old_scan re
54
- where = caller[1].split(/:/).first(2).join(":")
55
- d :scan => [s, where] if s
18
+ s = super
19
+ where = caller.drop_while { |x| x =~ /scan.$/ }.first
20
+ where = where.split(/:/).first(2).join(":")
21
+ if ENV["TALLY"] then
22
+ d scan:[where]
23
+ else
24
+ d scan:[s, where] if s
25
+ end
56
26
  s
57
27
  end
58
- end
59
28
 
60
- def d o
61
- $stderr.puts o.inspect
29
+ def d o
30
+ STDERR.puts o.inspect
31
+ end
62
32
  end
63
33
  end
64
-