ruby-next-core 0.14.0 → 0.14.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1061 @@
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/MethodMissingSuper
186
+ # rubocop:disable Style/MissingRespondToMissing
187
+ class Noop < Base
188
+ # Return node itself, no memoization
189
+ def method_missing(mid, node, *)
190
+ node
191
+ end
192
+ end
193
+ # rubocop:enable Style/MethodMissingSuper
194
+ # rubocop:enable Style/MissingRespondToMissing
195
+
196
+ class CaseIn < Base
197
+ def const(node, const)
198
+ node
199
+ end
200
+
201
+ def respond_to_deconstruct(node)
202
+ predicate_clause(:respond_to_deconstruct, node)
203
+ end
204
+
205
+ def array_size(node, size)
206
+ predicate_clause(:"array_size_#{size}", node)
207
+ end
208
+
209
+ def array_deconstructed(node)
210
+ predicate_clause(:array_deconstructed, node)
211
+ end
212
+
213
+ def hash_deconstructed(node, keys)
214
+ predicate_clause(:"hash_deconstructed_#{keys.join("_p_")}", node)
215
+ end
216
+
217
+ def respond_to_deconstruct_keys(node)
218
+ predicate_clause(:respond_to_deconstruct_keys, node)
219
+ end
220
+
221
+ def hash_keys(node, keys)
222
+ keys = keys.map { |key| key.is_a?(::Parser::AST::Node) ? key.children.first : key }
223
+
224
+ predicate_clause(:"hash_keys_#{keys.join("_p_")}", node)
225
+ end
226
+ end
227
+ end
228
+
229
+ class PatternMatching < Base
230
+ NAME = "pattern-matching"
231
+ SYNTAX_PROBE = "case 0; in 0; true; else; 1; end"
232
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
233
+
234
+ MATCHEE = :__m__
235
+ MATCHEE_ARR = :__m_arr__
236
+ MATCHEE_HASH = :__m_hash__
237
+
238
+ ALTERNATION_MARKER = :__alt__
239
+ CURRENT_HASH_KEY = :__chk__
240
+
241
+ def on_case_match(node)
242
+ context.track! self
243
+
244
+ @deconstructed_keys = {}
245
+ @predicates = Predicates::CaseIn.new
246
+ @lvars = []
247
+
248
+ matchee_ast =
249
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
250
+
251
+ patterns = locals.with(
252
+ matchee: MATCHEE,
253
+ arr: MATCHEE_ARR,
254
+ hash: MATCHEE_HASH
255
+ ) do
256
+ build_case_when(node.children[1..-1])
257
+ end
258
+
259
+ case_clause = predicates.process(s(:case, *patterns))
260
+
261
+ rewrite_case_in! node, matchee_ast, case_clause
262
+
263
+ node.updated(
264
+ :kwbegin,
265
+ [
266
+ matchee_ast, case_clause
267
+ ]
268
+ )
269
+ end
270
+
271
+ def on_match_pattern(node)
272
+ context.track! self
273
+
274
+ @deconstructed_keys = {}
275
+ @predicates = Predicates::Noop.new
276
+ @lvars = []
277
+
278
+ matchee =
279
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
280
+
281
+ pattern =
282
+ locals.with(
283
+ matchee: MATCHEE,
284
+ arr: MATCHEE_ARR,
285
+ hash: MATCHEE_HASH
286
+ ) do
287
+ with_declared_locals do
288
+ send(
289
+ :"#{node.children[1].type}_clause",
290
+ node.children[1]
291
+ )
292
+ end.then do |node|
293
+ s(:begin,
294
+ s(:or,
295
+ node,
296
+ no_matching_pattern))
297
+ end
298
+ end
299
+
300
+ node.updated(
301
+ :and,
302
+ [
303
+ matchee,
304
+ pattern
305
+ ]
306
+ ).tap do |new_node|
307
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
308
+ end
309
+ end
310
+
311
+ alias on_in_match on_match_pattern
312
+
313
+ def on_match_pattern_p(node)
314
+ context.track! self
315
+
316
+ @deconstructed_keys = {}
317
+ @predicates = Predicates::Noop.new
318
+ @lvars = []
319
+
320
+ matchee =
321
+ s(:begin, s(:lvasgn, MATCHEE, node.children[0]))
322
+
323
+ pattern =
324
+ locals.with(
325
+ matchee: MATCHEE,
326
+ arr: MATCHEE_ARR,
327
+ hash: MATCHEE_HASH
328
+ ) do
329
+ with_declared_locals do
330
+ send(
331
+ :"#{node.children[1].type}_clause",
332
+ node.children[1]
333
+ )
334
+ end
335
+ end
336
+
337
+ node.updated(
338
+ :and,
339
+ [
340
+ matchee,
341
+ pattern
342
+ ]
343
+ ).tap do |new_node|
344
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
345
+ end
346
+ end
347
+
348
+ private
349
+
350
+ def rewrite_case_in!(node, matchee, new_node)
351
+ replace(node.loc.keyword, "case; when (#{unparse(matchee)}) && false")
352
+ remove(node.children[0].loc.expression)
353
+
354
+ node.children[1..-1].each.with_index do |clause, i|
355
+ if clause&.type == :in_pattern
356
+ # handle multiline clauses differently
357
+ if clause.loc.last_line > clause.children[0].loc.last_line + 1
358
+ height = clause.loc.last_line - clause.children[0].loc.last_line
359
+ padding = "\n" * height
360
+ body_indent = " " * clause.children[2].loc.column
361
+ replace(
362
+ clause.loc.expression,
363
+ "when #{inline_blocks(unparse(new_node.children[i].children[0]))}" \
364
+ "#{padding}" \
365
+ "#{body_indent}#{clause.children[2].loc.expression.source}"
366
+ )
367
+ else
368
+ replace(
369
+ clause.loc.keyword.end.join(clause.children[0].loc.expression.end),
370
+ inline_blocks(unparse(new_node.children[i].children[0]))
371
+ )
372
+ remove(clause.children[1].loc.expression) if clause.children[1]
373
+ replace(clause.loc.keyword, "when ")
374
+ end
375
+ elsif clause.nil?
376
+ insert_after(node.children[-2].loc.expression, "; else; #{unparse(new_node.children.last)}")
377
+ end
378
+ end
379
+ end
380
+
381
+ def build_case_when(nodes)
382
+ else_clause = nil
383
+ clauses = []
384
+
385
+ nodes.each do |clause|
386
+ if clause&.type == :in_pattern
387
+ clauses << build_when_clause(clause)
388
+ else
389
+ else_clause = process(clause)
390
+ end
391
+ end
392
+
393
+ else_clause = (else_clause || no_matching_pattern).then do |node|
394
+ next node unless node.type == :empty_else
395
+ nil
396
+ end
397
+
398
+ clauses << else_clause
399
+ clauses
400
+ end
401
+
402
+ def build_when_clause(clause)
403
+ predicates.reset!
404
+ [
405
+ with_declared_locals do
406
+ with_guard(
407
+ send(
408
+ :"#{clause.children[0].type}_clause",
409
+ clause.children[0]
410
+ ),
411
+ clause.children[1] # guard
412
+ )
413
+ end,
414
+ process(clause.children[2] || s(:nil)) # expression
415
+ ].then do |children|
416
+ s(:when, *children)
417
+ end
418
+ end
419
+
420
+ def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
421
+ const, pattern = *node.children
422
+
423
+ predicates.const(case_eq_clause(const, right), const).then do |node|
424
+ next node if pattern.nil?
425
+
426
+ s(:begin,
427
+ s(:and,
428
+ node,
429
+ send(:"#{pattern.type}_clause", pattern)))
430
+ end
431
+ end
432
+
433
+ def match_alt_clause(node)
434
+ children = locals.with(ALTERNATION_MARKER => true) do
435
+ node.children.map.with_index do |child, i|
436
+ predicates.terminate! if i == 1
437
+ send :"#{child.type}_clause", child
438
+ end
439
+ end
440
+ s(:begin, s(:or, *children))
441
+ end
442
+
443
+ def match_as_clause(node, right = s(:lvar, locals[:matchee]))
444
+ s(:begin,
445
+ s(:and,
446
+ send(:"#{node.children[0].type}_clause", node.children[0], right),
447
+ match_var_clause(node.children[1], right)))
448
+ end
449
+
450
+ def match_var_clause(node, left = s(:lvar, locals[:matchee]))
451
+ var = node.children[0]
452
+ return s(:true) if var == :_
453
+
454
+ check_match_var_alternation!(var)
455
+
456
+ s(:begin,
457
+ s(:or,
458
+ s(:begin, build_var_assignment(var, left)),
459
+ s(:true)))
460
+ end
461
+
462
+ def pin_clause(node, right = s(:lvar, locals[:matchee]))
463
+ predicates.terminate!
464
+ case_eq_clause node.children[0], right
465
+ end
466
+
467
+ def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
468
+ predicates.terminate!
469
+ s(:begin, s(:send,
470
+ process(node), :===, right))
471
+ end
472
+
473
+ #=========== ARRAY PATTERN (START) ===============
474
+
475
+ def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
476
+ deconstruct_node(matchee).then do |dnode|
477
+ size_check = nil
478
+ # if there is no rest or tail, match the size first
479
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
480
+ size_check = predicates.array_size(
481
+ s(:begin,
482
+ s(:send,
483
+ node.children.size.to_ast_node,
484
+ :==,
485
+ s(:send, s(:lvar, locals[:arr]), :size))),
486
+ node.children.size
487
+ )
488
+ end
489
+
490
+ right =
491
+ if node.children.empty?
492
+ case_eq_clause(s(:array), s(:lvar, locals[:arr]))
493
+ elsif node.children.size > 1 && node.children.first.type == :match_rest && node.children.last.type == :match_rest
494
+ array_find(*node.children)
495
+ else
496
+ array_element(0, *node.children)
497
+ end
498
+
499
+ right = s(:and, size_check, right) if size_check
500
+
501
+ s(:begin,
502
+ s(:and,
503
+ dnode,
504
+ right))
505
+ end
506
+ end
507
+
508
+ alias array_pattern_with_tail_clause array_pattern_clause
509
+ alias find_pattern_clause array_pattern_clause
510
+
511
+ def deconstruct_node(matchee)
512
+ context.use_ruby_next!
513
+
514
+ # we do not memoize respond_to_check for arrays, 'cause
515
+ # we can memoize is together with #deconstruct result
516
+ respond_check = respond_to_check(matchee, :deconstruct)
517
+ right = s(:send, matchee, :deconstruct)
518
+
519
+ predicates.array_deconstructed(
520
+ s(:and,
521
+ respond_check,
522
+ s(:begin,
523
+ s(:and,
524
+ s(:begin,
525
+ s(:or,
526
+ s(:begin, s(:lvasgn, locals[:arr], right)),
527
+ s(:true))),
528
+ s(:begin,
529
+ s(:or,
530
+ s(:send,
531
+ s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
532
+ raise_error(:TypeError, "#deconstruct must return Array"))))))
533
+ )
534
+ end
535
+
536
+ def array_element(index, head, *tail)
537
+ return array_match_rest(index, head, *tail) if head.type == :match_rest
538
+
539
+ send("#{head.type}_array_element", head, index).then do |node|
540
+ next node if tail.empty?
541
+
542
+ s(:begin,
543
+ s(:and,
544
+ node,
545
+ array_element(index + 1, *tail)))
546
+ end
547
+ end
548
+
549
+ # [*a, 1, 2, *] -> arr.find.with_index { |_, i| (a = arr.take(i)) && arr[i] == 1 && arr[i + 1] == 2 }
550
+ def array_find(head, *nodes, tail)
551
+ index = s(:lvar, :__i__)
552
+
553
+ head_match =
554
+ unless head.children.empty?
555
+ # we only need to call this to track the lvar usage
556
+ build_var_assignment(head.children[0].children[0])
557
+
558
+ arr_take = s(:send,
559
+ s(:lvar, locals[:arr]),
560
+ :take,
561
+ index)
562
+
563
+ match_var_clause(head.children[0], arr_take)
564
+ end
565
+
566
+ tail_match =
567
+ unless tail.children.empty?
568
+ # we only need to call this to track the lvar usage
569
+ build_var_assignment(tail.children[0].children[0])
570
+
571
+ match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1))
572
+ end
573
+
574
+ nodes.each do |node|
575
+ if node.type == :match_var
576
+ # we only need to call this to track the lvar usage
577
+ build_var_assignment(node.children[0])
578
+ elsif node.type == :match_as
579
+ # we only need to call this to track the lvar usage
580
+ build_var_assignment(node.children[1].children[0])
581
+ end
582
+ end
583
+
584
+ pattern = array_rest_element(*nodes, index).then do |needle|
585
+ next needle unless head_match
586
+ s(:begin,
587
+ s(:and,
588
+ needle,
589
+ head_match))
590
+ end.then do |headed_needle|
591
+ next headed_needle unless tail_match
592
+
593
+ s(:begin,
594
+ s(:and,
595
+ headed_needle,
596
+ tail_match))
597
+ end
598
+
599
+ s(:block,
600
+ s(:send,
601
+ s(:send,
602
+ s(:lvar, locals[:arr]),
603
+ :find),
604
+ :with_index),
605
+ s(:args,
606
+ s(:arg, :_),
607
+ s(:arg, :__i__)),
608
+ pattern)
609
+ end
610
+
611
+ def array_match_rest(index, node, *tail)
612
+ size = tail.size + 1
613
+ child = node.children[0]
614
+
615
+ rest = arr_slice(index, -size).then do |r|
616
+ next r unless child
617
+
618
+ match_var_clause(
619
+ child,
620
+ r
621
+ )
622
+ end
623
+
624
+ return rest if tail.empty?
625
+
626
+ s(:begin,
627
+ s(:and,
628
+ rest,
629
+ array_rest_element(*tail, -(size - 1))))
630
+ end
631
+
632
+ def array_rest_element(head, *tail, index)
633
+ send("#{head.type}_array_element", head, index).then do |node|
634
+ next node if tail.empty?
635
+
636
+ s(:begin,
637
+ s(:and,
638
+ node,
639
+ array_rest_element(*tail, index + 1)))
640
+ end
641
+ end
642
+
643
+ def array_pattern_array_element(node, index)
644
+ element = arr_item_at(index)
645
+ locals.with(arr: locals[:arr, index]) do
646
+ predicates.push :"i#{index}"
647
+ array_pattern_clause(node, element).tap { predicates.pop }
648
+ end
649
+ end
650
+
651
+ def find_pattern_array_element(node, index)
652
+ element = arr_item_at(index)
653
+ locals.with(arr: locals[:arr, index]) do
654
+ predicates.push :"i#{index}"
655
+ find_pattern_clause(node, element).tap { predicates.pop }
656
+ end
657
+ end
658
+
659
+ def hash_pattern_array_element(node, index)
660
+ element = arr_item_at(index)
661
+ locals.with(hash: locals[:arr, index]) do
662
+ predicates.push :"i#{index}"
663
+ hash_pattern_clause(node, element).tap { predicates.pop }
664
+ end
665
+ end
666
+
667
+ def match_alt_array_element(node, index)
668
+ children = node.children.map do |child, i|
669
+ send :"#{child.type}_array_element", child, index
670
+ end
671
+ s(:begin, s(:or, *children))
672
+ end
673
+
674
+ def match_var_array_element(node, index)
675
+ match_var_clause(node, arr_item_at(index))
676
+ end
677
+
678
+ def match_as_array_element(node, index)
679
+ match_as_clause(node, arr_item_at(index))
680
+ end
681
+
682
+ def pin_array_element(node, index)
683
+ case_eq_array_element node.children[0], index
684
+ end
685
+
686
+ def case_eq_array_element(node, index)
687
+ case_eq_clause(node, arr_item_at(index))
688
+ end
689
+
690
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
691
+ s(:index, arr, index.to_ast_node)
692
+ end
693
+
694
+ def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
695
+ s(:index,
696
+ arr,
697
+ s(:irange,
698
+ lindex.to_ast_node,
699
+ rindex.to_ast_node))
700
+ end
701
+
702
+ #=========== ARRAY PATTERN (END) ===============
703
+
704
+ #=========== HASH PATTERN (START) ===============
705
+
706
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
707
+ # Optimization: avoid hash modifications when not needed
708
+ # (we use #dup and #delete when "reading" values when **rest is present
709
+ # to assign the rest of the hash copy to it)
710
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
711
+ keys = hash_pattern_destruction_keys(node.children)
712
+
713
+ specified_key_names = hash_pattern_keys(node.children)
714
+
715
+ deconstruct_keys_node(keys, matchee).then do |dnode|
716
+ right =
717
+ if node.children.empty?
718
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
719
+ elsif specified_key_names.empty?
720
+ hash_element(*node.children)
721
+ else
722
+ s(:begin,
723
+ s(:and,
724
+ having_hash_keys(specified_key_names),
725
+ hash_element(*node.children)))
726
+ end
727
+
728
+ predicates.pop
729
+
730
+ next dnode if right.nil?
731
+
732
+ s(:begin,
733
+ s(:and,
734
+ dnode,
735
+ right))
736
+ end
737
+ end
738
+
739
+ def hash_pattern_keys(children)
740
+ children.filter_map do |child|
741
+ # Skip ** without var
742
+ next if child.type == :match_rest || child.type == :match_nil_pattern
743
+
744
+ send("#{child.type}_hash_key", child)
745
+ end
746
+ end
747
+
748
+ def hash_pattern_destruction_keys(children)
749
+ return s(:nil) if children.empty?
750
+
751
+ children.filter_map do |child|
752
+ # Skip ** without var
753
+ next if child.type == :match_rest && child.children.empty?
754
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
755
+
756
+ send("#{child.type}_hash_key", child)
757
+ end.then { |keys| s(:array, *keys) }
758
+ end
759
+
760
+ def pair_hash_key(node)
761
+ node.children[0]
762
+ end
763
+
764
+ def match_var_hash_key(node)
765
+ check_match_var_alternation! node.children[0]
766
+
767
+ s(:sym, node.children[0])
768
+ end
769
+
770
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
771
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
772
+ # 'cause it remains immutable
773
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
774
+
775
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
776
+ hash_dup =
777
+ if @hash_match_rest
778
+ s(:begin, s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup)))
779
+ else
780
+ s(:true)
781
+ end
782
+
783
+ context.use_ruby_next!
784
+
785
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
786
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
787
+
788
+ key_names = keys.children.map { |node| node.children.last }
789
+ predicates.push locals[:hash]
790
+
791
+ s(:begin, s(:lvasgn, deconstruct_name,
792
+ s(:send,
793
+ matchee, :deconstruct_keys, keys))).then do |dnode|
794
+ next dnode if respond_to_checked
795
+
796
+ s(:and,
797
+ respond_check,
798
+ s(:begin,
799
+ s(:and,
800
+ s(:begin,
801
+ s(:or,
802
+ dnode,
803
+ s(:true))),
804
+ s(:begin,
805
+ s(:or,
806
+ s(:send,
807
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
808
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))))
809
+ end.then do |dnode|
810
+ predicates.hash_deconstructed(dnode, key_names)
811
+ end.then do |dnode|
812
+ next dnode unless @hash_match_rest
813
+
814
+ s(:begin,
815
+ s(:and,
816
+ dnode,
817
+ hash_dup))
818
+ end
819
+ end
820
+
821
+ def hash_pattern_hash_element(node, key)
822
+ element = hash_value_at(key)
823
+ key_index = deconstructed_key(key)
824
+ locals.with(hash: locals[:hash, key_index]) do
825
+ predicates.push :"k#{key_index}"
826
+ hash_pattern_clause(node, element).tap { predicates.pop }
827
+ end
828
+ end
829
+
830
+ def array_pattern_hash_element(node, key)
831
+ element = hash_value_at(key)
832
+ key_index = deconstructed_key(key)
833
+ locals.with(arr: locals[:hash, key_index]) do
834
+ predicates.push :"k#{key_index}"
835
+ array_pattern_clause(node, element).tap { predicates.pop }
836
+ end
837
+ end
838
+
839
+ def find_pattern_hash_element(node, key)
840
+ element = hash_value_at(key)
841
+ key_index = deconstructed_key(key)
842
+ locals.with(arr: locals[:hash, key_index]) do
843
+ predicates.push :"k#{key_index}"
844
+ find_pattern_clause(node, element).tap { predicates.pop }
845
+ end
846
+ end
847
+
848
+ def hash_element(head, *tail)
849
+ send("#{head.type}_hash_element", head).then do |node|
850
+ next node if tail.empty?
851
+
852
+ right = hash_element(*tail)
853
+
854
+ next node if right.nil?
855
+
856
+ s(:begin,
857
+ s(:and,
858
+ node,
859
+ right))
860
+ end
861
+ end
862
+
863
+ def pair_hash_element(node, _key = nil)
864
+ key, val = *node.children
865
+ send("#{val.type}_hash_element", val, key)
866
+ end
867
+
868
+ def match_alt_hash_element(node, key)
869
+ element_node = s(:begin, s(:lvasgn, locals[:hash, :el], hash_value_at(key)))
870
+
871
+ children = locals.with(hash_element: locals[:hash, :el]) do
872
+ node.children.map do |child, i|
873
+ send :"#{child.type}_hash_element", child, key
874
+ end
875
+ end
876
+
877
+ s(:begin,
878
+ s(:and,
879
+ s(:begin,
880
+ s(:or,
881
+ element_node,
882
+ s(:true))),
883
+ s(:begin,
884
+ s(:or, *children))))
885
+ end
886
+
887
+ def match_as_hash_element(node, key)
888
+ match_as_clause(node, hash_value_at(key))
889
+ end
890
+
891
+ def match_var_hash_element(node, key = nil)
892
+ key ||= node.children[0]
893
+ match_var_clause(node, hash_value_at(key))
894
+ end
895
+
896
+ def match_nil_pattern_hash_element(node, _key = nil)
897
+ s(:send,
898
+ s(:lvar, locals[:hash]),
899
+ :empty?)
900
+ end
901
+
902
+ def match_rest_hash_element(node, _key = nil)
903
+ # case {}; in **; end
904
+ return if node.children.empty?
905
+
906
+ child = node.children[0]
907
+
908
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
909
+
910
+ match_var_clause(child, s(:lvar, locals[:hash]))
911
+ end
912
+
913
+ def pin_hash_element(node, index)
914
+ case_eq_hash_element node.children[0], index
915
+ end
916
+
917
+ def case_eq_hash_element(node, key)
918
+ case_eq_clause node, hash_value_at(key)
919
+ end
920
+
921
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
922
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
923
+
924
+ if @hash_match_rest
925
+ s(:send,
926
+ hash, :delete,
927
+ key.to_ast_node)
928
+ else
929
+ s(:index,
930
+ hash,
931
+ key.to_ast_node)
932
+ end
933
+ end
934
+
935
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
936
+ s(:send,
937
+ hash, :key?,
938
+ key.to_ast_node)
939
+ end
940
+
941
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
942
+ keys.reduce(nil) do |acc, key|
943
+ pnode = hash_has_key(key, hash)
944
+ next pnode unless acc
945
+
946
+ s(:begin,
947
+ s(:and, acc, pnode))
948
+ end.then do |node|
949
+ predicates.hash_keys(node, keys)
950
+ end
951
+ end
952
+
953
+ #=========== HASH PATTERN (END) ===============
954
+
955
+ def with_guard(node, guard)
956
+ return node unless guard
957
+
958
+ s(:begin,
959
+ s(:and,
960
+ node,
961
+ guard.children[0])).then do |expr|
962
+ next expr unless guard.type == :unless_guard
963
+ s(:send, expr, :!)
964
+ end
965
+ end
966
+
967
+ def with_declared_locals
968
+ lvars.clear
969
+ node = yield
970
+
971
+ return node if lvars.empty?
972
+
973
+ # We need to declare match lvars outside of the outer `find` block,
974
+ # so we do that for that whole pattern
975
+ locals_declare = s(:begin, s(:masgn,
976
+ s(:mlhs, *lvars.uniq.map { |_1| s(:lvasgn, _1) }),
977
+ s(:nil)))
978
+
979
+ s(:begin,
980
+ s(:or,
981
+ locals_declare,
982
+ node))
983
+ end
984
+
985
+ def no_matching_pattern
986
+ raise_error(
987
+ :NoMatchingPatternError,
988
+ s(:send,
989
+ s(:lvar, locals[:matchee]), :inspect)
990
+ )
991
+ end
992
+
993
+ def raise_error(type, msg = "")
994
+ s(:send, s(:const, nil, :Kernel), :raise,
995
+ s(:const, nil, type),
996
+ msg.to_ast_node)
997
+ end
998
+
999
+ # Add respond_to? check
1000
+ def respond_to_check(node, mid)
1001
+ s(:send, node, :respond_to?, mid.to_ast_node)
1002
+ end
1003
+
1004
+ def respond_to_missing?(mid, *)
1005
+ return true if mid.to_s.match?(/_(clause|array_element)/)
1006
+ super
1007
+ end
1008
+
1009
+ def method_missing(mid, *args, &block)
1010
+ mid = mid.to_s
1011
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
1012
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
1013
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
1014
+ super
1015
+ end
1016
+
1017
+ private
1018
+
1019
+ attr_reader :deconstructed_keys, :predicates, :lvars
1020
+
1021
+ # Raise SyntaxError if match-var is used within alternation
1022
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
1023
+ def check_match_var_alternation!(name)
1024
+ return unless locals.key?(ALTERNATION_MARKER)
1025
+
1026
+ if name.is_a?(::Parser::AST::Node)
1027
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name.children.first})"
1028
+ end
1029
+
1030
+ return if name.start_with?("_")
1031
+
1032
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
1033
+ end
1034
+
1035
+ def deconstructed_key(key)
1036
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
1037
+
1038
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
1039
+ end
1040
+
1041
+ # Unparser generates `do .. end` or `{ ... }` multiline blocks, we want to
1042
+ # have single-line blocks with `{ ... }`.
1043
+ def inline_blocks(source)
1044
+ source.gsub(/(?:do|{) \|_, __i__\|\n\s*([^\n]+)\n\s*(?:end|})/, '{ |_, __i__| \1 }')
1045
+ end
1046
+
1047
+ # Value could be omitted for mass assignment
1048
+ def build_var_assignment(var, value = nil)
1049
+ unless var.is_a?(::Parser::AST::Node)
1050
+ lvars << var
1051
+ return s(:lvasgn, *[var, value].compact)
1052
+ end
1053
+
1054
+ asign_type = :"#{var.type.to_s[0]}vasgn"
1055
+
1056
+ s(asign_type, *[var.children[0], value].compact)
1057
+ end
1058
+ end
1059
+ end
1060
+ end
1061
+ end