ruby-next-core 0.15.3 → 1.0.0.rc.1

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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +118 -48
  4. data/bin/mspec +11 -0
  5. data/lib/.rbnext/2.1/ruby-next/commands/nextify.rb +295 -0
  6. data/lib/.rbnext/2.1/ruby-next/core.rb +10 -2
  7. data/lib/.rbnext/2.1/ruby-next/language.rb +54 -10
  8. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +82 -2
  9. data/lib/.rbnext/2.3/ruby-next/config.rb +79 -0
  10. data/lib/.rbnext/2.3/ruby-next/core/data.rb +159 -0
  11. data/lib/.rbnext/2.3/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
  12. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +6 -32
  13. data/lib/.rbnext/2.3/ruby-next/utils.rb +3 -22
  14. data/lib/.rbnext/2.6/ruby-next/core/data.rb +159 -0
  15. data/lib/.rbnext/2.7/ruby-next/core/data.rb +159 -0
  16. data/lib/.rbnext/2.7/ruby-next/core.rb +10 -2
  17. data/lib/.rbnext/2.7/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  18. data/lib/.rbnext/2.7/ruby-next/language/rewriters/2.7/pattern_matching.rb +2 -2
  19. data/lib/.rbnext/2.7/ruby-next/language/rewriters/text.rb +132 -0
  20. data/lib/.rbnext/3.2/ruby-next/commands/base.rb +55 -0
  21. data/lib/.rbnext/3.2/ruby-next/language/rewriters/2.7/pattern_matching.rb +1095 -0
  22. data/lib/.rbnext/3.2/ruby-next/rubocop.rb +210 -0
  23. data/lib/ruby-next/commands/nextify.rb +84 -2
  24. data/lib/ruby-next/config.rb +27 -0
  25. data/lib/ruby-next/core/data.rb +159 -0
  26. data/lib/ruby-next/core/matchdata/deconstruct.rb +9 -0
  27. data/lib/ruby-next/core/matchdata/deconstruct_keys.rb +20 -0
  28. data/lib/ruby-next/core/matchdata/named_captures.rb +11 -0
  29. data/lib/ruby-next/core/refinement/import.rb +44 -36
  30. data/lib/ruby-next/core/time/deconstruct_keys.rb +30 -0
  31. data/lib/ruby-next/core.rb +10 -2
  32. data/lib/ruby-next/irb.rb +2 -2
  33. data/lib/ruby-next/language/bootsnap.rb +2 -25
  34. data/lib/ruby-next/language/paco_parser.rb +7 -0
  35. data/lib/ruby-next/language/paco_parsers/base.rb +47 -0
  36. data/lib/ruby-next/language/paco_parsers/comments.rb +26 -0
  37. data/lib/ruby-next/language/paco_parsers/string_literals.rb +109 -0
  38. data/lib/ruby-next/language/parser.rb +24 -2
  39. data/lib/ruby-next/language/rewriters/3.0/args_forward_leading.rb +2 -2
  40. data/lib/ruby-next/language/rewriters/3.1/oneline_pattern_parensless.rb +1 -1
  41. data/lib/ruby-next/language/rewriters/3.2/anonymous_restargs.rb +104 -0
  42. data/lib/ruby-next/language/rewriters/abstract.rb +57 -0
  43. data/lib/ruby-next/language/rewriters/base.rb +6 -32
  44. data/lib/ruby-next/language/rewriters/edge/it_param.rb +58 -0
  45. data/lib/ruby-next/language/rewriters/edge.rb +12 -0
  46. data/lib/ruby-next/language/rewriters/proposed/bind_vars_pattern.rb +3 -0
  47. data/lib/ruby-next/language/rewriters/proposed/method_reference.rb +9 -20
  48. data/lib/ruby-next/language/rewriters/text.rb +132 -0
  49. data/lib/ruby-next/language/runtime.rb +9 -86
  50. data/lib/ruby-next/language/setup.rb +5 -2
  51. data/lib/ruby-next/language/unparser.rb +5 -0
  52. data/lib/ruby-next/language.rb +54 -10
  53. data/lib/ruby-next/pry.rb +1 -1
  54. data/lib/ruby-next/rubocop.rb +2 -0
  55. data/lib/ruby-next/utils.rb +3 -22
  56. data/lib/ruby-next/version.rb +1 -1
  57. data/lib/uby-next.rb +2 -2
  58. metadata +65 -12
@@ -0,0 +1,1095 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ using RubyNext
7
+
8
+ using(Module.new do
9
+ refine ::Parser::AST::Node do
10
+ def to_ast_node
11
+ self
12
+ end
13
+
14
+ # Useful to generate simple operation nodes
15
+ # (e.g., 'a + b')
16
+ def -(other)
17
+ ::Parser::AST::Node.new(:send, [self, :-, other.to_ast_node])
18
+ end
19
+
20
+ def +(other)
21
+ ::Parser::AST::Node.new(:send, [self, :+, other.to_ast_node])
22
+ end
23
+ end
24
+
25
+ refine String do
26
+ def to_ast_node
27
+ ::Parser::AST::Node.new(:str, [self])
28
+ end
29
+ end
30
+
31
+ refine Symbol do
32
+ def to_ast_node
33
+ ::Parser::AST::Node.new(:sym, [self])
34
+ end
35
+ end
36
+
37
+ refine Integer do
38
+ def to_ast_node
39
+ ::Parser::AST::Node.new(:int, [self])
40
+ end
41
+ end
42
+ end)
43
+
44
+ # We can memoize structural predicates to avoid double calculation.
45
+ #
46
+ # For example, consider the following case and the corresponding predicate chains:
47
+ #
48
+ # case val
49
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
50
+ # in [:created, 201] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
51
+ # in [401 | 403] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_1]
52
+ # end
53
+ #
54
+ # We can minimize the number of predicate calls by storing the intermediate values (prefixed with `p_`) and using them
55
+ # in the subsequent calls:
56
+ #
57
+ # case val
58
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
59
+ # in [:created, 201] #=> [:p_deconstructed, :p_arr_size_2]
60
+ # in [401 | 403] #=> [:p_deconstructed, :arr_size_is_1]
61
+ # end
62
+ #
63
+ # This way we mimic a naive decision tree algorithim.
64
+ module Predicates
65
+ class Processor < ::Parser::TreeRewriter
66
+ attr_reader :predicates
67
+
68
+ def initialize(predicates)
69
+ @predicates = predicates
70
+ super()
71
+ end
72
+
73
+ def on_lvasgn(node)
74
+ lvar, val = *node.children
75
+ if predicates.store[lvar] == false
76
+ process(val)
77
+ else
78
+ node
79
+ end
80
+ end
81
+
82
+ def on_and(node)
83
+ left, right = *node.children
84
+
85
+ if truthy(left)
86
+ process(right)
87
+ elsif truthy(right)
88
+ process(left)
89
+ else
90
+ node.updated(
91
+ :and,
92
+ [
93
+ process(left),
94
+ process(right)
95
+ ]
96
+ )
97
+ end
98
+ end
99
+
100
+ private
101
+
102
+ def truthy(node)
103
+ return false unless node.is_a?(::Parser::AST::Node)
104
+ return true if node.type == :true
105
+ return false if node.children.empty?
106
+
107
+ node.children.all? { |child| truthy(child) }
108
+ end
109
+ end
110
+
111
+ class Base
112
+ attr_reader :store, :predicates_by_path, :count, :terminated, :current_path
113
+ alias terminated? terminated
114
+
115
+ def initialize
116
+ # total number of predicates
117
+ @count = 0
118
+ # cache of all predicates by path
119
+ @predicates_by_path = {}
120
+ # all predicates and their dirty state
121
+ @store = {}
122
+
123
+ @current_path = []
124
+ end
125
+
126
+ def reset!
127
+ @current_path = []
128
+ @terminated = false
129
+ end
130
+
131
+ def push(path)
132
+ current_path << path
133
+ end
134
+
135
+ def pop
136
+ current_path.pop
137
+ end
138
+
139
+ def terminate!
140
+ @terminated = true
141
+ end
142
+
143
+ def predicate_clause(name, node)
144
+ if pred?(name)
145
+ read_pred(name)
146
+ else
147
+ write_pred(name, node)
148
+ end
149
+ end
150
+
151
+ def pred?(name)
152
+ predicates_by_path.key?(current_path + [name])
153
+ end
154
+
155
+ def read_pred(name)
156
+ lvar = predicates_by_path.fetch(current_path + [name])
157
+ # mark as used
158
+ store[lvar] = true
159
+ s(:lvar, lvar)
160
+ end
161
+
162
+ def write_pred(name, node)
163
+ return node if terminated?
164
+ @count += 1
165
+ lvar = :"__p_#{count}__"
166
+ predicates_by_path[current_path + [name]] = lvar
167
+ store[lvar] = false
168
+
169
+ s(:lvasgn,
170
+ lvar,
171
+ node)
172
+ end
173
+
174
+ def process(ast)
175
+ Processor.new(self).process(ast)
176
+ end
177
+
178
+ private
179
+
180
+ def s(type, *children)
181
+ ::Parser::AST::Node.new(type, children)
182
+ end
183
+ end
184
+
185
+ # rubocop:disable Style/MissingRespondToMissing
186
+ class Noop < Base
187
+ # Return node itself, no memoization
188
+ def method_missing(mid, node, *__rest__)
189
+ node
190
+ end
191
+ end
192
+ # rubocop:enable Style/MethodMissingSuper
193
+ # rubocop:enable Style/MissingRespondToMissing
194
+
195
+ class CaseIn < Base
196
+ def const(node, const)
197
+ node
198
+ end
199
+
200
+ def respond_to_deconstruct(node)
201
+ predicate_clause(:respond_to_deconstruct, node)
202
+ end
203
+
204
+ def array_size(node, size)
205
+ predicate_clause(:"array_size_#{size}", node)
206
+ end
207
+
208
+ def array_deconstructed(node)
209
+ predicate_clause(:array_deconstructed, node)
210
+ end
211
+
212
+ def hash_deconstructed(node, keys)
213
+ predicate_clause(:"hash_deconstructed_#{keys.join("_p_")}", node)
214
+ end
215
+
216
+ def respond_to_deconstruct_keys(node)
217
+ predicate_clause(:respond_to_deconstruct_keys, node)
218
+ end
219
+
220
+ def hash_keys(node, keys)
221
+ keys = keys.map { |key| key.is_a?(::Parser::AST::Node) ? key.children.first : key }
222
+
223
+ predicate_clause(:"hash_keys_#{keys.join("_p_")}", node)
224
+ end
225
+ end
226
+ end
227
+
228
+ class PatternMatching < Base
229
+ NAME = "pattern-matching"
230
+ SYNTAX_PROBE = "case 0; in 0; true; else; 1; end"
231
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
232
+
233
+ MATCHEE = :__m__
234
+ MATCHEE_ARR = :__m_arr__
235
+ MATCHEE_HASH = :__m_hash__
236
+
237
+ ALTERNATION_MARKER = :__alt__
238
+ CURRENT_HASH_KEY = :__chk__
239
+
240
+ def on_case_match(node)
241
+ context.track! self
242
+
243
+ @deconstructed_keys = {}
244
+ @predicates = Predicates::CaseIn.new
245
+ @lvars = []
246
+
247
+ matchee_ast =
248
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
249
+
250
+ patterns = locals.with(
251
+ matchee: MATCHEE,
252
+ arr: MATCHEE_ARR,
253
+ hash: MATCHEE_HASH
254
+ ) do
255
+ build_case_when(node.children[1..-1])
256
+ end
257
+
258
+ case_clause = predicates.process(s(:case, *patterns))
259
+
260
+ rewrite_case_in! node, matchee_ast, case_clause
261
+
262
+ node.updated(
263
+ :kwbegin,
264
+ [
265
+ matchee_ast, case_clause
266
+ ]
267
+ )
268
+ end
269
+
270
+ def on_match_pattern(node)
271
+ context.track! self
272
+
273
+ @deconstructed_keys = {}
274
+ @predicates = Predicates::Noop.new
275
+ @lvars = []
276
+
277
+ matchee =
278
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
279
+
280
+ pattern =
281
+ locals.with(
282
+ matchee: MATCHEE,
283
+ arr: MATCHEE_ARR,
284
+ hash: MATCHEE_HASH
285
+ ) do
286
+ with_declared_locals do
287
+ send(
288
+ :"#{node.children[1].type}_clause",
289
+ node.children[1]
290
+ )
291
+ end.then do |node|
292
+ s(:begin,
293
+ s(:or,
294
+ node,
295
+ no_matching_pattern))
296
+ end
297
+ end
298
+
299
+ node.updated(
300
+ :and,
301
+ [
302
+ matchee,
303
+ pattern
304
+ ]
305
+ ).tap do |new_node|
306
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
307
+ end
308
+ end
309
+
310
+ alias on_in_match on_match_pattern
311
+
312
+ def on_match_pattern_p(node)
313
+ context.track! self
314
+
315
+ @deconstructed_keys = {}
316
+ @predicates = Predicates::Noop.new
317
+ @lvars = []
318
+
319
+ matchee =
320
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
321
+
322
+ pattern =
323
+ locals.with(
324
+ matchee: MATCHEE,
325
+ arr: MATCHEE_ARR,
326
+ hash: MATCHEE_HASH
327
+ ) do
328
+ with_declared_locals do
329
+ send(
330
+ :"#{node.children[1].type}_clause",
331
+ node.children[1]
332
+ )
333
+ end
334
+ end
335
+
336
+ node.updated(
337
+ :and,
338
+ [
339
+ matchee,
340
+ pattern
341
+ ]
342
+ ).tap do |new_node|
343
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
344
+ end
345
+ end
346
+
347
+ private
348
+
349
+ def rewrite_case_in!(node, matchee, new_node)
350
+ replace(node.loc.keyword, "case; when (#{unparse(matchee)}) && false")
351
+ remove(node.children[0].loc.expression)
352
+
353
+ node.children[1..-1].each.with_index do |clause, i|
354
+ if clause&.type == :in_pattern
355
+ # handle multiline clauses differently
356
+ if clause.loc.last_line > clause.children[0].loc.last_line + 1
357
+ height = clause.loc.last_line - clause.children[0].loc.last_line
358
+ padding = "\n" * height
359
+ body_indent = " " * clause.children[2].loc.column
360
+ replace(
361
+ clause.loc.expression,
362
+ "when #{inline_blocks(unparse(new_node.children[i].children[0]))}" \
363
+ "#{padding}" \
364
+ "#{body_indent}#{clause.children[2].loc.expression.source}"
365
+ )
366
+ else
367
+ replace(
368
+ clause.loc.keyword.end.join(clause.children[0].loc.expression.end),
369
+ inline_blocks(unparse(new_node.children[i].children[0]))
370
+ )
371
+ remove(clause.children[1].loc.expression) if clause.children[1]
372
+ replace(clause.loc.keyword, "when ")
373
+ end
374
+ elsif clause.nil?
375
+ insert_after(node.children[-2].loc.expression, "; else; #{unparse(new_node.children.last)}")
376
+ end
377
+ end
378
+ end
379
+
380
+ def build_case_when(nodes)
381
+ else_clause = nil
382
+ clauses = []
383
+
384
+ nodes.each do |clause|
385
+ if clause&.type == :in_pattern
386
+ clauses << build_when_clause(clause)
387
+ else
388
+ else_clause = process(clause)
389
+ end
390
+ end
391
+
392
+ else_clause = (else_clause || no_matching_pattern).then do |node|
393
+ next node unless node.type == :empty_else
394
+ nil
395
+ end
396
+
397
+ clauses << else_clause
398
+ clauses
399
+ end
400
+
401
+ def build_when_clause(clause)
402
+ predicates.reset!
403
+ [
404
+ with_declared_locals do
405
+ with_guard(
406
+ send(
407
+ :"#{clause.children[0].type}_clause",
408
+ clause.children[0]
409
+ ),
410
+ clause.children[1] # guard
411
+ )
412
+ end,
413
+ process(clause.children[2] || s(:nil)) # expression
414
+ ].then do |children|
415
+ s(:when, *children)
416
+ end
417
+ end
418
+
419
+ def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
420
+ const, pattern = *node.children
421
+
422
+ predicates.const(case_eq_clause(const, right), const).then do |node|
423
+ next node if pattern.nil?
424
+
425
+ s(:begin,
426
+ s(:and,
427
+ node,
428
+ send(:"#{pattern.type}_clause", pattern, right)))
429
+ end
430
+ end
431
+
432
+ def match_alt_clause(node, matchee = s(:lvar, locals[:matchee]))
433
+ children = locals.with(ALTERNATION_MARKER => true) do
434
+ node.children.map.with_index do |child, i|
435
+ predicates.terminate! if i == 1
436
+ send :"#{child.type}_clause", child, matchee
437
+ end
438
+ end
439
+ s(:begin, s(:or, *children))
440
+ end
441
+
442
+ def match_as_clause(node, right = s(:lvar, locals[:matchee]))
443
+ s(:begin,
444
+ s(:and,
445
+ send(:"#{node.children[0].type}_clause", node.children[0], right),
446
+ match_var_clause(node.children[1], right)))
447
+ end
448
+
449
+ def match_var_clause(node, left = s(:lvar, locals[:matchee]))
450
+ var = node.children[0]
451
+ return s(:true) if var == :_
452
+
453
+ check_match_var_alternation!(var)
454
+
455
+ s(:begin,
456
+ s(:or,
457
+ s(:begin, build_var_assignment(var, left)),
458
+ s(:true)))
459
+ end
460
+
461
+ def pin_clause(node, right = s(:lvar, locals[:matchee]))
462
+ predicates.terminate!
463
+ case_eq_clause node.children[0], right
464
+ end
465
+
466
+ def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
467
+ predicates.terminate!
468
+ s(:begin, s(:send,
469
+ process(node), :===, right))
470
+ end
471
+
472
+ #=========== ARRAY PATTERN (START) ===============
473
+
474
+ def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
475
+ deconstruct_node(matchee).then do |dnode|
476
+ size_check = nil
477
+ # if there is no rest or tail, match the size first
478
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
479
+ size_check = predicates.array_size(
480
+ s(:begin,
481
+ s(:send,
482
+ node.children.size.to_ast_node,
483
+ :==,
484
+ s(:send, s(:lvar, locals[:arr]), :size))),
485
+ node.children.size
486
+ )
487
+ end
488
+
489
+ right =
490
+ if node.children.empty?
491
+ case_eq_clause(s(:array), s(:lvar, locals[:arr]))
492
+ elsif node.children.size > 1 && node.children.first.type == :match_rest && node.children.last.type == :match_rest
493
+ array_find(*node.children)
494
+ else
495
+ array_element(0, *node.children)
496
+ end
497
+
498
+ right = s(:and, size_check, right) if size_check
499
+
500
+ s(:begin,
501
+ s(:and,
502
+ dnode,
503
+ right))
504
+ end
505
+ end
506
+
507
+ alias array_pattern_with_tail_clause array_pattern_clause
508
+ alias find_pattern_clause array_pattern_clause
509
+
510
+ def deconstruct_node(matchee)
511
+ context.use_ruby_next!
512
+
513
+ # we do not memoize respond_to_check for arrays, 'cause
514
+ # we can memoize is together with #deconstruct result
515
+ respond_check = respond_to_check(matchee, :deconstruct)
516
+ right = s(:send, matchee, :deconstruct)
517
+
518
+ predicates.array_deconstructed(
519
+ s(:and,
520
+ respond_check,
521
+ s(:begin,
522
+ s(:and,
523
+ s(:begin,
524
+ s(:or,
525
+ s(:begin, s(:lvasgn, locals[:arr], right)),
526
+ s(:true))),
527
+ s(:begin,
528
+ s(:or,
529
+ s(:send,
530
+ s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
531
+ raise_error(:TypeError, "#deconstruct must return Array"))))))
532
+ )
533
+ end
534
+
535
+ def array_element(index, head, *tail)
536
+ return array_match_rest(index, head, *tail) if head.type == :match_rest
537
+
538
+ send("#{head.type}_array_element", head, index).then do |node|
539
+ next node if tail.empty?
540
+
541
+ s(:begin,
542
+ s(:and,
543
+ node,
544
+ array_element(index + 1, *tail)))
545
+ end
546
+ end
547
+
548
+ # [*a, 1, 2, *] -> arr.find.with_index { |_, i| (a = arr.take(i)) && arr[i] == 1 && arr[i + 1] == 2 }
549
+ def array_find(head, *nodes, tail)
550
+ index = s(:lvar, :__i__)
551
+
552
+ head_match =
553
+ unless head.children.empty?
554
+ # we only need to call this to track the lvar usage
555
+ build_var_assignment(head.children[0].children[0])
556
+
557
+ arr_take = s(:send,
558
+ s(:lvar, locals[:arr]),
559
+ :take,
560
+ index)
561
+
562
+ match_var_clause(head.children[0], arr_take)
563
+ end
564
+
565
+ tail_match =
566
+ unless tail.children.empty?
567
+ # we only need to call this to track the lvar usage
568
+ build_var_assignment(tail.children[0].children[0])
569
+
570
+ match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1))
571
+ end
572
+
573
+ nodes.each do |node|
574
+ if node.type == :match_var
575
+ # we only need to call this to track the lvar usage
576
+ build_var_assignment(node.children[0])
577
+ elsif node.type == :match_as
578
+ # we only need to call this to track the lvar usage
579
+ build_var_assignment(node.children[1].children[0])
580
+ end
581
+ end
582
+
583
+ pattern = array_rest_element(*nodes, index).then do |needle|
584
+ next needle unless head_match
585
+ s(:begin,
586
+ s(:and,
587
+ needle,
588
+ head_match))
589
+ end.then do |headed_needle|
590
+ next headed_needle unless tail_match
591
+
592
+ s(:begin,
593
+ s(:and,
594
+ headed_needle,
595
+ tail_match))
596
+ end
597
+
598
+ s(:block,
599
+ s(:send,
600
+ s(:send,
601
+ s(:lvar, locals[:arr]),
602
+ :find),
603
+ :with_index),
604
+ s(:args,
605
+ s(:arg, :_),
606
+ s(:arg, :__i__)),
607
+ pattern)
608
+ end
609
+
610
+ def array_match_rest(index, node, *tail)
611
+ size = tail.size + 1
612
+ child = node.children[0]
613
+
614
+ rest = arr_slice(index, -size).then do |r|
615
+ next r unless child
616
+
617
+ match_var_clause(
618
+ child,
619
+ r
620
+ )
621
+ end
622
+
623
+ return rest if tail.empty?
624
+
625
+ s(:begin,
626
+ s(:and,
627
+ rest,
628
+ array_rest_element(*tail, -(size - 1))))
629
+ end
630
+
631
+ def array_rest_element(head, *tail, index)
632
+ send("#{head.type}_array_element", head, index).then do |node|
633
+ next node if tail.empty?
634
+
635
+ s(:begin,
636
+ s(:and,
637
+ node,
638
+ array_rest_element(*tail, index + 1)))
639
+ end
640
+ end
641
+
642
+ def array_pattern_array_element(node, index)
643
+ element = arr_item_at(index)
644
+ locals.with(arr: locals[:arr, index]) do
645
+ predicates.push :"i#{index}"
646
+ array_pattern_clause(node, element).tap { predicates.pop }
647
+ end
648
+ end
649
+
650
+ def find_pattern_array_element(node, index)
651
+ element = arr_item_at(index)
652
+ locals.with(arr: locals[:arr, index]) do
653
+ predicates.push :"i#{index}"
654
+ find_pattern_clause(node, element).tap { predicates.pop }
655
+ end
656
+ end
657
+
658
+ def hash_pattern_array_element(node, index)
659
+ element = arr_item_at(index)
660
+ locals.with(hash: locals[:arr, index]) do
661
+ predicates.push :"i#{index}"
662
+ hash_pattern_clause(node, element).tap { predicates.pop }
663
+ end
664
+ end
665
+
666
+ def const_pattern_array_element(node, index)
667
+ element = arr_item_at(index)
668
+ locals.with(arr: locals[:arr, index]) do
669
+ predicates.push :"i#{index}"
670
+ const_pattern_clause(node, element).tap { predicates.pop }
671
+ end
672
+ end
673
+
674
+ def match_alt_array_element(node, index)
675
+ children = node.children.map do |child, i|
676
+ send :"#{child.type}_array_element", child, index
677
+ end
678
+ s(:begin, s(:or, *children))
679
+ end
680
+
681
+ def match_var_array_element(node, index)
682
+ element = arr_item_at(index)
683
+ locals.with(arr: locals[:arr, index]) do
684
+ predicates.push :"i#{index}"
685
+ match_var_clause(node, element).tap { predicates.pop }
686
+ end
687
+ end
688
+
689
+ def match_as_array_element(node, index)
690
+ element = arr_item_at(index)
691
+ locals.with(arr: locals[:arr, index]) do
692
+ predicates.push :"i#{index}"
693
+ match_as_clause(node, element).tap { predicates.pop }
694
+ end
695
+ end
696
+
697
+ def pin_array_element(node, index)
698
+ case_eq_array_element node.children[0], index
699
+ end
700
+
701
+ def case_eq_array_element(node, index)
702
+ case_eq_clause(node, arr_item_at(index))
703
+ end
704
+
705
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
706
+ s(:index, arr, index.to_ast_node)
707
+ end
708
+
709
+ def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
710
+ s(:index,
711
+ arr,
712
+ s(:irange,
713
+ lindex.to_ast_node,
714
+ rindex.to_ast_node))
715
+ end
716
+
717
+ #=========== ARRAY PATTERN (END) ===============
718
+
719
+ #=========== HASH PATTERN (START) ===============
720
+
721
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
722
+ # Optimization: avoid hash modifications when not needed
723
+ # (we use #dup and #delete when "reading" values when **rest is present
724
+ # to assign the rest of the hash copy to it)
725
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
726
+ keys = hash_pattern_destruction_keys(node.children)
727
+
728
+ specified_key_names = hash_pattern_keys(node.children)
729
+
730
+ deconstruct_keys_node(keys, matchee).then do |dnode|
731
+ right =
732
+ if node.children.empty?
733
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
734
+ elsif specified_key_names.empty?
735
+ hash_element(*node.children)
736
+ else
737
+ s(:begin,
738
+ s(:and,
739
+ having_hash_keys(specified_key_names),
740
+ hash_element(*node.children)))
741
+ end
742
+
743
+ predicates.pop
744
+
745
+ next dnode if right.nil?
746
+
747
+ s(:begin,
748
+ s(:and,
749
+ dnode,
750
+ right))
751
+ end
752
+ end
753
+
754
+ def hash_pattern_keys(children)
755
+ children.filter_map do |child|
756
+ # Skip ** without var
757
+ next if child.type == :match_rest || child.type == :match_nil_pattern
758
+
759
+ send("#{child.type}_hash_key", child)
760
+ end
761
+ end
762
+
763
+ def hash_pattern_destruction_keys(children)
764
+ return s(:nil) if children.empty?
765
+
766
+ children.filter_map do |child|
767
+ # Skip ** without var
768
+ next if child.type == :match_rest && child.children.empty?
769
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
770
+
771
+ send("#{child.type}_hash_key", child)
772
+ end.then { |keys| s(:array, *keys) }
773
+ end
774
+
775
+ def pair_hash_key(node)
776
+ node.children[0]
777
+ end
778
+
779
+ def match_var_hash_key(node)
780
+ check_match_var_alternation! node.children[0]
781
+
782
+ s(:sym, node.children[0])
783
+ end
784
+
785
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
786
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
787
+ # 'cause it remains immutable
788
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
789
+
790
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
791
+ hash_dup =
792
+ if @hash_match_rest
793
+ s(:begin, s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup)))
794
+ else
795
+ s(:true)
796
+ end
797
+
798
+ context.use_ruby_next!
799
+
800
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
801
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
802
+
803
+ key_names = keys.children.map { |node| node.children.last }
804
+ predicates.push locals[:hash]
805
+
806
+ s(:begin, s(:lvasgn, deconstruct_name,
807
+ s(:send,
808
+ matchee, :deconstruct_keys, keys))).then do |dnode|
809
+ next dnode if respond_to_checked
810
+
811
+ s(:and,
812
+ respond_check,
813
+ s(:begin,
814
+ s(:and,
815
+ s(:begin,
816
+ s(:or,
817
+ dnode,
818
+ s(:true))),
819
+ s(:begin,
820
+ s(:or,
821
+ s(:send,
822
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
823
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))))
824
+ end.then do |dnode|
825
+ predicates.hash_deconstructed(dnode, key_names)
826
+ end.then do |dnode|
827
+ next dnode unless @hash_match_rest
828
+
829
+ s(:begin,
830
+ s(:and,
831
+ dnode,
832
+ hash_dup))
833
+ end
834
+ end
835
+
836
+ def hash_pattern_hash_element(node, key)
837
+ element = hash_value_at(key)
838
+ key_index = deconstructed_key(key)
839
+ locals.with(hash: locals[:hash, key_index]) do
840
+ predicates.push :"k#{key_index}"
841
+ hash_pattern_clause(node, element).tap { predicates.pop }
842
+ end
843
+ end
844
+
845
+ def array_pattern_hash_element(node, key)
846
+ element = hash_value_at(key)
847
+ key_index = deconstructed_key(key)
848
+ locals.with(arr: locals[:hash, key_index]) do
849
+ predicates.push :"k#{key_index}"
850
+ array_pattern_clause(node, element).tap { predicates.pop }
851
+ end
852
+ end
853
+
854
+ def find_pattern_hash_element(node, key)
855
+ element = hash_value_at(key)
856
+ key_index = deconstructed_key(key)
857
+ locals.with(arr: locals[:hash, key_index]) do
858
+ predicates.push :"k#{key_index}"
859
+ find_pattern_clause(node, element).tap { predicates.pop }
860
+ end
861
+ end
862
+
863
+ def const_pattern_hash_element(node, key)
864
+ element = hash_value_at(key)
865
+ key_index = deconstructed_key(key)
866
+ locals.with(hash: locals[:hash, key_index]) do
867
+ predicates.push :"k#{key_index}"
868
+ const_pattern_clause(node, element).tap { predicates.pop }
869
+ end
870
+ end
871
+
872
+ def hash_element(head, *tail)
873
+ send("#{head.type}_hash_element", head).then do |node|
874
+ next node if tail.empty?
875
+
876
+ right = hash_element(*tail)
877
+
878
+ next node if right.nil?
879
+
880
+ s(:begin,
881
+ s(:and,
882
+ node,
883
+ right))
884
+ end
885
+ end
886
+
887
+ def pair_hash_element(node, _key = nil)
888
+ key, val = *node.children
889
+ send("#{val.type}_hash_element", val, key)
890
+ end
891
+
892
+ def match_alt_hash_element(node, key)
893
+ element_node = s(:begin, s(:lvasgn, locals[:hash, :el], hash_value_at(key)))
894
+
895
+ children = locals.with(hash_element: locals[:hash, :el]) do
896
+ node.children.map do |child, i|
897
+ send :"#{child.type}_hash_element", child, key
898
+ end
899
+ end
900
+
901
+ s(:begin,
902
+ s(:and,
903
+ s(:begin,
904
+ s(:or,
905
+ element_node,
906
+ s(:true))),
907
+ s(:begin,
908
+ s(:or, *children))))
909
+ end
910
+
911
+ def match_as_hash_element(node, key)
912
+ element = hash_value_at(key)
913
+ key_index = deconstructed_key(key)
914
+ locals.with(hash: locals[:hash, key_index]) do
915
+ predicates.push :"k#{key_index}"
916
+ match_as_clause(node, element).tap { predicates.pop }
917
+ end
918
+ end
919
+
920
+ def match_var_hash_element(node, key = nil)
921
+ key ||= node.children[0]
922
+ element = hash_value_at(key)
923
+ key_index = deconstructed_key(key)
924
+ locals.with(hash: locals[:hash, key_index]) do
925
+ predicates.push :"k#{key_index}"
926
+ match_var_clause(node, element).tap { predicates.pop }
927
+ end
928
+ end
929
+
930
+ def match_nil_pattern_hash_element(node, _key = nil)
931
+ s(:send,
932
+ s(:lvar, locals[:hash]),
933
+ :empty?)
934
+ end
935
+
936
+ def match_rest_hash_element(node, _key = nil)
937
+ # case {}; in **; end
938
+ return if node.children.empty?
939
+
940
+ child = node.children[0]
941
+
942
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
943
+
944
+ match_var_clause(child, s(:lvar, locals[:hash]))
945
+ end
946
+
947
+ def pin_hash_element(node, index)
948
+ case_eq_hash_element node.children[0], index
949
+ end
950
+
951
+ def case_eq_hash_element(node, key)
952
+ case_eq_clause node, hash_value_at(key)
953
+ end
954
+
955
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
956
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
957
+
958
+ if @hash_match_rest
959
+ s(:send,
960
+ hash, :delete,
961
+ key.to_ast_node)
962
+ else
963
+ s(:index,
964
+ hash,
965
+ key.to_ast_node)
966
+ end
967
+ end
968
+
969
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
970
+ s(:send,
971
+ hash, :key?,
972
+ key.to_ast_node)
973
+ end
974
+
975
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
976
+ keys.reduce(nil) do |acc, key|
977
+ pnode = hash_has_key(key, hash)
978
+ next pnode unless acc
979
+
980
+ s(:begin,
981
+ s(:and, acc, pnode))
982
+ end.then do |node|
983
+ predicates.hash_keys(node, keys)
984
+ end
985
+ end
986
+
987
+ #=========== HASH PATTERN (END) ===============
988
+
989
+ def with_guard(node, guard)
990
+ return node unless guard
991
+
992
+ s(:begin,
993
+ s(:and,
994
+ node,
995
+ guard.children[0])).then do |expr|
996
+ next expr unless guard.type == :unless_guard
997
+ s(:send, expr, :!)
998
+ end
999
+ end
1000
+
1001
+ def with_declared_locals
1002
+ lvars.clear
1003
+ node = yield
1004
+
1005
+ return node if lvars.empty?
1006
+
1007
+ # We need to declare match lvars outside of the outer `find` block,
1008
+ # so we do that for that whole pattern
1009
+ locals_declare = s(:begin, s(:masgn,
1010
+ s(:mlhs, *lvars.uniq.map { s(:lvasgn, _1) }),
1011
+ s(:nil)))
1012
+
1013
+ s(:begin,
1014
+ s(:or,
1015
+ locals_declare,
1016
+ node))
1017
+ end
1018
+
1019
+ def no_matching_pattern
1020
+ raise_error(
1021
+ :NoMatchingPatternError,
1022
+ s(:send,
1023
+ s(:lvar, locals[:matchee]), :inspect)
1024
+ )
1025
+ end
1026
+
1027
+ def raise_error(type, msg = "")
1028
+ s(:send, s(:const, nil, :Kernel), :raise,
1029
+ s(:const, nil, type),
1030
+ msg.to_ast_node)
1031
+ end
1032
+
1033
+ # Add respond_to? check
1034
+ def respond_to_check(node, mid)
1035
+ s(:send, node, :respond_to?, mid.to_ast_node)
1036
+ end
1037
+
1038
+ def respond_to_missing?(mid, *__rest__)
1039
+ return true if mid.to_s.match?(/_(clause|array_element)/)
1040
+ super
1041
+ end
1042
+
1043
+ def method_missing(mid, *args, &block)
1044
+ mid = mid.to_s
1045
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
1046
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
1047
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
1048
+ super
1049
+ end
1050
+
1051
+ private
1052
+
1053
+ attr_reader :deconstructed_keys, :predicates, :lvars
1054
+
1055
+ # Raise SyntaxError if match-var is used within alternation
1056
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
1057
+ def check_match_var_alternation!(name)
1058
+ return unless locals.key?(ALTERNATION_MARKER)
1059
+
1060
+ if name.is_a?(::Parser::AST::Node)
1061
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name.children.first})"
1062
+ end
1063
+
1064
+ return if name.start_with?("_")
1065
+
1066
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
1067
+ end
1068
+
1069
+ def deconstructed_key(key)
1070
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
1071
+
1072
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
1073
+ end
1074
+
1075
+ # Unparser generates `do .. end` or `{ ... }` multiline blocks, we want to
1076
+ # have single-line blocks with `{ ... }`.
1077
+ def inline_blocks(source)
1078
+ source.gsub(/(?:do|{) \|_, __i__\|\n\s*([^\n]+)\n\s*(?:end|})/, '{ |_, __i__| \1 }')
1079
+ end
1080
+
1081
+ # Value could be omitted for mass assignment
1082
+ def build_var_assignment(var, value = nil)
1083
+ unless var.is_a?(::Parser::AST::Node)
1084
+ lvars << var
1085
+ return s(:lvasgn, *[var, value].compact)
1086
+ end
1087
+
1088
+ asign_type = :"#{var.type.to_s[0]}vasgn"
1089
+
1090
+ s(asign_type, *[var.children[0], value].compact)
1091
+ end
1092
+ end
1093
+ end
1094
+ end
1095
+ end