ruby-next-core 0.2.0 → 0.3.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 (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +69 -7
  4. data/bin/ruby-next +1 -1
  5. data/lib/ruby-next/cli.rb +45 -6
  6. data/lib/ruby-next/commands/core_ext.rb +166 -0
  7. data/lib/ruby-next/commands/nextify.rb +18 -3
  8. data/lib/ruby-next/core.rb +149 -1
  9. data/lib/ruby-next/core/array/deconstruct.rb +21 -0
  10. data/lib/ruby-next/core/array/difference_union_intersection.rb +15 -21
  11. data/lib/ruby-next/core/constants/no_matching_pattern_error.rb +17 -0
  12. data/lib/ruby-next/core/enumerable/filter.rb +20 -18
  13. data/lib/ruby-next/core/enumerable/filter_map.rb +28 -40
  14. data/lib/ruby-next/core/enumerable/tally.rb +9 -23
  15. data/lib/ruby-next/core/enumerator/produce.rb +12 -14
  16. data/lib/ruby-next/core/hash/deconstruct_keys.rb +21 -0
  17. data/lib/ruby-next/core/hash/merge.rb +8 -10
  18. data/lib/ruby-next/core/kernel/then.rb +7 -9
  19. data/lib/ruby-next/core/proc/compose.rb +12 -14
  20. data/lib/ruby-next/core/string/split.rb +11 -0
  21. data/lib/ruby-next/core/struct/deconstruct.rb +7 -0
  22. data/lib/ruby-next/core/struct/deconstruct_keys.rb +34 -0
  23. data/lib/ruby-next/core/time/ceil.rb +10 -0
  24. data/lib/ruby-next/core/time/floor.rb +9 -0
  25. data/lib/ruby-next/core/unboundmethod/bind_call.rb +9 -0
  26. data/lib/ruby-next/core_ext.rb +18 -0
  27. data/lib/ruby-next/language.rb +4 -2
  28. data/lib/ruby-next/language/parser.rb +5 -1
  29. data/lib/ruby-next/language/rewriters/base.rb +2 -2
  30. data/lib/ruby-next/language/rewriters/method_reference.rb +3 -1
  31. data/lib/ruby-next/language/rewriters/numbered_params.rb +2 -2
  32. data/lib/ruby-next/language/rewriters/pattern_matching.rb +36 -17
  33. data/lib/ruby-next/language/runtime.rb +2 -3
  34. data/lib/ruby-next/version.rb +1 -1
  35. metadata +13 -3
  36. data/lib/ruby-next/core/pattern_matching.rb +0 -37
@@ -1,8 +1,121 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "set"
4
+
3
5
  module RubyNext
4
6
  module Core
7
+ # Patch contains the extension implementation
8
+ # and meta information (e.g., Ruby version).
9
+ class Patch
10
+ attr_reader :refineables, :name, :mod, :method_name, :version, :body, :singleton, :core_ext, :supported, :native, :location
11
+
12
+ # Create a new patch for module/class (mod)
13
+ # with the specified uniq name
14
+ #
15
+ # `core_ext` defines the strategy for core extensions:
16
+ # - :patch — extend class directly
17
+ # - :prepend — extend class by prepending a module (e.g., when needs `super`)
18
+ def initialize(mod = nil, method:, name: nil, version:, supported: nil, native: nil, location: nil, refineable: mod, core_ext: :patch, singleton: nil)
19
+ @mod = mod
20
+ @method_name = method
21
+ @version = version
22
+ if method_name && mod
23
+ @supported = supported.nil? ? mod.method_defined?(method_name) : supported
24
+ # define whether running Ruby has a native implementation for this method
25
+ # for that, we check the source_location (which is nil for C defined methods)
26
+ @native = native.nil? ? (supported? && mod.instance_method(method_name).source_location.nil?) : native
27
+ end
28
+ @singleton = singleton
29
+ @refineables = Array(refineable)
30
+ @body = yield
31
+ @core_ext = core_ext
32
+ @location = location || build_location(caller_locations(1, 5))
33
+ @name = name || build_module_name
34
+ end
35
+
36
+ def prepend?
37
+ core_ext == :prepend
38
+ end
39
+
40
+ def core_ext?
41
+ !mod.nil?
42
+ end
43
+
44
+ alias supported? supported
45
+ alias native? native
46
+ alias singleton? singleton
47
+
48
+ def to_module
49
+ Module.new.tap do |ext|
50
+ ext.module_eval(body, *location)
51
+
52
+ RubyNext::Core.const_set(name, ext)
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ def build_module_name
59
+ mod_name = singleton? ? singleton.name : mod.name
60
+ camelized_method_name = method_name.to_s.split("_").map(&:capitalize).join
61
+
62
+ "#{mod_name}#{camelized_method_name}".gsub(/\W/, "")
63
+ end
64
+
65
+ def build_location(trace_locations)
66
+ # The caller_locations behaviour depends on implementaion,
67
+ # e.g. in JRuby https://github.com/jruby/jruby/issues/6055
68
+ while trace_locations.first.label != "patch"
69
+ trace_locations.shift
70
+ end
71
+
72
+ trace_location = trace_locations[1]
73
+
74
+ [trace_location.absolute_path, trace_location.lineno + 2]
75
+ end
76
+ end
77
+
78
+ # Registry for patches
79
+ class Patches
80
+ attr_reader :extensions, :refined
81
+
82
+ def initialize
83
+ @names = Set.new
84
+ @extensions = Hash.new { |h, k| h[k] = [] }
85
+ @refined = Hash.new { |h, k| h[k] = [] }
86
+ end
87
+
88
+ # Register new patch
89
+ def <<(patch)
90
+ raise ArgumentError, "Patch already registered: #{patch.name}" if @names.include?(patch.name)
91
+ @names << patch.name
92
+ @extensions[patch.mod] << patch if patch.core_ext?
93
+ patch.refineables.each { |r| @refined[r] << patch } unless patch.native?
94
+ end
95
+ end
96
+
5
97
  class << self
98
+ STRATEGIES = %i[refine core_ext].freeze
99
+
100
+ attr_reader :strategy
101
+
102
+ def strategy=(val)
103
+ raise ArgumentError, "Unknown strategy: #{val}. Available: #{STRATEGIES.join(",")}" unless STRATEGIES.include?(val)
104
+ @strategy = val
105
+ end
106
+
107
+ def refine?
108
+ strategy == :refine
109
+ end
110
+
111
+ def core_ext?
112
+ strategy == :core_ext
113
+ end
114
+
115
+ def patch(*args, **kwargs, &block)
116
+ patches << Patch.new(*args, **kwargs, &block)
117
+ end
118
+
6
119
  # Inject `using RubyNext` at the top of the source code
7
120
  def inject!(contents)
8
121
  if contents.frozen?
@@ -12,7 +125,14 @@ module RubyNext
12
125
  end
13
126
  contents
14
127
  end
128
+
129
+ def patches
130
+ @patches ||= Patches.new
131
+ end
15
132
  end
133
+
134
+ # Use refinements by default
135
+ self.strategy = :refine
16
136
  end
17
137
  end
18
138
 
@@ -30,5 +150,33 @@ require_relative "core/array/difference_union_intersection"
30
150
 
31
151
  require_relative "core/hash/merge"
32
152
 
153
+ require_relative "core/string/split"
154
+
155
+ require_relative "core/unboundmethod/bind_call"
156
+
157
+ require_relative "core/time/floor"
158
+ require_relative "core/time/ceil"
159
+
33
160
  # Core extensions required for pattern matching
34
- require_relative "core/pattern_matching"
161
+ # Required for pattern matching with refinements
162
+ unless defined?(NoMatchingPatternError)
163
+ class NoMatchingPatternError < RuntimeError
164
+ end
165
+ end
166
+
167
+ require_relative "core/constants/no_matching_pattern_error"
168
+ require_relative "core/array/deconstruct"
169
+ require_relative "core/hash/deconstruct_keys"
170
+ require_relative "core/struct/deconstruct"
171
+ require_relative "core/struct/deconstruct_keys"
172
+
173
+ # Generate refinements
174
+ RubyNext.module_eval do
175
+ RubyNext::Core.patches.refined.each do |mod, patches|
176
+ refine mod do
177
+ patches.each do |patch|
178
+ module_eval(patch.body, *patch.location)
179
+ end
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ RubyNext::Core.patch Array, method: :deconstruct, version: "2.7" do
4
+ <<~RUBY
5
+ def deconstruct
6
+ self
7
+ end
8
+ RUBY
9
+ end
10
+
11
+ # We need to hack `respond_to?` in Ruby 2.5, since it's not working with refinements
12
+ if Gem::Version.new(RUBY_VERSION) < Gem::Version.new("2.6")
13
+ RubyNext::Core.patch refineable: Array, name: "ArrayRespondToDeconstruct", method: :deconstruct, version: "2.7" do
14
+ <<~RUBY
15
+ def respond_to?(mid, *)
16
+ return true if mid == :deconstruct
17
+ super
18
+ end
19
+ RUBY
20
+ end
21
+ end
@@ -1,31 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless [].respond_to?(:union)
4
- RubyNext.module_eval do
5
- refine Array do
6
- def union(*others)
7
- others.reduce(Array.new(self).uniq) { |acc, arr| acc | arr }
8
- end
3
+ RubyNext::Core.patch Array, method: :union, version: "2.6" do
4
+ <<~RUBY
5
+ def union(*others)
6
+ others.reduce(Array.new(self).uniq) { |acc, arr| acc | arr }
9
7
  end
10
- end
8
+ RUBY
11
9
  end
12
10
 
13
- unless [].respond_to?(:difference)
14
- RubyNext.module_eval do
15
- refine Array do
16
- def difference(*others)
17
- others.reduce(Array.new(self)) { |acc, arr| acc - arr }
18
- end
11
+ RubyNext::Core.patch Array, method: :difference, version: "2.6" do
12
+ <<~RUBY
13
+ def difference(*others)
14
+ others.reduce(Array.new(self)) { |acc, arr| acc - arr }
19
15
  end
20
- end
16
+ RUBY
21
17
  end
22
18
 
23
- unless [].respond_to?(:intersection)
24
- RubyNext.module_eval do
25
- refine Array do
26
- def intersection(*others)
27
- others.reduce(Array.new(self)) { |acc, arr| acc & arr }
28
- end
19
+ RubyNext::Core.patch Array, method: :intersection, version: "2.7" do
20
+ <<~RUBY
21
+ def intersection(*others)
22
+ others.reduce(Array.new(self)) { |acc, arr| acc & arr }
29
23
  end
30
- end
24
+ RUBY
31
25
  end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Special patch to define the error constant in generated files
4
+ RubyNext::Core.patch Object,
5
+ name: "NoMatchingPatternError",
6
+ method: nil,
7
+ refineable: [],
8
+ version: "2.7",
9
+ # avoid defining the constant twice, 'causae it's already included in core
10
+ # we only use the contents in `ruby-next core_ext`.
11
+ supported: true,
12
+ location: [__FILE__, __LINE__ + 2] do
13
+ <<~RUBY
14
+ class NoMatchingPatternError < RuntimeError
15
+ end
16
+ RUBY
17
+ end
@@ -1,23 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless [].respond_to?(:filter)
4
- RubyNext.module_eval do
5
- refine Enumerable do
6
- alias filter select
7
- end
3
+ RubyNext::Core.patch Enumerable, method: :filter, version: "2.6" do
4
+ <<~RUBY
5
+ alias filter select
6
+ RUBY
7
+ end
8
8
 
9
- # Refine Array seprately, 'cause refining modules is vulnerable to prepend:
10
- # - https://bugs.ruby-lang.org/issues/13446
11
- #
12
- # Also, Array also have `filter!`
13
- refine Array do
14
- alias filter select
15
- alias filter! select!
16
- end
9
+ # Refine Array seprately, 'cause refining modules is vulnerable to prepend:
10
+ # - https://bugs.ruby-lang.org/issues/13446
11
+ #
12
+ # Also, Array also have `filter!`
13
+ RubyNext::Core.patch Array, method: :filter!, version: "2.6" do
14
+ <<~RUBY
15
+ alias filter select
16
+ alias filter! select!
17
+ RUBY
18
+ end
17
19
 
18
- refine Hash do
19
- alias filter select
20
- alias filter! select!
21
- end
22
- end
20
+ RubyNext::Core.patch Hash, method: :filter!, version: "2.6" do
21
+ <<~RUBY
22
+ alias filter select
23
+ alias filter! select!
24
+ RUBY
23
25
  end
@@ -1,50 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless [].respond_to?(:filter_map)
4
- module RubyNext
5
- module Core
6
- module EnumerableFilterMap
7
- def filter_map
8
- if block_given?
9
- result = []
10
- each do |element|
11
- res = yield element
12
- result << res if res
13
- end
14
- result
15
- else
16
- Enumerator.new do |yielder|
17
- result = []
18
- each do |element|
19
- res = yielder.yield element
20
- result << res if res
21
- end
22
- result
23
- end
3
+ # Refine Array seprately, 'cause refining modules is vulnerable to prepend:
4
+ # - https://bugs.ruby-lang.org/issues/13446
5
+ RubyNext::Core.patch Enumerable, method: :filter_map, version: "2.7", refineable: [Enumerable, Array] do
6
+ <<~RUBY
7
+ def filter_map
8
+ if block_given?
9
+ result = []
10
+ each do |element|
11
+ res = yield element
12
+ result << res if res
13
+ end
14
+ result
15
+ else
16
+ Enumerator.new do |yielder|
17
+ result = []
18
+ each do |element|
19
+ res = yielder.yield element
20
+ result << res if res
24
21
  end
22
+ result
25
23
  end
26
24
  end
27
25
  end
28
- end
29
-
30
- RubyNext.module_eval do
31
- refine Enumerable do
32
- include RubyNext::Core::EnumerableFilterMap
33
- end
26
+ RUBY
27
+ end
34
28
 
35
- refine Enumerator::Lazy do
36
- def filter_map
37
- Enumerator::Lazy.new(self) do |yielder, *values|
38
- result = yield(*values)
39
- yielder << result if result
40
- end
29
+ RubyNext::Core.patch Enumerator::Lazy, method: :filter_map, version: "2.7" do
30
+ <<~RUBY
31
+ def filter_map
32
+ Enumerator::Lazy.new(self) do |yielder, *values|
33
+ result = yield(*values)
34
+ yielder << result if result
41
35
  end
42
36
  end
43
-
44
- # Refine Array seprately, 'cause refining modules is vulnerable to prepend:
45
- # - https://bugs.ruby-lang.org/issues/13446
46
- refine Array do
47
- include RubyNext::Core::EnumerableFilterMap
48
- end
49
- end
37
+ RUBY
50
38
  end
@@ -1,28 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless [].respond_to?(:tally)
4
- module RubyNext
5
- module Core
6
- module EnumerableTally
7
- def tally
8
- each_with_object({}) do |v, acc|
9
- acc[v] ||= 0
10
- acc[v] += 1
11
- end
12
- end
3
+ # Refine Array seprately, 'cause refining modules is vulnerable to prepend:
4
+ # - https://bugs.ruby-lang.org/issues/13446
5
+ RubyNext::Core.patch Enumerable, method: :tally, version: "2.7", refineable: [Enumerable, Array] do
6
+ <<~RUBY
7
+ def tally
8
+ each_with_object({}) do |v, acc|
9
+ acc[v] ||= 0
10
+ acc[v] += 1
13
11
  end
14
12
  end
15
- end
16
-
17
- RubyNext.module_eval do
18
- refine Enumerable do
19
- include RubyNext::Core::EnumerableTally
20
- end
21
-
22
- # Refine Array seprately, 'cause refining modules is vulnerable to prepend:
23
- # - https://bugs.ruby-lang.org/issues/13446
24
- refine Array do
25
- include RubyNext::Core::EnumerableTally
26
- end
27
- end
13
+ RUBY
28
14
  end
@@ -1,22 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- unless Enumerator.respond_to?(:produce)
4
- RubyNext.module_eval do
5
- refine Enumerator.singleton_class do
6
- # Based on https://github.com/zverok/enumerator_generate
7
- def produce(*rest, &block)
8
- raise ArgumentError, "wrong number of arguments (given #{rest.size}, expected 0..1)" if rest.size > 1
9
- raise ArgumentError, "No block given" unless block
3
+ RubyNext::Core.patch Enumerator.singleton_class, method: :produce, singleton: Enumerator, version: "2.7" do
4
+ <<~'RUBY'
5
+ # Based on https://github.com/zverok/enumerator_generate
6
+ def produce(*rest, &block)
7
+ raise ArgumentError, "wrong number of arguments (given #{rest.size}, expected 0..1)" if rest.size > 1
8
+ raise ArgumentError, "No block given" unless block
10
9
 
11
- Enumerator.new(Float::INFINITY) do |y|
12
- val = rest.empty? ? yield() : rest.pop
10
+ Enumerator.new(Float::INFINITY) do |y|
11
+ val = rest.empty? ? yield() : rest.pop
13
12
 
14
- loop do
15
- y << val
16
- val = yield(val)
17
- end
13
+ loop do
14
+ y << val
15
+ val = yield(val)
18
16
  end
19
17
  end
20
18
  end
21
- end
19
+ RUBY
22
20
  end