ruby-next-core 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +279 -0
  5. data/bin/parse +19 -0
  6. data/bin/ruby-next +16 -0
  7. data/bin/transform +21 -0
  8. data/lib/ruby-next.rb +37 -0
  9. data/lib/ruby-next/cli.rb +55 -0
  10. data/lib/ruby-next/commands/base.rb +42 -0
  11. data/lib/ruby-next/commands/nextify.rb +118 -0
  12. data/lib/ruby-next/core.rb +34 -0
  13. data/lib/ruby-next/core/array/difference_union_intersection.rb +31 -0
  14. data/lib/ruby-next/core/enumerable/filter.rb +23 -0
  15. data/lib/ruby-next/core/enumerable/filter_map.rb +50 -0
  16. data/lib/ruby-next/core/enumerable/tally.rb +28 -0
  17. data/lib/ruby-next/core/enumerator/produce.rb +22 -0
  18. data/lib/ruby-next/core/hash/merge.rb +16 -0
  19. data/lib/ruby-next/core/kernel/then.rb +12 -0
  20. data/lib/ruby-next/core/pattern_matching.rb +37 -0
  21. data/lib/ruby-next/core/proc/compose.rb +21 -0
  22. data/lib/ruby-next/core/runtime.rb +10 -0
  23. data/lib/ruby-next/language.rb +117 -0
  24. data/lib/ruby-next/language/bootsnap.rb +26 -0
  25. data/lib/ruby-next/language/eval.rb +64 -0
  26. data/lib/ruby-next/language/parser.rb +24 -0
  27. data/lib/ruby-next/language/rewriters/args_forward.rb +57 -0
  28. data/lib/ruby-next/language/rewriters/base.rb +105 -0
  29. data/lib/ruby-next/language/rewriters/endless_range.rb +60 -0
  30. data/lib/ruby-next/language/rewriters/method_reference.rb +31 -0
  31. data/lib/ruby-next/language/rewriters/numbered_params.rb +41 -0
  32. data/lib/ruby-next/language/rewriters/pattern_matching.rb +522 -0
  33. data/lib/ruby-next/language/runtime.rb +96 -0
  34. data/lib/ruby-next/language/setup.rb +43 -0
  35. data/lib/ruby-next/language/unparser.rb +8 -0
  36. data/lib/ruby-next/utils.rb +36 -0
  37. data/lib/ruby-next/version.rb +5 -0
  38. data/lib/uby-next.rb +68 -0
  39. metadata +117 -0
@@ -0,0 +1,60 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ class EndlessRange < Base
7
+ SYNTAX_PROBE = "[0, 1][1..]"
8
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.6.0")
9
+
10
+ def on_index(node)
11
+ @current_index = node
12
+ new_index = process(node.children.last)
13
+ return unless new_index != node.children.last
14
+
15
+ node.updated(
16
+ nil,
17
+ [
18
+ node.children.first,
19
+ new_index
20
+ ]
21
+ )
22
+ end
23
+
24
+ def on_erange(node)
25
+ return unless node.children.last.nil?
26
+
27
+ context.track! self
28
+
29
+ new_end =
30
+ if index_arg?(node)
31
+ s(:int, -1)
32
+ else
33
+ s(:const,
34
+ s(:const,
35
+ s(:cbase), :Float),
36
+ :INFINITY)
37
+ end
38
+
39
+ node.updated(
40
+ :irange,
41
+ [
42
+ node.children.first,
43
+ new_end
44
+ ]
45
+ )
46
+ end
47
+
48
+ alias_method :on_irange, :on_erange
49
+
50
+ private
51
+
52
+ attr_reader :current_index
53
+
54
+ def index_arg?(node)
55
+ current_index&.children&.include?(node)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ class MethodReference < Base
7
+ SYNTAX_PROBE = "Language.:transform"
8
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
9
+
10
+ def on_meth_ref(node)
11
+ context.track! self
12
+
13
+ receiver, mid = *node.children
14
+
15
+ node.updated(
16
+ :send,
17
+ [
18
+ receiver,
19
+ :method,
20
+ s(:sym, mid)
21
+ ]
22
+ )
23
+ end
24
+
25
+ unless transform(SYNTAX_PROBE) == "Language.method(:transform)"
26
+ warn_custom_parser_required_for("method reference")
27
+ end
28
+ end
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ using RubyNext
4
+
5
+ module RubyNext
6
+ module Language
7
+ module Rewriters
8
+ class NumberedParams < Base
9
+ SYNTAX_PROBE = "proc { _1 }.call(1)"
10
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
11
+
12
+ def on_numblock(node)
13
+ context.track! self
14
+
15
+ proc_or_lambda, num, *rest = *node.children
16
+
17
+ node.updated(
18
+ :block,
19
+ [
20
+ proc_or_lambda,
21
+ proc_args(num),
22
+ *rest
23
+ ]
24
+ )
25
+ end
26
+
27
+ private
28
+
29
+ def proc_args(n)
30
+ return s(:args, s(:procarg0, s(:arg, :_1))) if n == 1
31
+
32
+ (1..n).map do |numero|
33
+ s(:arg, :"_#{numero}")
34
+ end.then do |args|
35
+ s(:args, *args)
36
+ end
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,522 @@
1
+ # frozen_string_literal: true
2
+
3
+ using RubyNext
4
+
5
+ module RubyNext
6
+ module Language
7
+ module Rewriters
8
+ using(Module.new do
9
+ refine ::Parser::AST::Node do
10
+ def to_ast_node
11
+ self
12
+ end
13
+ end
14
+
15
+ refine String do
16
+ def to_ast_node
17
+ ::Parser::AST::Node.new(:str, [self])
18
+ end
19
+ end
20
+
21
+ refine Symbol do
22
+ def to_ast_node
23
+ ::Parser::AST::Node.new(:sym, [self])
24
+ end
25
+ end
26
+
27
+ refine Integer do
28
+ def to_ast_node
29
+ ::Parser::AST::Node.new(:int, [self])
30
+ end
31
+ end
32
+ end)
33
+
34
+ class PatternMatching < Base
35
+ SYNTAX_PROBE = "case 0; in 0; true; else; 1; end"
36
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
37
+
38
+ MATCHEE = :__m__
39
+ MATCHEE_ARR = :__m_arr__
40
+ MATCHEE_HASH = :__m_hash__
41
+
42
+ def on_case_match(node)
43
+ context.track! self
44
+
45
+ @deconstructed = []
46
+
47
+ matchee_ast =
48
+ s(:lvasgn, MATCHEE, node.children[0])
49
+
50
+ ifs_ast = locals.with(
51
+ matchee: MATCHEE,
52
+ arr: MATCHEE_ARR,
53
+ hash: MATCHEE_HASH
54
+ ) do
55
+ build_if_clause(node.children[1], node.children[2..-1])
56
+ end
57
+
58
+ node.updated(
59
+ :begin,
60
+ [
61
+ matchee_ast, ifs_ast
62
+ ]
63
+ )
64
+ end
65
+
66
+ def on_in_match(node)
67
+ context.track! self
68
+
69
+ @deconstructed = []
70
+
71
+ matchee =
72
+ s(:lvasgn, MATCHEE, node.children[0])
73
+
74
+ pattern =
75
+ locals.with(
76
+ matchee: MATCHEE,
77
+ arr: MATCHEE_ARR,
78
+ hash: MATCHEE_HASH
79
+ ) do
80
+ send(
81
+ :"#{node.children[1].type}_clause",
82
+ node.children[1]
83
+ ).then do |node|
84
+ s(:or,
85
+ node,
86
+ no_matching_pattern)
87
+ end
88
+ end
89
+
90
+ node.updated(
91
+ :and,
92
+ [
93
+ matchee,
94
+ pattern
95
+ ]
96
+ )
97
+ end
98
+
99
+ private
100
+
101
+ def build_if_clause(node, rest)
102
+ if node&.type == :in_pattern
103
+ build_in_pattern(node, rest)
104
+ else
105
+ raise "Unexpected else in the middle of case ... in" if rest && rest.size > 0
106
+ # else clause must be present
107
+ node || no_matching_pattern
108
+ end
109
+ end
110
+
111
+ def build_in_pattern(clause, rest)
112
+ [
113
+ with_guard(
114
+ send(
115
+ :"#{clause.children[0].type}_clause",
116
+ clause.children[0]
117
+ ),
118
+ clause.children[1] # guard
119
+ ),
120
+ clause.children[2] || s(:nil) # expression
121
+ ].then do |children|
122
+ if rest && rest.size > 0
123
+ children << build_if_clause(rest.first, rest[1..-1])
124
+ end
125
+
126
+ s(:if, *children)
127
+ end
128
+ end
129
+
130
+ def const_pattern_clause(node)
131
+ const, pattern = *node.children
132
+
133
+ case_eq_clause(const).then do |node|
134
+ next node if pattern.nil?
135
+
136
+ s(:and,
137
+ node,
138
+ send(:"#{pattern.type}_clause", pattern))
139
+ end
140
+ end
141
+
142
+ def match_alt_clause(node)
143
+ children = node.children.map do |child|
144
+ send :"#{child.type}_clause", child
145
+ end
146
+ s(:or, *children)
147
+ end
148
+
149
+ def match_as_clause(node)
150
+ s(:and,
151
+ case_eq_clause(node.children[0]),
152
+ match_var_clause(node.children[1], s(:lvar, locals[:matchee])))
153
+ end
154
+
155
+ def match_var_clause(node, left = s(:lvar, locals[:matchee]))
156
+ s(:or,
157
+ s(:lvasgn, node.children[0], left),
158
+ s(:true)) # rubocop:disable Lint/BooleanSymbol
159
+ end
160
+
161
+ def pin_clause(node)
162
+ case_eq_clause node.children[0]
163
+ end
164
+
165
+ def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
166
+ s(:send,
167
+ node, :===, right)
168
+ end
169
+
170
+ #=========== ARRAY PATTERN (START) ===============
171
+
172
+ def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
173
+ deconstruct_node(matchee).then do |dnode|
174
+ right =
175
+ if node.children.empty?
176
+ case_eq_clause(s(:array), s(:lvar, locals[:arr]))
177
+ else
178
+ array_element(0, *node.children)
179
+ end
180
+
181
+ # already deconsrtructed
182
+ next right if dnode.nil?
183
+
184
+ # if there is no rest or tail, match the size first
185
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
186
+ right =
187
+ s(:and,
188
+ s(:send,
189
+ node.children.size.to_ast_node,
190
+ :==,
191
+ s(:send, s(:lvar, locals[:arr]), :size)),
192
+ right)
193
+ end
194
+
195
+ s(:and,
196
+ dnode,
197
+ right)
198
+ end
199
+ end
200
+
201
+ alias array_pattern_with_tail_clause array_pattern_clause
202
+
203
+ def deconstruct_node(matchee)
204
+ # only deconstruct once per case
205
+ return if deconstructed.include?(locals[:arr])
206
+
207
+ context.use_ruby_next!
208
+
209
+ right = s(:send, matchee, :deconstruct)
210
+
211
+ deconstructed << locals[:arr]
212
+ s(:and,
213
+ s(:or,
214
+ s(:lvasgn, locals[:arr], right),
215
+ s(:true)), # rubocop:disable Lint/BooleanSymbol
216
+ s(:or,
217
+ case_eq_clause(s(:const, nil, :Array), s(:lvar, locals[:arr])),
218
+ raise_error(:TypeError)))
219
+ end
220
+
221
+ def array_element(index, head, *tail)
222
+ return array_match_rest(index, head, *tail) if head.type == :match_rest
223
+
224
+ send("#{head.type}_array_element", head, index).then do |node|
225
+ next node if tail.empty?
226
+
227
+ s(:and,
228
+ node,
229
+ array_element(index + 1, *tail))
230
+ end
231
+ end
232
+
233
+ def array_match_rest(index, node, *tail)
234
+ child = node.children[0]
235
+ rest = arr_rest_items(index, tail.size).then do |r|
236
+ next r unless child
237
+ match_var_clause(
238
+ child,
239
+ r
240
+ )
241
+ end
242
+
243
+ return rest if tail.empty?
244
+
245
+ s(:and,
246
+ rest,
247
+ array_rest_element(*tail))
248
+ end
249
+
250
+ def array_rest_element(head, *tail)
251
+ send("#{head.type}_array_element", head, -(tail.size + 1)).then do |node|
252
+ next node if tail.empty?
253
+
254
+ s(:and,
255
+ node,
256
+ array_rest_element(*tail))
257
+ end
258
+ end
259
+
260
+ def array_pattern_array_element(node, index)
261
+ element = arr_item_at(index)
262
+ locals.with(arr: locals[:arr, index]) do
263
+ array_pattern_clause(node, element)
264
+ end
265
+ end
266
+
267
+ def hash_pattern_array_element(node, index)
268
+ element = arr_item_at(index)
269
+ locals.with(hash: locals[:arr, index]) do
270
+ hash_pattern_clause(node, element)
271
+ end
272
+ end
273
+
274
+ def match_alt_array_element(node, index)
275
+ children = node.children.map do |child, i|
276
+ send :"#{child.type}_array_element", child, index
277
+ end
278
+ s(:or, *children)
279
+ end
280
+
281
+ def match_var_array_element(node, index)
282
+ match_var_clause(node, arr_item_at(index))
283
+ end
284
+
285
+ def pin_array_element(node, index)
286
+ case_eq_array_element node.children[0], index
287
+ end
288
+
289
+ def case_eq_array_element(node, index)
290
+ case_eq_clause(node, arr_item_at(index))
291
+ end
292
+
293
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
294
+ s(:index, arr, index.to_ast_node)
295
+ end
296
+
297
+ def arr_rest_items(index, size, arr = s(:lvar, locals[:arr]))
298
+ s(:index,
299
+ arr,
300
+ s(:irange,
301
+ s(:int, index),
302
+ s(:int, -(size + 1))))
303
+ end
304
+
305
+ #=========== ARRAY PATTERN (END) ===============
306
+
307
+ #=========== HASH PATTERN (START) ===============
308
+
309
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
310
+ # Optimization: avoid hash modifications when not needed
311
+ # (we use #dup and #delete when "reading" values when **rest is present
312
+ # to assign the rest of the hash copy to it)
313
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
314
+ keys = hash_pattern_keys(node.children)
315
+
316
+ deconstruct_keys_node(keys, matchee).then do |dnode|
317
+ right =
318
+ if node.children.empty?
319
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
320
+ else
321
+ hash_element(*node.children)
322
+ end
323
+
324
+ return dnode if right.nil?
325
+
326
+ s(:and,
327
+ dnode,
328
+ right)
329
+ end
330
+ end
331
+
332
+ def hash_pattern_keys(children)
333
+ return s(:nil) if children.empty?
334
+
335
+ children.filter_map do |child|
336
+ # Skip ** without var
337
+ next if child.type == :match_rest && child.children.empty?
338
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
339
+
340
+ send("#{child.type}_hash_key", child)
341
+ end.then { |keys| s(:array, *keys) }
342
+ end
343
+
344
+ def pair_hash_key(node)
345
+ node.children[0]
346
+ end
347
+
348
+ def match_var_hash_key(node)
349
+ s(:sym, node.children[0])
350
+ end
351
+
352
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
353
+ # Deconstruct once and use a copy of the hash for each pattern if we need **rest.
354
+ hash_dup =
355
+ if @hash_match_rest
356
+ s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup))
357
+ else
358
+ s(:lvasgn, locals[:hash], s(:lvar, locals[:hash, :src]))
359
+ end
360
+
361
+ # Create a copy of the original hash if already deconstructed
362
+ return hash_dup if deconstructed.include?(locals[:hash])
363
+
364
+ context.use_ruby_next!
365
+
366
+ deconstructed << locals[:hash]
367
+
368
+ right = s(:send,
369
+ matchee, :deconstruct_keys, keys)
370
+
371
+ s(:and,
372
+ s(:or,
373
+ s(:lvasgn, locals[:hash, :src], right),
374
+ s(:true)), # rubocop:disable Lint/BooleanSymbol
375
+ s(:and,
376
+ s(:or,
377
+ case_eq_clause(s(:const, nil, :Hash), s(:lvar, locals[:hash, :src])),
378
+ raise_error(:TypeError)),
379
+ hash_dup))
380
+ end
381
+
382
+ def hash_pattern_hash_element(node, key)
383
+ element = hash_value_at(key)
384
+ locals.with(hash: locals[:hash, deconstructed.size]) do
385
+ hash_pattern_clause(node, element)
386
+ end
387
+ end
388
+
389
+ def array_pattern_hash_element(node, key)
390
+ element = hash_value_at(key)
391
+ locals.with(arr: locals[:hash, deconstructed.size]) do
392
+ array_pattern_clause(node, element)
393
+ end
394
+ end
395
+
396
+ def hash_element(head, *tail)
397
+ send("#{head.type}_hash_element", head).then do |node|
398
+ next node if tail.empty?
399
+
400
+ right = hash_element(*tail)
401
+
402
+ next node if right.nil?
403
+
404
+ s(:and,
405
+ node,
406
+ right)
407
+ end
408
+ end
409
+
410
+ def pair_hash_element(node, _key = nil)
411
+ key, val = *node.children
412
+ send("#{val.type}_hash_element", val, key)
413
+ end
414
+
415
+ def match_alt_hash_element(node, key)
416
+ element_node = s(:lvasgn, locals[:hash, :el], hash_value_at(key))
417
+
418
+ children = locals.with(hash_element: locals[:hash, :el]) do
419
+ node.children.map do |child, i|
420
+ send :"#{child.type}_hash_element", child, key
421
+ end
422
+ end
423
+
424
+ s(:and,
425
+ s(:or,
426
+ element_node,
427
+ s(:true)), # rubocop:disable Lint/BooleanSymbol
428
+ s(:or, *children))
429
+ end
430
+
431
+ def match_var_hash_element(node, key = nil)
432
+ key ||= node.children[0]
433
+ # We need to check whether key is present first
434
+ s(:and,
435
+ hash_has_key(key),
436
+ match_var_clause(node, hash_value_at(key)))
437
+ end
438
+
439
+ def match_nil_pattern_hash_element(node, _key = nil)
440
+ s(:send,
441
+ s(:lvar, locals[:hash]),
442
+ :empty?)
443
+ end
444
+
445
+ def match_rest_hash_element(node, _key = nil)
446
+ # case {}; in **; end
447
+ return if node.children.empty?
448
+
449
+ child = node.children[0]
450
+
451
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
452
+
453
+ match_var_clause(child, s(:lvar, locals[:hash]))
454
+ end
455
+
456
+ def case_eq_hash_element(node, key)
457
+ case_eq_clause node, hash_value_at(key)
458
+ end
459
+
460
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
461
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
462
+
463
+ if @hash_match_rest
464
+ s(:send,
465
+ hash, :delete,
466
+ key.to_ast_node)
467
+ else
468
+ s(:index,
469
+ hash,
470
+ key.to_ast_node)
471
+ end
472
+ end
473
+
474
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
475
+ s(:send,
476
+ hash, :key?,
477
+ key.to_ast_node)
478
+ end
479
+
480
+ #=========== HASH PATTERN (END) ===============
481
+
482
+ def with_guard(node, guard)
483
+ return node unless guard
484
+
485
+ s(:and,
486
+ node,
487
+ guard.children[0]).then do |expr|
488
+ next expr unless guard.type == :unless_guard
489
+ s(:send, expr, :!)
490
+ end
491
+ end
492
+
493
+ def no_matching_pattern
494
+ raise_error :NoMatchingPatternError
495
+ end
496
+
497
+ def raise_error(type)
498
+ s(:send, s(:const, nil, :Kernel), :raise,
499
+ s(:const, nil, type),
500
+ s(:send,
501
+ s(:lvar, locals[:matchee]), :inspect))
502
+ end
503
+
504
+ def respond_to_missing?(mid, *)
505
+ return true if mid.match?(/_(clause|array_element)/)
506
+ super
507
+ end
508
+
509
+ def method_missing(mid, *args, &block)
510
+ return case_eq_clause(args.first) if mid.match?(/_clause$/)
511
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
512
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
513
+ super
514
+ end
515
+
516
+ private
517
+
518
+ attr_reader :deconstructed
519
+ end
520
+ end
521
+ end
522
+ end