ruby-next-core 0.14.0 → 0.15.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,1060 @@
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, *)
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)))
429
+ end
430
+ end
431
+
432
+ def match_alt_clause(node)
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
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 match_alt_array_element(node, index)
667
+ children = node.children.map do |child, i|
668
+ send :"#{child.type}_array_element", child, index
669
+ end
670
+ s(:begin, s(:or, *children))
671
+ end
672
+
673
+ def match_var_array_element(node, index)
674
+ match_var_clause(node, arr_item_at(index))
675
+ end
676
+
677
+ def match_as_array_element(node, index)
678
+ match_as_clause(node, arr_item_at(index))
679
+ end
680
+
681
+ def pin_array_element(node, index)
682
+ case_eq_array_element node.children[0], index
683
+ end
684
+
685
+ def case_eq_array_element(node, index)
686
+ case_eq_clause(node, arr_item_at(index))
687
+ end
688
+
689
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
690
+ s(:index, arr, index.to_ast_node)
691
+ end
692
+
693
+ def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
694
+ s(:index,
695
+ arr,
696
+ s(:irange,
697
+ lindex.to_ast_node,
698
+ rindex.to_ast_node))
699
+ end
700
+
701
+ #=========== ARRAY PATTERN (END) ===============
702
+
703
+ #=========== HASH PATTERN (START) ===============
704
+
705
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
706
+ # Optimization: avoid hash modifications when not needed
707
+ # (we use #dup and #delete when "reading" values when **rest is present
708
+ # to assign the rest of the hash copy to it)
709
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
710
+ keys = hash_pattern_destruction_keys(node.children)
711
+
712
+ specified_key_names = hash_pattern_keys(node.children)
713
+
714
+ deconstruct_keys_node(keys, matchee).then do |dnode|
715
+ right =
716
+ if node.children.empty?
717
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
718
+ elsif specified_key_names.empty?
719
+ hash_element(*node.children)
720
+ else
721
+ s(:begin,
722
+ s(:and,
723
+ having_hash_keys(specified_key_names),
724
+ hash_element(*node.children)))
725
+ end
726
+
727
+ predicates.pop
728
+
729
+ next dnode if right.nil?
730
+
731
+ s(:begin,
732
+ s(:and,
733
+ dnode,
734
+ right))
735
+ end
736
+ end
737
+
738
+ def hash_pattern_keys(children)
739
+ children.filter_map do |child|
740
+ # Skip ** without var
741
+ next if child.type == :match_rest || child.type == :match_nil_pattern
742
+
743
+ send("#{child.type}_hash_key", child)
744
+ end
745
+ end
746
+
747
+ def hash_pattern_destruction_keys(children)
748
+ return s(:nil) if children.empty?
749
+
750
+ children.filter_map do |child|
751
+ # Skip ** without var
752
+ next if child.type == :match_rest && child.children.empty?
753
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
754
+
755
+ send("#{child.type}_hash_key", child)
756
+ end.then { |keys| s(:array, *keys) }
757
+ end
758
+
759
+ def pair_hash_key(node)
760
+ node.children[0]
761
+ end
762
+
763
+ def match_var_hash_key(node)
764
+ check_match_var_alternation! node.children[0]
765
+
766
+ s(:sym, node.children[0])
767
+ end
768
+
769
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
770
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
771
+ # 'cause it remains immutable
772
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
773
+
774
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
775
+ hash_dup =
776
+ if @hash_match_rest
777
+ s(:begin, s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup)))
778
+ else
779
+ s(:true)
780
+ end
781
+
782
+ context.use_ruby_next!
783
+
784
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
785
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
786
+
787
+ key_names = keys.children.map { |node| node.children.last }
788
+ predicates.push locals[:hash]
789
+
790
+ s(:begin, s(:lvasgn, deconstruct_name,
791
+ s(:send,
792
+ matchee, :deconstruct_keys, keys))).then do |dnode|
793
+ next dnode if respond_to_checked
794
+
795
+ s(:and,
796
+ respond_check,
797
+ s(:begin,
798
+ s(:and,
799
+ s(:begin,
800
+ s(:or,
801
+ dnode,
802
+ s(:true))),
803
+ s(:begin,
804
+ s(:or,
805
+ s(:send,
806
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
807
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))))
808
+ end.then do |dnode|
809
+ predicates.hash_deconstructed(dnode, key_names)
810
+ end.then do |dnode|
811
+ next dnode unless @hash_match_rest
812
+
813
+ s(:begin,
814
+ s(:and,
815
+ dnode,
816
+ hash_dup))
817
+ end
818
+ end
819
+
820
+ def hash_pattern_hash_element(node, key)
821
+ element = hash_value_at(key)
822
+ key_index = deconstructed_key(key)
823
+ locals.with(hash: locals[:hash, key_index]) do
824
+ predicates.push :"k#{key_index}"
825
+ hash_pattern_clause(node, element).tap { predicates.pop }
826
+ end
827
+ end
828
+
829
+ def array_pattern_hash_element(node, key)
830
+ element = hash_value_at(key)
831
+ key_index = deconstructed_key(key)
832
+ locals.with(arr: locals[:hash, key_index]) do
833
+ predicates.push :"k#{key_index}"
834
+ array_pattern_clause(node, element).tap { predicates.pop }
835
+ end
836
+ end
837
+
838
+ def find_pattern_hash_element(node, key)
839
+ element = hash_value_at(key)
840
+ key_index = deconstructed_key(key)
841
+ locals.with(arr: locals[:hash, key_index]) do
842
+ predicates.push :"k#{key_index}"
843
+ find_pattern_clause(node, element).tap { predicates.pop }
844
+ end
845
+ end
846
+
847
+ def hash_element(head, *tail)
848
+ send("#{head.type}_hash_element", head).then do |node|
849
+ next node if tail.empty?
850
+
851
+ right = hash_element(*tail)
852
+
853
+ next node if right.nil?
854
+
855
+ s(:begin,
856
+ s(:and,
857
+ node,
858
+ right))
859
+ end
860
+ end
861
+
862
+ def pair_hash_element(node, _key = nil)
863
+ key, val = *node.children
864
+ send("#{val.type}_hash_element", val, key)
865
+ end
866
+
867
+ def match_alt_hash_element(node, key)
868
+ element_node = s(:begin, s(:lvasgn, locals[:hash, :el], hash_value_at(key)))
869
+
870
+ children = locals.with(hash_element: locals[:hash, :el]) do
871
+ node.children.map do |child, i|
872
+ send :"#{child.type}_hash_element", child, key
873
+ end
874
+ end
875
+
876
+ s(:begin,
877
+ s(:and,
878
+ s(:begin,
879
+ s(:or,
880
+ element_node,
881
+ s(:true))),
882
+ s(:begin,
883
+ s(:or, *children))))
884
+ end
885
+
886
+ def match_as_hash_element(node, key)
887
+ match_as_clause(node, hash_value_at(key))
888
+ end
889
+
890
+ def match_var_hash_element(node, key = nil)
891
+ key ||= node.children[0]
892
+ match_var_clause(node, hash_value_at(key))
893
+ end
894
+
895
+ def match_nil_pattern_hash_element(node, _key = nil)
896
+ s(:send,
897
+ s(:lvar, locals[:hash]),
898
+ :empty?)
899
+ end
900
+
901
+ def match_rest_hash_element(node, _key = nil)
902
+ # case {}; in **; end
903
+ return if node.children.empty?
904
+
905
+ child = node.children[0]
906
+
907
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
908
+
909
+ match_var_clause(child, s(:lvar, locals[:hash]))
910
+ end
911
+
912
+ def pin_hash_element(node, index)
913
+ case_eq_hash_element node.children[0], index
914
+ end
915
+
916
+ def case_eq_hash_element(node, key)
917
+ case_eq_clause node, hash_value_at(key)
918
+ end
919
+
920
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
921
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
922
+
923
+ if @hash_match_rest
924
+ s(:send,
925
+ hash, :delete,
926
+ key.to_ast_node)
927
+ else
928
+ s(:index,
929
+ hash,
930
+ key.to_ast_node)
931
+ end
932
+ end
933
+
934
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
935
+ s(:send,
936
+ hash, :key?,
937
+ key.to_ast_node)
938
+ end
939
+
940
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
941
+ keys.reduce(nil) do |acc, key|
942
+ pnode = hash_has_key(key, hash)
943
+ next pnode unless acc
944
+
945
+ s(:begin,
946
+ s(:and, acc, pnode))
947
+ end.then do |node|
948
+ predicates.hash_keys(node, keys)
949
+ end
950
+ end
951
+
952
+ #=========== HASH PATTERN (END) ===============
953
+
954
+ def with_guard(node, guard)
955
+ return node unless guard
956
+
957
+ s(:begin,
958
+ s(:and,
959
+ node,
960
+ guard.children[0])).then do |expr|
961
+ next expr unless guard.type == :unless_guard
962
+ s(:send, expr, :!)
963
+ end
964
+ end
965
+
966
+ def with_declared_locals
967
+ lvars.clear
968
+ node = yield
969
+
970
+ return node if lvars.empty?
971
+
972
+ # We need to declare match lvars outside of the outer `find` block,
973
+ # so we do that for that whole pattern
974
+ locals_declare = s(:begin, s(:masgn,
975
+ s(:mlhs, *lvars.uniq.map { |_1| s(:lvasgn, _1) }),
976
+ s(:nil)))
977
+
978
+ s(:begin,
979
+ s(:or,
980
+ locals_declare,
981
+ node))
982
+ end
983
+
984
+ def no_matching_pattern
985
+ raise_error(
986
+ :NoMatchingPatternError,
987
+ s(:send,
988
+ s(:lvar, locals[:matchee]), :inspect)
989
+ )
990
+ end
991
+
992
+ def raise_error(type, msg = "")
993
+ s(:send, s(:const, nil, :Kernel), :raise,
994
+ s(:const, nil, type),
995
+ msg.to_ast_node)
996
+ end
997
+
998
+ # Add respond_to? check
999
+ def respond_to_check(node, mid)
1000
+ s(:send, node, :respond_to?, mid.to_ast_node)
1001
+ end
1002
+
1003
+ def respond_to_missing?(mid, *)
1004
+ return true if mid.to_s.match?(/_(clause|array_element)/)
1005
+ super
1006
+ end
1007
+
1008
+ def method_missing(mid, *args, &block)
1009
+ mid = mid.to_s
1010
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
1011
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
1012
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
1013
+ super
1014
+ end
1015
+
1016
+ private
1017
+
1018
+ attr_reader :deconstructed_keys, :predicates, :lvars
1019
+
1020
+ # Raise SyntaxError if match-var is used within alternation
1021
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
1022
+ def check_match_var_alternation!(name)
1023
+ return unless locals.key?(ALTERNATION_MARKER)
1024
+
1025
+ if name.is_a?(::Parser::AST::Node)
1026
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name.children.first})"
1027
+ end
1028
+
1029
+ return if name.start_with?("_")
1030
+
1031
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
1032
+ end
1033
+
1034
+ def deconstructed_key(key)
1035
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
1036
+
1037
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
1038
+ end
1039
+
1040
+ # Unparser generates `do .. end` or `{ ... }` multiline blocks, we want to
1041
+ # have single-line blocks with `{ ... }`.
1042
+ def inline_blocks(source)
1043
+ source.gsub(/(?:do|{) \|_, __i__\|\n\s*([^\n]+)\n\s*(?:end|})/, '{ |_, __i__| \1 }')
1044
+ end
1045
+
1046
+ # Value could be omitted for mass assignment
1047
+ def build_var_assignment(var, value = nil)
1048
+ unless var.is_a?(::Parser::AST::Node)
1049
+ lvars << var
1050
+ return s(:lvasgn, *[var, value].compact)
1051
+ end
1052
+
1053
+ asign_type = :"#{var.type.to_s[0]}vasgn"
1054
+
1055
+ s(asign_type, *[var.children[0], value].compact)
1056
+ end
1057
+ end
1058
+ end
1059
+ end
1060
+ end