ruby-next-core 0.2.0

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 (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