ruby-next-core 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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