ruby-next-core 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 3c192e6090c23fb8d462dae6403a3e952f661bd0972f49eaaaba064cb2b07084
4
- data.tar.gz: c72d83029a820636ce4809e727ad385a5d85bffafee94db2a4764f40eccb36f4
3
+ metadata.gz: '0456426952d07bcec1db2c70d256177bd8d1503c8edc7e667e2af62bf5f03ae3'
4
+ data.tar.gz: 4528e483af6cd8cdbf7b613454a0051abd5aaad4ec6cd02c727690d64fbfd848
5
5
  SHA512:
6
- metadata.gz: 26eb5a69b3993cde39121bd19d710d5850f9dc722a807776bdf969b3fc0b52625de05f4f583e10b97fd137bc5c7a108c1c7b4582b8f541d109282c85ed244dc4
7
- data.tar.gz: eed1785c806afd65b681f4b22c98658f0e2b43bc20d534ffc1382c476e600b2715d048e1e5409f65c43f92f9913531c1858b6a842348dd172526e1a2391f7865
6
+ metadata.gz: a25d4e8c619b0644f4bcb0f411d0f0c86d1aa7c343ab0e55b42c4d8adad10b2c5c7072ee13f019c9da29223774f6c34c353f58b64f32caf91e60040ec1b029a2
7
+ data.tar.gz: 02e6e9bdc4379b2542fe6802160f2946ac9d151878bea3f51bcd27da749f7e0bb2cf2891aa0ddf88f5a8221185d8ab9e1bdab4e639de6d69c2a44409e734626c
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## master
4
4
 
5
+ ## 0.4.0 (2020-03-09)
6
+
7
+ - Optimize pattern matching transpiled code. ([@palkan][])
8
+
9
+ For array patterns, transpiled code is ~1.5-2x faster than native.
10
+ For hash patterns it's about the same.
11
+
12
+ - Pattern matching is 100% compatible with RubySpec. ([@palkan][])
13
+
14
+ - Add `Symbol#start_with?/end_with?`. ([@palkan][])
15
+
5
16
  ## 0.3.0 (2020-02-14) 💕
6
17
 
7
18
  - Add `Time#floor` and `Time#ceil`. ([@palkan][])
@@ -48,7 +59,7 @@ You can still use this feature by enabling it explicitly (see Readme).
48
59
  - Support in modifier. ([@palkan][])
49
60
 
50
61
  ```ruby
51
- {a:1, b: 2} in {a:, **}
62
+ {a: 1, b: 2} in {a:, **}
52
63
  p a #=> 1
53
64
  ```
54
65
 
data/README.md CHANGED
@@ -3,9 +3,10 @@
3
3
 
4
4
  # Ruby Next
5
5
 
6
- > Make all Rubies quack like edge Ruby!
6
+ <img align="right" height="184"
7
+ title="Ruby Next logo" src="./assets/images/logo.svg">
7
8
 
8
- Ruby Next is a collection of **polyfills** and a **transpiler** for supporting the latest and upcoming Ruby features (APIs and syntax) in older versions and alternative implementations. For example, you can use pattern matching and `Kernel#then` in Ruby 2.5 or [mruby][].
9
+ Ruby Next is a **transpiler** and a collection of **polyfills** for supporting the latest and upcoming Ruby features (APIs and syntax) in older versions and alternative implementations. For example, you can use pattern matching and `Kernel#then` in Ruby 2.5 or [mruby][].
9
10
 
10
11
  Who might be interested in Ruby Next?
11
12
 
@@ -14,9 +15,10 @@ Who might be interested in Ruby Next?
14
15
  - **Users of non-MRI implementations** such as [mruby][], [JRuby][], [TruffleRuby][], [Opal][], [RubyMotion][], [Artichoke][], [Prism][].
15
16
 
16
17
  Ruby Next also aims to help the community to assess new, _experimental_, MRI features by making it easier to play with them.
17
- That's why Ruby Next implements the `trunk` features as fast as possible.
18
+ That's why Ruby Next implements the `master` features as fast as possible.
18
19
 
19
- _⚡️ The project is in a **beta** phase. That means that the main functionality has been implemented (see [the list][features]) and APIs shouldn't change a lot in the nearest future. On the other hand, the number of users/projects is not enough to say we're "production-ready". So, can't wait to hear your feedback 🙂_
20
+ <a href="https://evilmartians.com/?utm_source=ruby-next">
21
+ <img src="https://evilmartians.com/badges/sponsored-by-evil-martians.svg" alt="Sponsored by Evil Martians" width="236" height="54"></a>
20
22
 
21
23
  ## Links
22
24
 
@@ -92,7 +94,9 @@ gem "ruby-next"
92
94
 
93
95
  # gemspec
94
96
  spec.add_dependency "ruby-next"
97
+ ```
95
98
 
99
+ ```sh
96
100
  # or install globally
97
101
  gem install ruby-next
98
102
  ```
@@ -164,7 +168,7 @@ The behaviour depends on whether you transpile a single file or a directory:
164
168
 
165
169
  ```sh
166
170
  $ ruby-next nextify my_ruby.rb -o my_ruby_next.rb -V
167
- RubyNext core strategy: refine
171
+ Ruby Next core strategy: refine
168
172
  Generated: my_ruby_next.rb
169
173
  ```
170
174
 
@@ -298,7 +302,7 @@ Then, add to your Gemfile:
298
302
 
299
303
  ```ruby
300
304
  source "https://rubygems.pkg.github.com/ruby-next" do
301
- gem "parser", "2.7.0.100"
305
+ gem "parser", "~> 2.7.0.100", "< 2.7.1"
302
306
  end
303
307
 
304
308
  gem "ruby-next"
@@ -311,7 +315,7 @@ gem "ruby-next"
311
315
  You can install `ruby-next` globally by running the following commands:
312
316
 
313
317
  ```sh
314
- gem install parser -v "2.7.0.100" --source "https://USERNAME:ACCESS_TOKEN@rubygems.pkg.github.com/ruby-next"
318
+ gem install parser -v "~> 2.7.0.100" -v "< 2.7.1" --source "https://USERNAME:ACCESS_TOKEN@rubygems.pkg.github.com/ruby-next"
315
319
  gem install ruby-next
316
320
  ```
317
321
 
@@ -144,7 +144,7 @@ module RubyNext
144
144
 
145
145
  def generation_meta
146
146
  <<~MSG
147
- # Generated by RubyNext v#{RubyNext::VERSION} using the following command:
147
+ # Generated by Ruby Next v#{RubyNext::VERSION} using the following command:
148
148
  #
149
149
  # #{original_command}
150
150
  #
@@ -152,6 +152,9 @@ require_relative "core/hash/merge"
152
152
 
153
153
  require_relative "core/string/split"
154
154
 
155
+ require_relative "core/symbol/start_with"
156
+ require_relative "core/symbol/end_with"
157
+
155
158
  require_relative "core/unboundmethod/bind_call"
156
159
 
157
160
  require_relative "core/time/floor"
@@ -8,8 +8,6 @@ RubyNext::Core.patch Struct, method: :deconstruct_keys, version: "2.7" do
8
8
 
9
9
  return to_h unless keys
10
10
 
11
- return {} if size < keys.size
12
-
13
11
  keys.each_with_object({}) do |k, acc|
14
12
  # if k is Symbol and not a member of a Struct return {}
15
13
  next if (Symbol === k || String === k) && !members.include?(k.to_sym)
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Symbol, method: :end_with?, version: "2.7" do
4
+ <<~RUBY
5
+ def end_with?(*prefixes)
6
+ to_s.end_with?(*prefixes)
7
+ end
8
+ RUBY
9
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Symbol, method: :start_with?, version: "2.7" do
4
+ <<~RUBY
5
+ def start_with?(*prefixes)
6
+ to_s.start_with?(*prefixes)
7
+ end
8
+ RUBY
9
+ end
@@ -31,6 +31,190 @@ module RubyNext
31
31
  end
32
32
  end)
33
33
 
34
+ # We can memoize structural predicates to avoid double calculation.
35
+ #
36
+ # For example, consider the following case and the corresponding predicate chains:
37
+ #
38
+ # case val
39
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
40
+ # in [:created, 201] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
41
+ # in [401 | 403] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_1]
42
+ # end
43
+ #
44
+ # We can minimize the number of predicate calls by storing the intermediate values (prefixed with `p_`) and using them
45
+ # in the subsequent calls:
46
+ #
47
+ # case val
48
+ # in [:ok, 200] #=> [:respond_to_deconstruct, :deconstruct_type, :arr_size_is_2]
49
+ # in [:created, 201] #=> [:p_deconstructed, :p_arr_size_2]
50
+ # in [401 | 403] #=> [:p_deconstructed, :arr_size_is_1]
51
+ # end
52
+ #
53
+ # This way we mimic a naive decision tree algorithim.
54
+ module Predicates
55
+ class Processor < ::Parser::TreeRewriter
56
+ attr_reader :predicates
57
+
58
+ def initialize(predicates)
59
+ @predicates = predicates
60
+ super()
61
+ end
62
+
63
+ def on_lvasgn(node)
64
+ lvar, val = *node.children
65
+ if predicates.store[lvar] == false
66
+ process(val)
67
+ else
68
+ node
69
+ end
70
+ end
71
+
72
+ def on_and(node)
73
+ left, right = *node.children
74
+
75
+ if truthy(left)
76
+ process(right)
77
+ elsif truthy(right)
78
+ process(left)
79
+ else
80
+ node.updated(
81
+ :and,
82
+ [
83
+ process(left),
84
+ process(right)
85
+ ]
86
+ )
87
+ end
88
+ end
89
+
90
+ private
91
+
92
+ def truthy(node)
93
+ return false unless node.is_a?(::Parser::AST::Node)
94
+ return true if node.type == :true
95
+ return false if node.children.empty?
96
+
97
+ node.children.all? { |child| truthy(child) }
98
+ end
99
+ end
100
+
101
+ class Base
102
+ attr_reader :store, :predicates_by_path, :count, :terminated, :current_path
103
+ alias terminated? terminated
104
+
105
+ def initialize
106
+ # total number of predicates
107
+ @count = 0
108
+ # cache of all predicates by path
109
+ @predicates_by_path = {}
110
+ # all predicates and their dirty state
111
+ @store = {}
112
+
113
+ @current_path = []
114
+ end
115
+
116
+ def reset!
117
+ @current_path = []
118
+ @terminated = false
119
+ end
120
+
121
+ def push(path)
122
+ current_path << path
123
+ end
124
+
125
+ def pop
126
+ current_path.pop
127
+ end
128
+
129
+ def terminate!
130
+ @terminated = true
131
+ end
132
+
133
+ def predicate_clause(name, node)
134
+ if pred?(name)
135
+ read_pred(name)
136
+ else
137
+ write_pred(name, node)
138
+ end
139
+ end
140
+
141
+ def pred?(name)
142
+ predicates_by_path.key?(current_path + [name])
143
+ end
144
+
145
+ def read_pred(name)
146
+ lvar = predicates_by_path.fetch(current_path + [name])
147
+ # mark as used
148
+ store[lvar] = true
149
+ s(:lvar, lvar)
150
+ end
151
+
152
+ def write_pred(name, node)
153
+ return node if terminated?
154
+ @count += 1
155
+ lvar = :"__p_#{count}__"
156
+ predicates_by_path[current_path + [name]] = lvar
157
+ store[lvar] = false
158
+
159
+ s(:lvasgn,
160
+ lvar,
161
+ node)
162
+ end
163
+
164
+ def process(ast)
165
+ Processor.new(self).process(ast)
166
+ end
167
+
168
+ private
169
+
170
+ def s(type, *children)
171
+ ::Parser::AST::Node.new(type, children)
172
+ end
173
+ end
174
+
175
+ # rubocop:disable Style/MethodMissingSuper
176
+ # rubocop:disable Style/MissingRespondToMissing
177
+ class Noop < Base
178
+ # Return node itself, no memoization
179
+ def method_missing(mid, node, *)
180
+ node
181
+ end
182
+ end
183
+ # rubocop:enable Style/MethodMissingSuper
184
+ # rubocop:enable Style/MissingRespondToMissing
185
+
186
+ class CaseIn < Base
187
+ def const(node, const)
188
+ node
189
+ end
190
+
191
+ def respond_to_deconstruct(node)
192
+ predicate_clause(:respond_to_deconstruct, node)
193
+ end
194
+
195
+ def array_size(node, size)
196
+ predicate_clause(:"array_size_#{size}", node)
197
+ end
198
+
199
+ def array_deconstructed(node)
200
+ predicate_clause(:array_deconstructed, node)
201
+ end
202
+
203
+ def hash_deconstructed(node, keys)
204
+ predicate_clause(:"hash_deconstructed_#{keys.join("_p_")}", node)
205
+ end
206
+
207
+ def respond_to_deconstruct_keys(node)
208
+ predicate_clause(:respond_to_deconstruct_keys, node)
209
+ end
210
+
211
+ def hash_key(node, key)
212
+ key = key.children.first if key.is_a?(::Parser::AST::Node)
213
+ predicate_clause(:"hash_key_#{key}", node)
214
+ end
215
+ end
216
+ end
217
+
34
218
  class PatternMatching < Base
35
219
  SYNTAX_PROBE = "case 0; in 0; true; else; 1; end"
36
220
  MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
@@ -39,15 +223,19 @@ module RubyNext
39
223
  MATCHEE_ARR = :__m_arr__
40
224
  MATCHEE_HASH = :__m_hash__
41
225
 
226
+ ALTERNATION_MARKER = :__alt__
227
+ CURRENT_HASH_KEY = :__chk__
228
+
42
229
  def on_case_match(node)
43
230
  context.track! self
44
231
 
45
- @deconstructed = []
232
+ @deconstructed_keys = {}
233
+ @predicates = Predicates::CaseIn.new
46
234
 
47
235
  matchee_ast =
48
236
  s(:lvasgn, MATCHEE, node.children[0])
49
237
 
50
- ifs_ast = locals.with(
238
+ patterns = locals.with(
51
239
  matchee: MATCHEE,
52
240
  arr: MATCHEE_ARR,
53
241
  hash: MATCHEE_HASH
@@ -55,10 +243,13 @@ module RubyNext
55
243
  build_if_clause(node.children[1], node.children[2..-1])
56
244
  end
57
245
 
246
+ # remove unused predicate assignments and truthy expressions
247
+ patterns = predicates.process(patterns)
248
+
58
249
  node.updated(
59
250
  :begin,
60
251
  [
61
- matchee_ast, ifs_ast
252
+ matchee_ast, patterns
62
253
  ]
63
254
  )
64
255
  end
@@ -66,6 +257,9 @@ module RubyNext
66
257
  def on_in_match(node)
67
258
  context.track! self
68
259
 
260
+ @deconstructed_keys = {}
261
+ @predicates = Predicates::Noop.new
262
+
69
263
  matchee =
70
264
  s(:lvasgn, MATCHEE, node.children[0])
71
265
 
@@ -98,11 +292,15 @@ module RubyNext
98
292
 
99
293
  def build_if_clause(node, rest)
100
294
  if node&.type == :in_pattern
295
+ predicates.reset!
101
296
  build_in_pattern(node, rest)
102
297
  else
103
298
  raise "Unexpected else in the middle of case ... in" if rest && rest.size > 0
104
299
  # else clause must be present
105
- node || no_matching_pattern
300
+ (process(node) || no_matching_pattern).then do |else_node|
301
+ next else_node unless else_node.type == :empty_else
302
+ s(:empty)
303
+ end
106
304
  end
107
305
  end
108
306
 
@@ -115,7 +313,7 @@ module RubyNext
115
313
  ),
116
314
  clause.children[1] # guard
117
315
  ),
118
- clause.children[2] || s(:nil) # expression
316
+ process(clause.children[2] || s(:nil)) # expression
119
317
  ].then do |children|
120
318
  if rest && rest.size > 0
121
319
  children << build_if_clause(rest.first, rest[1..-1])
@@ -125,10 +323,10 @@ module RubyNext
125
323
  end
126
324
  end
127
325
 
128
- def const_pattern_clause(node)
326
+ def const_pattern_clause(node, right = s(:lvar, locals[:matchee]))
129
327
  const, pattern = *node.children
130
328
 
131
- case_eq_clause(const).then do |node|
329
+ predicates.const(case_eq_clause(const, right), const).then do |node|
132
330
  next node if pattern.nil?
133
331
 
134
332
  s(:and,
@@ -138,37 +336,58 @@ module RubyNext
138
336
  end
139
337
 
140
338
  def match_alt_clause(node)
141
- children = node.children.map do |child|
142
- send :"#{child.type}_clause", child
339
+ children = locals.with(ALTERNATION_MARKER => true) do
340
+ node.children.map.with_index do |child, i|
341
+ predicates.terminate! if i == 1
342
+ send :"#{child.type}_clause", child
343
+ end
143
344
  end
144
345
  s(:or, *children)
145
346
  end
146
347
 
147
348
  def match_as_clause(node, right = s(:lvar, locals[:matchee]))
148
349
  s(:and,
149
- case_eq_clause(node.children[0]),
350
+ send(:"#{node.children[0].type}_clause", node.children[0], right),
150
351
  match_var_clause(node.children[1], right))
151
352
  end
152
353
 
153
354
  def match_var_clause(node, left = s(:lvar, locals[:matchee]))
355
+ return s(:true) if node.children[0] == :_
356
+
357
+ check_match_var_alternation! node.children[0]
358
+
154
359
  s(:or,
155
360
  s(:lvasgn, node.children[0], left),
156
- s(:true)) # rubocop:disable Lint/BooleanSymbol
361
+ s(:true))
157
362
  end
158
363
 
159
- def pin_clause(node)
160
- case_eq_clause node.children[0]
364
+ def pin_clause(node, right = s(:lvar, locals[:matchee]))
365
+ predicates.terminate!
366
+ case_eq_clause node.children[0], right
161
367
  end
162
368
 
163
369
  def case_eq_clause(node, right = s(:lvar, locals[:matchee]))
370
+ predicates.terminate!
164
371
  s(:send,
165
- node, :===, right)
372
+ process(node), :===, right)
166
373
  end
167
374
 
168
375
  #=========== ARRAY PATTERN (START) ===============
169
376
 
170
377
  def array_pattern_clause(node, matchee = s(:lvar, locals[:matchee]))
171
378
  deconstruct_node(matchee).then do |dnode|
379
+ size_check = nil
380
+ # if there is no rest or tail, match the size first
381
+ unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
382
+ size_check = predicates.array_size(
383
+ s(:send,
384
+ node.children.size.to_ast_node,
385
+ :==,
386
+ s(:send, s(:lvar, locals[:arr]), :size)),
387
+ node.children.size
388
+ )
389
+ end
390
+
172
391
  right =
173
392
  if node.children.empty?
174
393
  case_eq_clause(s(:array), s(:lvar, locals[:arr]))
@@ -176,49 +395,36 @@ module RubyNext
176
395
  array_element(0, *node.children)
177
396
  end
178
397
 
179
- # already deconsrtructed
180
- next right if dnode.nil?
181
-
182
- # if there is no rest or tail, match the size first
183
- unless node.type == :array_pattern_with_tail || node.children.any? { |n| n.type == :match_rest }
184
- right =
185
- s(:and,
186
- s(:send,
187
- node.children.size.to_ast_node,
188
- :==,
189
- s(:send, s(:lvar, locals[:arr]), :size)),
190
- right)
191
- end
398
+ right = s(:and, size_check, right) if size_check
192
399
 
193
400
  s(:and,
194
401
  dnode,
195
402
  right)
196
- end.then do |right|
197
- s(:and,
198
- respond_to_check(matchee, :deconstruct),
199
- right)
200
403
  end
201
404
  end
202
405
 
203
406
  alias array_pattern_with_tail_clause array_pattern_clause
204
407
 
205
408
  def deconstruct_node(matchee)
206
- # only deconstruct once per case
207
- return if deconstructed&.include?(locals[:arr])
208
-
209
409
  context.use_ruby_next!
210
410
 
411
+ # we do not memoize respond_to_check for arrays, 'cause
412
+ # we can memoize is together with #deconstruct result
413
+ respond_check = respond_to_check(matchee, :deconstruct)
211
414
  right = s(:send, matchee, :deconstruct)
212
415
 
213
- deconstructed << locals[:arr] if deconstructed
214
-
215
- s(:and,
216
- s(:or,
217
- s(:lvasgn, locals[:arr], right),
218
- s(:true)), # rubocop:disable Lint/BooleanSymbol
219
- s(:or,
220
- case_eq_clause(s(:const, nil, :Array), s(:lvar, locals[:arr])),
221
- raise_error(:TypeError, "#deconstruct must return Array")))
416
+ predicates.array_deconstructed(
417
+ s(:and,
418
+ respond_check,
419
+ s(:and,
420
+ s(:or,
421
+ s(:lvasgn, locals[:arr], right),
422
+ s(:true)),
423
+ s(:or,
424
+ s(:send,
425
+ s(:const, nil, :Array), :===, s(:lvar, locals[:arr])),
426
+ raise_error(:TypeError, "#deconstruct must return Array"))))
427
+ )
222
428
  end
223
429
 
224
430
  def array_element(index, head, *tail)
@@ -237,6 +443,7 @@ module RubyNext
237
443
  child = node.children[0]
238
444
  rest = arr_rest_items(index, tail.size).then do |r|
239
445
  next r unless child
446
+
240
447
  match_var_clause(
241
448
  child,
242
449
  r
@@ -263,14 +470,16 @@ module RubyNext
263
470
  def array_pattern_array_element(node, index)
264
471
  element = arr_item_at(index)
265
472
  locals.with(arr: locals[:arr, index]) do
266
- array_pattern_clause(node, element)
473
+ predicates.push :"i#{index}"
474
+ array_pattern_clause(node, element).tap { predicates.pop }
267
475
  end
268
476
  end
269
477
 
270
478
  def hash_pattern_array_element(node, index)
271
479
  element = arr_item_at(index)
272
480
  locals.with(hash: locals[:arr, index]) do
273
- hash_pattern_clause(node, element)
481
+ predicates.push :"i#{index}"
482
+ hash_pattern_clause(node, element).tap { predicates.pop }
274
483
  end
275
484
  end
276
485
 
@@ -318,29 +527,42 @@ module RubyNext
318
527
  # (we use #dup and #delete when "reading" values when **rest is present
319
528
  # to assign the rest of the hash copy to it)
320
529
  @hash_match_rest = node.children.any? { |child| child.type == :match_rest || child.type == :match_nil_pattern }
321
- keys = hash_pattern_keys(node.children)
530
+ keys = hash_pattern_destruction_keys(node.children)
531
+
532
+ specified_key_names = hash_pattern_keys(node.children)
322
533
 
323
534
  deconstruct_keys_node(keys, matchee).then do |dnode|
324
535
  right =
325
536
  if node.children.empty?
326
537
  case_eq_clause(s(:hash), s(:lvar, locals[:hash]))
327
- else
538
+ elsif specified_key_names.empty?
328
539
  hash_element(*node.children)
540
+ else
541
+ s(:and,
542
+ having_hash_keys(specified_key_names),
543
+ hash_element(*node.children))
329
544
  end
330
545
 
546
+ predicates.pop
547
+
331
548
  next dnode if right.nil?
332
549
 
333
550
  s(:and,
334
551
  dnode,
335
552
  right)
336
- end.then do |right|
337
- s(:and,
338
- respond_to_check(matchee, :deconstruct_keys),
339
- right)
340
553
  end
341
554
  end
342
555
 
343
556
  def hash_pattern_keys(children)
557
+ children.filter_map do |child|
558
+ # Skip ** without var
559
+ next if child.type == :match_rest || child.type == :match_nil_pattern
560
+
561
+ send("#{child.type}_hash_key", child)
562
+ end
563
+ end
564
+
565
+ def hash_pattern_destruction_keys(children)
344
566
  return s(:nil) if children.empty?
345
567
 
346
568
  children.filter_map do |child|
@@ -357,51 +579,73 @@ module RubyNext
357
579
  end
358
580
 
359
581
  def match_var_hash_key(node)
582
+ check_match_var_alternation! node.children[0]
583
+
360
584
  s(:sym, node.children[0])
361
585
  end
362
586
 
363
587
  def deconstruct_keys_node(keys, matchee = s(:lvar, locals[:matchee]))
364
- # Deconstruct once and use a copy of the hash for each pattern if we need **rest.
588
+ # Use original hash returned by #deconstruct_keys if not **rest matching,
589
+ # 'cause it remains immutable
590
+ deconstruct_name = @hash_match_rest ? locals[:hash, :src] : locals[:hash]
591
+
592
+ # Duplicate the source hash when matching **rest, 'cause we mutate it
365
593
  hash_dup =
366
594
  if @hash_match_rest
367
595
  s(:lvasgn, locals[:hash], s(:send, s(:lvar, locals[:hash, :src]), :dup))
368
596
  else
369
- s(:lvasgn, locals[:hash], s(:lvar, locals[:hash, :src]))
597
+ s(:true)
370
598
  end
371
599
 
372
- # Create a copy of the original hash if already deconstructed
373
- # FIXME: need better algorithm to handle different key sets
374
- # return hash_dup if deconstructed&.include?(locals[:hash])
375
-
376
600
  context.use_ruby_next!
377
601
 
378
- deconstructed << locals[:hash] if deconstructed
602
+ respond_to_checked = predicates.pred?(:respond_to_deconstruct_keys)
603
+ respond_check = predicates.respond_to_deconstruct_keys(respond_to_check(matchee, :deconstruct_keys))
379
604
 
380
- right = s(:send,
381
- matchee, :deconstruct_keys, keys)
605
+ key_names = keys.children.map { |node| node.children.last }
606
+ predicates.push locals[:hash]
607
+
608
+ s(:lvasgn, deconstruct_name,
609
+ s(:send,
610
+ matchee, :deconstruct_keys, keys)).then do |dnode|
611
+ next dnode if respond_to_checked
612
+
613
+ s(:and,
614
+ respond_check,
615
+ s(:and,
616
+ s(:or,
617
+ dnode,
618
+ s(:true)),
619
+ s(:or,
620
+ s(:send,
621
+ s(:const, nil, :Hash), :===, s(:lvar, deconstruct_name)),
622
+ raise_error(:TypeError, "#deconstruct_keys must return Hash"))))
623
+ end.then do |dnode|
624
+ predicates.hash_deconstructed(dnode, key_names)
625
+ end.then do |dnode|
626
+ next dnode unless @hash_match_rest
382
627
 
383
- s(:and,
384
- s(:or,
385
- s(:lvasgn, locals[:hash, :src], right),
386
- s(:true)), # rubocop:disable Lint/BooleanSymbol
387
628
  s(:and,
388
- s(:or,
389
- case_eq_clause(s(:const, nil, :Hash), s(:lvar, locals[:hash, :src])),
390
- raise_error(:TypeError, "#deconstruct_keys must return Hash")),
391
- hash_dup))
629
+ dnode,
630
+ hash_dup)
631
+ end
392
632
  end
393
633
 
394
634
  def hash_pattern_hash_element(node, key)
395
635
  element = hash_value_at(key)
396
- locals.with(hash: locals[:hash, deconstructed.size]) do
397
- hash_pattern_clause(node, element)
636
+ key_index = deconstructed_key(key)
637
+ locals.with(hash: locals[:hash, key_index]) do
638
+ predicates.push :"k#{key_index}"
639
+ hash_pattern_clause(node, element).tap { predicates.pop }
398
640
  end
399
641
  end
400
642
 
401
643
  def array_pattern_hash_element(node, key)
402
644
  element = hash_value_at(key)
403
- locals.with(arr: locals[:hash, deconstructed.size]) do
404
- array_pattern_clause(node, element)
645
+ key_index = deconstructed_key(key)
646
+ locals.with(arr: locals[:hash, key_index]) do
647
+ predicates.push :"k#{key_index}"
648
+ array_pattern_clause(node, element).tap { predicates.pop }
405
649
  end
406
650
  end
407
651
 
@@ -436,16 +680,17 @@ module RubyNext
436
680
  s(:and,
437
681
  s(:or,
438
682
  element_node,
439
- s(:true)), # rubocop:disable Lint/BooleanSymbol
683
+ s(:true)),
440
684
  s(:or, *children))
441
685
  end
442
686
 
687
+ def match_as_hash_element(node, key)
688
+ match_as_clause(node, hash_value_at(key))
689
+ end
690
+
443
691
  def match_var_hash_element(node, key = nil)
444
692
  key ||= node.children[0]
445
- # We need to check whether key is present first
446
- s(:and,
447
- hash_has_key(key),
448
- match_var_clause(node, hash_value_at(key)))
693
+ match_var_clause(node, hash_value_at(key))
449
694
  end
450
695
 
451
696
  def match_nil_pattern_hash_element(node, _key = nil)
@@ -489,6 +734,17 @@ module RubyNext
489
734
  key.to_ast_node)
490
735
  end
491
736
 
737
+ def having_hash_keys(keys, hash = s(:lvar, locals[:hash]))
738
+ key = keys.shift
739
+ node = predicates.hash_key(hash_has_key(key, hash), key)
740
+
741
+ keys.reduce(node) do |res, key|
742
+ s(:and,
743
+ res,
744
+ predicates.hash_key(hash_has_key(key, hash), key))
745
+ end
746
+ end
747
+
492
748
  #=========== HASH PATTERN (END) ===============
493
749
 
494
750
  def with_guard(node, guard)
@@ -516,6 +772,7 @@ module RubyNext
516
772
  msg.to_ast_node)
517
773
  end
518
774
 
775
+ # Add respond_to? check
519
776
  def respond_to_check(node, mid)
520
777
  s(:send, node, :respond_to?, mid.to_ast_node)
521
778
  end
@@ -526,7 +783,7 @@ module RubyNext
526
783
  end
527
784
 
528
785
  def method_missing(mid, *args, &block)
529
- return case_eq_clause(args.first) if mid.match?(/_clause$/)
786
+ return case_eq_clause(*args) if mid.match?(/_clause$/)
530
787
  return case_eq_array_element(*args) if mid.match?(/_array_element$/)
531
788
  return case_eq_hash_element(*args) if mid.match?(/_hash_element$/)
532
789
  super
@@ -534,7 +791,23 @@ module RubyNext
534
791
 
535
792
  private
536
793
 
537
- attr_reader :deconstructed
794
+ attr_reader :deconstructed_keys, :predicates
795
+
796
+ # Raise SyntaxError if match-var is used within alternation
797
+ # https://github.com/ruby/ruby/blob/672213ef1ca2b71312084057e27580b340438796/compile.c#L5900
798
+ def check_match_var_alternation!(name)
799
+ return unless locals.key?(ALTERNATION_MARKER)
800
+
801
+ return if name.start_with?("_")
802
+
803
+ raise ::SyntaxError, "illegal variable in alternative pattern (#{name})"
804
+ end
805
+
806
+ def deconstructed_key(key)
807
+ return deconstructed_keys[key] if deconstructed_keys.key?(key)
808
+
809
+ deconstructed_keys[key] = :"k#{deconstructed_keys.size}"
810
+ end
538
811
  end
539
812
  end
540
813
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module RubyNext
4
- VERSION = "0.3.0"
4
+ VERSION = "0.4.0"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ruby-next-core
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vladimir Dementyev
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-02-15 00:00:00.000000000 Z
11
+ date: 2020-03-09 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: parser
@@ -76,6 +76,8 @@ files:
76
76
  - lib/ruby-next/core/string/split.rb
77
77
  - lib/ruby-next/core/struct/deconstruct.rb
78
78
  - lib/ruby-next/core/struct/deconstruct_keys.rb
79
+ - lib/ruby-next/core/symbol/end_with.rb
80
+ - lib/ruby-next/core/symbol/start_with.rb
79
81
  - lib/ruby-next/core/time/ceil.rb
80
82
  - lib/ruby-next/core/time/floor.rb
81
83
  - lib/ruby-next/core/unboundmethod/bind_call.rb