ruby-next-core 0.14.0 → 0.14.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.
@@ -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