ruby-next-core 0.9.2 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +22 -0
  3. data/README.md +14 -4
  4. data/lib/.rbnext/2.3/ruby-next/commands/core_ext.rb +167 -0
  5. data/lib/.rbnext/2.3/ruby-next/commands/nextify.rb +198 -0
  6. data/lib/.rbnext/2.3/ruby-next/language/eval.rb +66 -0
  7. data/lib/.rbnext/2.3/ruby-next/language/rewriters/base.rb +121 -0
  8. data/lib/.rbnext/2.3/ruby-next/language/rewriters/endless_range.rb +63 -0
  9. data/lib/.rbnext/2.3/ruby-next/language/rewriters/pattern_matching.rb +944 -0
  10. data/lib/.rbnext/2.3/ruby-next/utils.rb +65 -0
  11. data/lib/ruby-next.rb +8 -6
  12. data/lib/ruby-next/cli.rb +2 -2
  13. data/lib/ruby-next/commands/core_ext.rb +1 -1
  14. data/lib/ruby-next/core.rb +27 -21
  15. data/lib/ruby-next/core/array/deconstruct.rb +9 -9
  16. data/lib/ruby-next/core/array/difference_union_intersection.rb +12 -12
  17. data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +3 -3
  18. data/lib/ruby-next/core/enumerable/filter.rb +8 -8
  19. data/lib/ruby-next/core/enumerable/filter_map.rb +25 -25
  20. data/lib/ruby-next/core/enumerable/tally.rb +7 -7
  21. data/lib/ruby-next/core/enumerator/produce.rb +12 -12
  22. data/lib/ruby-next/core/hash/deconstruct_keys.rb +9 -9
  23. data/lib/ruby-next/core/hash/except.rb +11 -0
  24. data/lib/ruby-next/core/hash/merge.rb +8 -8
  25. data/lib/ruby-next/core/kernel/then.rb +2 -2
  26. data/lib/ruby-next/core/proc/compose.rb +11 -11
  27. data/lib/ruby-next/core/string/split.rb +6 -6
  28. data/lib/ruby-next/core/struct/deconstruct.rb +2 -2
  29. data/lib/ruby-next/core/struct/deconstruct_keys.rb +17 -17
  30. data/lib/ruby-next/core/symbol/end_with.rb +4 -4
  31. data/lib/ruby-next/core/symbol/start_with.rb +4 -4
  32. data/lib/ruby-next/core/time/ceil.rb +6 -6
  33. data/lib/ruby-next/core/time/floor.rb +4 -4
  34. data/lib/ruby-next/core/unboundmethod/bind_call.rb +4 -4
  35. data/lib/ruby-next/core_ext.rb +1 -1
  36. data/lib/ruby-next/language.rb +12 -1
  37. data/lib/ruby-next/language/parser.rb +0 -3
  38. data/lib/ruby-next/language/proposed.rb +3 -0
  39. data/lib/ruby-next/language/rewriters/args_forward.rb +23 -20
  40. data/lib/ruby-next/language/rewriters/base.rb +1 -1
  41. data/lib/ruby-next/language/rewriters/endless_method.rb +25 -3
  42. data/lib/ruby-next/language/rewriters/find_pattern.rb +44 -0
  43. data/lib/ruby-next/language/rewriters/method_reference.rb +1 -1
  44. data/lib/ruby-next/language/rewriters/pattern_matching.rb +102 -12
  45. data/lib/ruby-next/language/rewriters/right_hand_assignment.rb +1 -1
  46. data/lib/ruby-next/language/rewriters/safe_navigation.rb +87 -0
  47. data/lib/ruby-next/language/rewriters/shorthand_hash.rb +47 -0
  48. data/lib/ruby-next/language/rewriters/squiggly_heredoc.rb +36 -0
  49. data/lib/ruby-next/language/unparser.rb +0 -14
  50. data/lib/ruby-next/logging.rb +1 -1
  51. data/lib/ruby-next/rubocop.rb +15 -9
  52. data/lib/ruby-next/setup_self.rb +22 -0
  53. data/lib/ruby-next/version.rb +1 -1
  54. data/lib/uby-next.rb +8 -4
  55. metadata +20 -7
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module KernelEval
6
+ if Utils.refine_modules?
7
+ refine Kernel do
8
+ def eval(source, bind = nil, *args)
9
+ new_source = ::RubyNext::Language::Runtime.transform(
10
+ source,
11
+ using: ((!bind.nil?) || nil) && bind.receiver == TOPLEVEL_BINDING.receiver || ((!(((!bind.nil?) || nil) && bind.receiver).nil?) || nil) && (((!bind.nil?) || nil) && bind.receiver).is_a?(Module)
12
+ )
13
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
14
+ super new_source, bind, *args
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ module InstanceEval # :nodoc:
21
+ refine Object do
22
+ def instance_eval(*args, &block)
23
+ return super(*args, &block) if block_given?
24
+
25
+ source = args.shift
26
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
27
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
28
+ super new_source, *args
29
+ end
30
+ end
31
+ end
32
+
33
+ module ClassEval
34
+ refine Module do
35
+ def module_eval(*args, &block)
36
+ return super(*args, &block) if block_given?
37
+
38
+ source = args.shift
39
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
40
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
41
+ super new_source, *args
42
+ end
43
+
44
+ def class_eval(*args, &block)
45
+ return super(*args, &block) if block_given?
46
+
47
+ source = args.shift
48
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
49
+ RubyNext.debug_source(new_source, "(#{caller_locations(1, 1).first})")
50
+ super new_source, *args
51
+ end
52
+ end
53
+ end
54
+
55
+ # Refinements for `eval`-like methods.
56
+ # Transpiling eval is only possible if we do not use local from the binding,
57
+ # because we cannot access the binding of caller (without non-production ready hacks).
58
+ #
59
+ # This module is meant mainly for testing purposes.
60
+ module Eval
61
+ include InstanceEval
62
+ include ClassEval
63
+ include KernelEval
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,121 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ using RubyNext
7
+
8
+ CUSTOM_PARSER_REQUIRED = <<-MSG
9
+ The %s feature is not a part of the latest stable Ruby release
10
+ and is not supported by your Parser gem version.
11
+ Use RubyNext's parser to use it: https://github.com/ruby-next/parser
12
+
13
+ MSG
14
+
15
+ class Base < ::Parser::TreeRewriter
16
+ class LocalsTracker
17
+ attr_reader :stacks
18
+
19
+ def initialize
20
+ @stacks = []
21
+ end
22
+
23
+ def with(**locals)
24
+ stacks << locals
25
+ yield.tap { stacks.pop }
26
+ end
27
+
28
+ def [](name, suffix = nil)
29
+ fetch(name).then do |name|
30
+ next name unless suffix
31
+ :"#{name}#{suffix}__"
32
+ end
33
+ end
34
+
35
+ def key?(name)
36
+ !!fetch(name) { false } # rubocop:disable Style/RedundantFetchBlock
37
+ end
38
+
39
+ def fetch(name)
40
+ ind = -1
41
+
42
+ loop do
43
+ break stacks[ind][name] if stacks[ind].key?(name)
44
+ ind -= 1
45
+ break if stacks[ind].nil?
46
+ end.then do |name|
47
+ next name unless name.nil?
48
+
49
+ return yield if block_given?
50
+ raise ArgumentError, "Local var not found in scope: #{name}"
51
+ end
52
+ end
53
+ end
54
+
55
+ class << self
56
+ # Returns true if the syntax is supported
57
+ # by the current Ruby (performs syntax check, not version check)
58
+ def unsupported_syntax?
59
+ save_verbose, $VERBOSE = $VERBOSE, nil
60
+ eval_mid = Kernel.respond_to?(:eval_without_ruby_next) ? :eval_without_ruby_next : :eval
61
+ Kernel.send eval_mid, self::SYNTAX_PROBE, nil, __FILE__, __LINE__
62
+ false
63
+ rescue SyntaxError, NameError
64
+ true
65
+ ensure
66
+ $VERBOSE = save_verbose
67
+ end
68
+
69
+ # Returns true if the syntax is supported
70
+ # by the specified version
71
+ def unsupported_version?(version)
72
+ self::MIN_SUPPORTED_VERSION > version
73
+ end
74
+
75
+ private
76
+
77
+ def transform(source)
78
+ Language.transform(source, rewriters: [self], using: false)
79
+ end
80
+ end
81
+
82
+ attr_reader :locals
83
+
84
+ def initialize(context)
85
+ @context = context
86
+ @locals = LocalsTracker.new
87
+ super()
88
+ end
89
+
90
+ def s(type, *children)
91
+ ::Parser::AST::Node.new(type, children)
92
+ end
93
+
94
+ private
95
+
96
+ def replace(range, ast)
97
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.replace(range, unparse(ast))
98
+ end
99
+
100
+ def remove(range)
101
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.remove(range)
102
+ end
103
+
104
+ def insert_after(range, ast)
105
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_after(range, unparse(ast))
106
+ end
107
+
108
+ def insert_before(range, ast)
109
+ ((!@source_rewriter.nil?) || nil) && @source_rewriter.insert_before(range, unparse(ast))
110
+ end
111
+
112
+ def unparse(ast)
113
+ return ast if ast.is_a?(String)
114
+ Unparser.unparse(ast)
115
+ end
116
+
117
+ attr_reader :context
118
+ end
119
+ end
120
+ end
121
+ end
@@ -0,0 +1,63 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ class EndlessRange < Base
7
+ NAME = "endless-range"
8
+ SYNTAX_PROBE = "[0, 1][1..]"
9
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.6.0")
10
+
11
+ def on_index(node)
12
+ @current_index = node
13
+ new_index = process(node.children.last)
14
+ return unless new_index != node.children.last
15
+
16
+ node.updated(
17
+ nil,
18
+ [
19
+ node.children.first,
20
+ new_index
21
+ ]
22
+ )
23
+ end
24
+
25
+ def on_erange(node)
26
+ return unless node.children.last.nil?
27
+
28
+ context.track! self
29
+
30
+ new_end =
31
+ if index_arg?(node)
32
+ s(:int, -1)
33
+ else
34
+ s(:const,
35
+ s(:const,
36
+ s(:cbase), :Float),
37
+ :INFINITY)
38
+ end
39
+
40
+ replace(node.loc.expression, "#{node.children.first.loc.expression.source}..#{unparse(new_end)}")
41
+
42
+ node.updated(
43
+ :irange,
44
+ [
45
+ node.children.first,
46
+ new_end
47
+ ]
48
+ )
49
+ end
50
+
51
+ alias_method :on_irange, :on_erange
52
+
53
+ private
54
+
55
+ attr_reader :current_index
56
+
57
+ def index_arg?(node)
58
+ ((!(((!current_index.nil?) || nil) && current_index.children).nil?) || nil) && (((!current_index.nil?) || nil) && current_index.children).include?(node)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,944 @@
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 -(val)
17
+ ::Parser::AST::Node.new(:send, [self, :-, val.to_ast_node])
18
+ end
19
+
20
+ def +(val)
21
+ ::Parser::AST::Node.new(:send, [self, :+, val.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_key(node, key)
222
+ key = key.children.first if key.is_a?(::Parser::AST::Node)
223
+ predicate_clause(:"hash_key_#{key}", 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
+
246
+ matchee_ast =
247
+ s(:lvasgn, MATCHEE, node.children[0])
248
+
249
+ patterns = locals.with(
250
+ matchee: MATCHEE,
251
+ arr: MATCHEE_ARR,
252
+ hash: MATCHEE_HASH
253
+ ) do
254
+ build_case_when(node.children[1..-1])
255
+ end
256
+
257
+ case_clause = predicates.process(s(:case, *patterns))
258
+
259
+ rewrite_case_in! node, matchee_ast, case_clause
260
+
261
+ node.updated(
262
+ :kwbegin,
263
+ [
264
+ matchee_ast, case_clause
265
+ ]
266
+ )
267
+ end
268
+
269
+ def on_in_match(node)
270
+ context.track! self
271
+
272
+ @deconstructed_keys = {}
273
+ @predicates = Predicates::Noop.new
274
+
275
+ matchee =
276
+ s(:lvasgn, MATCHEE, node.children[0])
277
+
278
+ pattern =
279
+ locals.with(
280
+ matchee: MATCHEE,
281
+ arr: MATCHEE_ARR,
282
+ hash: MATCHEE_HASH
283
+ ) do
284
+ send(
285
+ :"#{node.children[1].type}_clause",
286
+ node.children[1]
287
+ ).then do |node|
288
+ s(:or,
289
+ node,
290
+ no_matching_pattern)
291
+ end
292
+ end
293
+
294
+ node.updated(
295
+ :and,
296
+ [
297
+ matchee,
298
+ pattern
299
+ ]
300
+ ).tap do |new_node|
301
+ replace(node.loc.expression, inline_blocks(unparse(new_node)))
302
+ end
303
+ end
304
+
305
+ private
306
+
307
+ def rewrite_case_in!(node, matchee, new_node)
308
+ replace(node.loc.keyword, "case; when (#{unparse(matchee)}) && false")
309
+ remove(node.children[0].loc.expression)
310
+
311
+ node.children[1..-1].each.with_index do |clause, i|
312
+ if ((!clause.nil?) || nil) && clause.type == :in_pattern
313
+ # handle multiline clauses differently
314
+ if clause.loc.last_line > clause.children[0].loc.last_line + 1
315
+ height = clause.loc.last_line - clause.children[0].loc.last_line
316
+ padding = "\n" * height
317
+ body_indent = " " * clause.children[2].loc.column
318
+ replace(
319
+ clause.loc.expression,
320
+ "when #{inline_blocks(unparse(new_node.children[i].children[0]))}" \
321
+ "#{padding}" \
322
+ "#{body_indent}#{clause.children[2].loc.expression.source}"
323
+ )
324
+ else
325
+ replace(
326
+ clause.loc.keyword.end.join(clause.children[0].loc.expression.end),
327
+ inline_blocks(unparse(new_node.children[i].children[0]))
328
+ )
329
+ remove(clause.children[1].loc.expression) if clause.children[1]
330
+ replace(clause.loc.keyword, "when ")
331
+ end
332
+ elsif clause.nil?
333
+ insert_after(node.children[-2].loc.expression, "; else; #{unparse(new_node.children.last)}")
334
+ end
335
+ end
336
+ end
337
+
338
+ def build_case_when(nodes)
339
+ else_clause = nil
340
+ clauses = []
341
+
342
+ nodes.each do |clause|
343
+ if ((!clause.nil?) || nil) && clause.type == :in_pattern
344
+ clauses << build_when_clause(clause)
345
+ else
346
+ else_clause = process(clause)
347
+ end
348
+ end
349
+
350
+ else_clause = (else_clause || no_matching_pattern).then do |node|
351
+ next node unless node.type == :empty_else
352
+ s(:empty)
353
+ end
354
+
355
+ clauses << else_clause
356
+ clauses
357
+ end
358
+
359
+ def build_when_clause(clause)
360
+ predicates.reset!
361
+ [
362
+ with_guard(
363
+ send(
364
+ :"#{clause.children[0].type}_clause",
365
+ clause.children[0]
366
+ ),
367
+ clause.children[1] # guard
368
+ ),
369
+ process(clause.children[2] || s(:nil)) # expression
370
+ ].then do |children|
371
+ s(:when, *children)
372
+ end
373
+ end
374
+
375
+ def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
376
+ const, pattern = *node.children
377
+
378
+ predicates.const(case_eq_clause(const, right), const).then do |node|
379
+ next node if pattern.nil?
380
+
381
+ s(:and,
382
+ node,
383
+ send(:"#{pattern.type}_clause", pattern))
384
+ end
385
+ end
386
+
387
+ def match_alt_clause(node)
388
+ children = locals.with(ALTERNATION_MARKER => true) do
389
+ node.children.map.with_index do |child, i|
390
+ predicates.terminate! if i == 1
391
+ send :"#{child.type}_clause", child
392
+ end
393
+ end
394
+ s(:or, *children)
395
+ end
396
+
397
+ def match_as_clause(node, right = s(:lvar, locals[:matchee]))
398
+ s(:and,
399
+ send(:"#{node.children[0].type}_clause", node.children[0], right),
400
+ match_var_clause(node.children[1], right))
401
+ end
402
+
403
+ def match_var_clause(node, left = s(:lvar, locals[:matchee]))
404
+ return s(:true) if node.children[0] == :_
405
+
406
+ check_match_var_alternation! node.children[0]
407
+
408
+ s(:or,
409
+ s(:lvasgn, node.children[0], left),
410
+ s(:true))
411
+ end
412
+
413
+ def pin_clause(node, right = s(:lvar, locals[:matchee]))
414
+ predicates.terminate!
415
+ case_eq_clause node.children[0], right
416
+ end
417
+
418
+ def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
419
+ predicates.terminate!
420
+ s(:send,
421
+ process(node), :===, right)
422
+ end
423
+
424
+ #=========== ARRAY PATTERN (START) ===============
425
+
426
+ def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
427
+ deconstruct_node(matchee).then do |dnode|
428
+ size_check = nil
429
+ # if there is no rest or tail, match the size first
430
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
431
+ size_check = predicates.array_size(
432
+ s(:send,
433
+ node.children.size.to_ast_node,
434
+ :==,
435
+ s(:send, s(:lvar, locals[:arr]), :size)),
436
+ node.children.size
437
+ )
438
+ end
439
+
440
+ right =
441
+ if node.children.empty?
442
+ case_eq_clause(s(:array), s(:lvar, locals[:arr]))
443
+ elsif node.children.size > 1 && node.children.first.type == :match_rest && node.children.last.type == :match_rest
444
+ array_find(*node.children)
445
+ else
446
+ array_element(0, *node.children)
447
+ end
448
+
449
+ right = s(:and, size_check, right) if size_check
450
+
451
+ s(:and,
452
+ dnode,
453
+ right)
454
+ end
455
+ end
456
+
457
+ alias array_pattern_with_tail_clause array_pattern_clause
458
+ alias find_pattern_clause array_pattern_clause
459
+
460
+ def deconstruct_node(matchee)
461
+ context.use_ruby_next!
462
+
463
+ # we do not memoize respond_to_check for arrays, 'cause
464
+ # we can memoize is together with #deconstruct result
465
+ respond_check = respond_to_check(matchee, :deconstruct)
466
+ right = s(:send, matchee, :deconstruct)
467
+
468
+ predicates.array_deconstructed(
469
+ s(:and,
470
+ respond_check,
471
+ s(:and,
472
+ s(:or,
473
+ s(:lvasgn, locals[:arr], right),
474
+ s(:true)),
475
+ s(:or,
476
+ s(:send,
477
+ s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
478
+ raise_error(:TypeError, "#deconstruct must return Array"))))
479
+ )
480
+ end
481
+
482
+ def array_element(index, head, *tail)
483
+ return array_match_rest(index, head, *tail) if head.type == :match_rest
484
+
485
+ send("#{head.type}_array_element", head, index).then do |node|
486
+ next node if tail.empty?
487
+
488
+ s(:and,
489
+ node,
490
+ array_element(index + 1, *tail))
491
+ end
492
+ end
493
+
494
+ # [*a, 1, 2, *] -> arr.find.with_index { |_, i| (a = arr.take(i)) && arr[i] == 1 && arr[i + 1] == 2 }
495
+ def array_find(head, *nodes, tail)
496
+ index = s(:lvar, :__i__)
497
+
498
+ match_vars = []
499
+
500
+ head_match =
501
+ unless head.children.empty?
502
+ match_vars << s(:lvasgn, head.children[0].children[0])
503
+
504
+ arr_take = s(:send,
505
+ s(:lvar, locals[:arr]),
506
+ :take,
507
+ index)
508
+
509
+ match_var_clause(head.children[0], arr_take)
510
+ end
511
+
512
+ tail_match =
513
+ unless tail.children.empty?
514
+ match_vars << s(:lvasgn, tail.children[0].children[0])
515
+
516
+ match_var_clause(tail.children[0], arr_slice(index + nodes.size, -1))
517
+ end
518
+
519
+ nodes.each do |node|
520
+ if node.type == :match_var
521
+ match_vars << s(:lvasgn, node.children[0])
522
+ elsif node.type == :match_as
523
+ match_vars << s(:lvasgn, node.children[1].children[0])
524
+ end
525
+ end
526
+
527
+ pattern = array_rest_element(*nodes, index).then do |needle|
528
+ next needle unless head_match
529
+ s(:and,
530
+ needle,
531
+ head_match)
532
+ end.then do |headed_needle|
533
+ next headed_needle unless tail_match
534
+
535
+ s(:and,
536
+ headed_needle,
537
+ tail_match)
538
+ end
539
+
540
+ s(:block,
541
+ s(:send,
542
+ s(:send,
543
+ s(:lvar, locals[:arr]),
544
+ :find),
545
+ :with_index),
546
+ s(:args,
547
+ s(:arg, :_),
548
+ s(:arg, :__i__)),
549
+ pattern).then do |block|
550
+ next block if match_vars.empty?
551
+
552
+ # We need to declare match vars outside of `find` block
553
+ locals_declare = s(:masgn,
554
+ s(:mlhs, *match_vars),
555
+ s(:nil))
556
+
557
+ s(:or,
558
+ locals_declare,
559
+ block)
560
+ end
561
+ end
562
+
563
+ def array_match_rest(index, node, *tail)
564
+ size = tail.size + 1
565
+ child = node.children[0]
566
+
567
+ rest = arr_slice(index, -size).then do |r|
568
+ next r unless child
569
+
570
+ match_var_clause(
571
+ child,
572
+ r
573
+ )
574
+ end
575
+
576
+ return rest if tail.empty?
577
+
578
+ s(:and,
579
+ rest,
580
+ array_rest_element(*tail, -(size - 1)))
581
+ end
582
+
583
+ def array_rest_element(head, *tail, index)
584
+ send("#{head.type}_array_element", head, index).then do |node|
585
+ next node if tail.empty?
586
+
587
+ s(:and,
588
+ node,
589
+ array_rest_element(*tail, index + 1))
590
+ end
591
+ end
592
+
593
+ def array_pattern_array_element(node, index)
594
+ element = arr_item_at(index)
595
+ locals.with(arr: locals[:arr, index]) do
596
+ predicates.push :"i#{index}"
597
+ array_pattern_clause(node, element).tap { predicates.pop }
598
+ end
599
+ end
600
+
601
+ def hash_pattern_array_element(node, index)
602
+ element = arr_item_at(index)
603
+ locals.with(hash: locals[:arr, index]) do
604
+ predicates.push :"i#{index}"
605
+ hash_pattern_clause(node, element).tap { predicates.pop }
606
+ end
607
+ end
608
+
609
+ def match_alt_array_element(node, index)
610
+ children = node.children.map do |child, i|
611
+ send :"#{child.type}_array_element", child, index
612
+ end
613
+ s(:or, *children)
614
+ end
615
+
616
+ def match_var_array_element(node, index)
617
+ match_var_clause(node, arr_item_at(index))
618
+ end
619
+
620
+ def match_as_array_element(node, index)
621
+ match_as_clause(node, arr_item_at(index))
622
+ end
623
+
624
+ def pin_array_element(node, index)
625
+ case_eq_array_element node.children[0], index
626
+ end
627
+
628
+ def case_eq_array_element(node, index)
629
+ case_eq_clause(node, arr_item_at(index))
630
+ end
631
+
632
+ def arr_item_at(index, arr = s(:lvar, locals[:arr]))
633
+ s(:index, arr, index.to_ast_node)
634
+ end
635
+
636
+ def arr_slice(lindex, rindex, arr = s(:lvar, locals[:arr]))
637
+ s(:index,
638
+ arr,
639
+ s(:irange,
640
+ lindex.to_ast_node,
641
+ rindex.to_ast_node))
642
+ end
643
+
644
+ #=========== ARRAY PATTERN (END) ===============
645
+
646
+ #=========== HASH PATTERN (START) ===============
647
+
648
+ def hash_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
649
+ # Optimization: avoid hash modifications when not needed
650
+ # (we use #dup and #delete when "reading" values when **rest is present
651
+ # to assign the rest of the hash copy to it)
652
+ @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
653
+ keys = hash_pattern_destruction_keys(node.children)
654
+
655
+ specified_key_names = hash_pattern_keys(node.children)
656
+
657
+ deconstruct_keys_node(keys, matchee).then do |dnode|
658
+ right =
659
+ if node.children.empty?
660
+ case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
661
+ elsif specified_key_names.empty?
662
+ hash_element(*node.children)
663
+ else
664
+ s(:and,
665
+ having_hash_keys(specified_key_names),
666
+ hash_element(*node.children))
667
+ end
668
+
669
+ predicates.pop
670
+
671
+ next dnode if right.nil?
672
+
673
+ s(:and,
674
+ dnode,
675
+ right)
676
+ end
677
+ end
678
+
679
+ def hash_pattern_keys(children)
680
+ children.filter_map do |child|
681
+ # Skip ** without var
682
+ next if child.type == :match_rest || child.type == :match_nil_pattern
683
+
684
+ send("#{child.type}_hash_key", child)
685
+ end
686
+ end
687
+
688
+ def hash_pattern_destruction_keys(children)
689
+ return s(:nil) if children.empty?
690
+
691
+ children.filter_map do |child|
692
+ # Skip ** without var
693
+ next if child.type == :match_rest && child.children.empty?
694
+ return s(:nil) if child.type == :match_rest || child.type == :match_nil_pattern
695
+
696
+ send("#{child.type}_hash_key", child)
697
+ end.then { |keys| s(:array, *keys) }
698
+ end
699
+
700
+ def pair_hash_key(node)
701
+ node.children[0]
702
+ end
703
+
704
+ def match_var_hash_key(node)
705
+ check_match_var_alternation! node.children[0]
706
+
707
+ s(:sym, node.children[0])
708
+ end
709
+
710
+ def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
711
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
712
+ # 'cause it remains immutable
713
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
714
+
715
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
716
+ hash_dup =
717
+ if @hash_match_rest
718
+ s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup))
719
+ else
720
+ s(:true)
721
+ end
722
+
723
+ context.use_ruby_next!
724
+
725
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
726
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
727
+
728
+ key_names = keys.children.map { |node| node.children.last }
729
+ predicates.push locals[:hash]
730
+
731
+ s(:lvasgn, deconstruct_name,
732
+ s(:send,
733
+ matchee, :deconstruct_keys, keys)).then do |dnode|
734
+ next dnode if respond_to_checked
735
+
736
+ s(:and,
737
+ respond_check,
738
+ s(:and,
739
+ s(:or,
740
+ dnode,
741
+ s(:true)),
742
+ s(:or,
743
+ s(:send,
744
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
745
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))
746
+ end.then do |dnode|
747
+ predicates.hash_deconstructed(dnode, key_names)
748
+ end.then do |dnode|
749
+ next dnode unless @hash_match_rest
750
+
751
+ s(:and,
752
+ dnode,
753
+ hash_dup)
754
+ end
755
+ end
756
+
757
+ def hash_pattern_hash_element(node, key)
758
+ element = hash_value_at(key)
759
+ key_index = deconstructed_key(key)
760
+ locals.with(hash: locals[:hash, key_index]) do
761
+ predicates.push :"k#{key_index}"
762
+ hash_pattern_clause(node, element).tap { predicates.pop }
763
+ end
764
+ end
765
+
766
+ def array_pattern_hash_element(node, key)
767
+ element = hash_value_at(key)
768
+ key_index = deconstructed_key(key)
769
+ locals.with(arr: locals[:hash, key_index]) do
770
+ predicates.push :"k#{key_index}"
771
+ array_pattern_clause(node, element).tap { predicates.pop }
772
+ end
773
+ end
774
+
775
+ def hash_element(head, *tail)
776
+ send("#{head.type}_hash_element", head).then do |node|
777
+ next node if tail.empty?
778
+
779
+ right = hash_element(*tail)
780
+
781
+ next node if right.nil?
782
+
783
+ s(:and,
784
+ node,
785
+ right)
786
+ end
787
+ end
788
+
789
+ def pair_hash_element(node, _key = nil)
790
+ key, val = *node.children
791
+ send("#{val.type}_hash_element", val, key)
792
+ end
793
+
794
+ def match_alt_hash_element(node, key)
795
+ element_node = s(:lvasgn, locals[:hash, :el], hash_value_at(key))
796
+
797
+ children = locals.with(hash_element: locals[:hash, :el]) do
798
+ node.children.map do |child, i|
799
+ send :"#{child.type}_hash_element", child, key
800
+ end
801
+ end
802
+
803
+ s(:and,
804
+ s(:or,
805
+ element_node,
806
+ s(:true)),
807
+ s(:or, *children))
808
+ end
809
+
810
+ def match_as_hash_element(node, key)
811
+ match_as_clause(node, hash_value_at(key))
812
+ end
813
+
814
+ def match_var_hash_element(node, key = nil)
815
+ key ||= node.children[0]
816
+ match_var_clause(node, hash_value_at(key))
817
+ end
818
+
819
+ def match_nil_pattern_hash_element(node, _key = nil)
820
+ s(:send,
821
+ s(:lvar, locals[:hash]),
822
+ :empty?)
823
+ end
824
+
825
+ def match_rest_hash_element(node, _key = nil)
826
+ # case {}; in **; end
827
+ return if node.children.empty?
828
+
829
+ child = node.children[0]
830
+
831
+ raise ArgumentError, "Unknown hash match_rest child: #{child.type}" unless child.type == :match_var
832
+
833
+ match_var_clause(child, s(:lvar, locals[:hash]))
834
+ end
835
+
836
+ def case_eq_hash_element(node, key)
837
+ case_eq_clause node, hash_value_at(key)
838
+ end
839
+
840
+ def hash_value_at(key, hash = s(:lvar, locals[:hash]))
841
+ return s(:lvar, locals.fetch(:hash_element)) if locals.key?(:hash_element)
842
+
843
+ if @hash_match_rest
844
+ s(:send,
845
+ hash, :delete,
846
+ key.to_ast_node)
847
+ else
848
+ s(:index,
849
+ hash,
850
+ key.to_ast_node)
851
+ end
852
+ end
853
+
854
+ def hash_has_key(key, hash = s(:lvar, locals[:hash]))
855
+ s(:send,
856
+ hash, :key?,
857
+ key.to_ast_node)
858
+ end
859
+
860
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
861
+ key = keys.shift
862
+ node = predicates.hash_key(hash_has_key(key, hash), key)
863
+
864
+ keys.reduce(node) do |res, key|
865
+ s(:and,
866
+ res,
867
+ predicates.hash_key(hash_has_key(key, hash), key))
868
+ end
869
+ end
870
+
871
+ #=========== HASH PATTERN (END) ===============
872
+
873
+ def with_guard(node, guard)
874
+ return node unless guard
875
+
876
+ s(:and,
877
+ node,
878
+ guard.children[0]).then do |expr|
879
+ next expr unless guard.type == :unless_guard
880
+ s(:send, expr, :!)
881
+ end
882
+ end
883
+
884
+ def no_matching_pattern
885
+ raise_error(
886
+ :NoMatchingPatternError,
887
+ s(:send,
888
+ s(:lvar, locals[:matchee]), :inspect)
889
+ )
890
+ end
891
+
892
+ def raise_error(type, msg = "")
893
+ s(:send, s(:const, nil, :Kernel), :raise,
894
+ s(:const, nil, type),
895
+ msg.to_ast_node)
896
+ end
897
+
898
+ # Add respond_to? check
899
+ def respond_to_check(node, mid)
900
+ s(:send, node, :respond_to?, mid.to_ast_node)
901
+ end
902
+
903
+ def respond_to_missing?(mid, *)
904
+ return true if mid.to_s.match?(/_(clause|array_element)/)
905
+ super
906
+ end
907
+
908
+ def method_missing(mid, *args, &block)
909
+ mid = mid.to_s
910
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
911
+ return case_eq_array_element(*args) if mid.match?(/_array_element$/)
912
+ return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
913
+ super
914
+ end
915
+
916
+ private
917
+
918
+ attr_reader :deconstructed_keys, :predicates
919
+
920
+ # Raise SyntaxError if match-var is used within alternation
921
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
922
+ def check_match_var_alternation!(name)
923
+ return unless locals.key?(ALTERNATION_MARKER)
924
+
925
+ return if name.start_with?("_")
926
+
927
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
928
+ end
929
+
930
+ def deconstructed_key(key)
931
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
932
+
933
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
934
+ end
935
+
936
+ # Unparser generates `do .. end` blocks, we want to
937
+ # have single-line blocks with `{ ... }`.
938
+ def inline_blocks(source)
939
+ source.gsub(/do \|_, __i__\|\n\s*([^\n]+)\n\s*end/, '{ |_, __i__| \1 }')
940
+ end
941
+ end
942
+ end
943
+ end
944
+ end