ruby-next-core 0.2.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +68 -0
- data/LICENSE.txt +21 -0
- data/README.md +279 -0
- data/bin/parse +19 -0
- data/bin/ruby-next +16 -0
- data/bin/transform +21 -0
- data/lib/ruby-next.rb +37 -0
- data/lib/ruby-next/cli.rb +55 -0
- data/lib/ruby-next/commands/base.rb +42 -0
- data/lib/ruby-next/commands/nextify.rb +118 -0
- data/lib/ruby-next/core.rb +34 -0
- data/lib/ruby-next/core/array/difference_union_intersection.rb +31 -0
- data/lib/ruby-next/core/enumerable/filter.rb +23 -0
- data/lib/ruby-next/core/enumerable/filter_map.rb +50 -0
- data/lib/ruby-next/core/enumerable/tally.rb +28 -0
- data/lib/ruby-next/core/enumerator/produce.rb +22 -0
- data/lib/ruby-next/core/hash/merge.rb +16 -0
- data/lib/ruby-next/core/kernel/then.rb +12 -0
- data/lib/ruby-next/core/pattern_matching.rb +37 -0
- data/lib/ruby-next/core/proc/compose.rb +21 -0
- data/lib/ruby-next/core/runtime.rb +10 -0
- data/lib/ruby-next/language.rb +117 -0
- data/lib/ruby-next/language/bootsnap.rb +26 -0
- data/lib/ruby-next/language/eval.rb +64 -0
- data/lib/ruby-next/language/parser.rb +24 -0
- data/lib/ruby-next/language/rewriters/args_forward.rb +57 -0
- data/lib/ruby-next/language/rewriters/base.rb +105 -0
- data/lib/ruby-next/language/rewriters/endless_range.rb +60 -0
- data/lib/ruby-next/language/rewriters/method_reference.rb +31 -0
- data/lib/ruby-next/language/rewriters/numbered_params.rb +41 -0
- data/lib/ruby-next/language/rewriters/pattern_matching.rb +522 -0
- data/lib/ruby-next/language/runtime.rb +96 -0
- data/lib/ruby-next/language/setup.rb +43 -0
- data/lib/ruby-next/language/unparser.rb +8 -0
- data/lib/ruby-next/utils.rb +36 -0
- data/lib/ruby-next/version.rb +5 -0
- data/lib/uby-next.rb +68 -0
- metadata +117 -0
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "ruby-next"
|
4
|
+
require "ruby-next/language"
|
5
|
+
|
6
|
+
require "ruby-next/commands/base"
|
7
|
+
require "ruby-next/commands/nextify"
|
8
|
+
|
9
|
+
module RubyNext
|
10
|
+
# Command line interface for RubyNext
|
11
|
+
class CLI
|
12
|
+
class << self
|
13
|
+
attr_accessor :verbose
|
14
|
+
end
|
15
|
+
|
16
|
+
self.verbose = false
|
17
|
+
|
18
|
+
COMMANDS = {
|
19
|
+
"nextify" => Commands::Nextify
|
20
|
+
}.freeze
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
end
|
24
|
+
|
25
|
+
def run(args = ARGV)
|
26
|
+
maybe_print_version(args)
|
27
|
+
|
28
|
+
command = args.shift
|
29
|
+
|
30
|
+
raise "Command must be specified!" unless command
|
31
|
+
|
32
|
+
COMMANDS.fetch(command) do
|
33
|
+
raise "Unknown command: #{command}. Available commands: #{COMMANDS.keys.join(",")}"
|
34
|
+
end.run(args)
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def maybe_print_version(args)
|
40
|
+
args = args.dup
|
41
|
+
begin
|
42
|
+
OptionParser.new do |opts|
|
43
|
+
opts.banner = "Usage: ruby-next COMMAND [options]"
|
44
|
+
|
45
|
+
opts.on("-v", "--version", "Print version") do
|
46
|
+
STDOUT.puts RubyNext::VERSION
|
47
|
+
exit 0
|
48
|
+
end
|
49
|
+
end.parse!(args)
|
50
|
+
rescue OptionParser::InvalidOption
|
51
|
+
# skip and pass all args to the command's parser
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "optparse"
|
4
|
+
|
5
|
+
module RubyNext
|
6
|
+
module Commands
|
7
|
+
class Base
|
8
|
+
class << self
|
9
|
+
def run(args)
|
10
|
+
new(args).run
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def initialize(args)
|
15
|
+
parse! args
|
16
|
+
end
|
17
|
+
|
18
|
+
def parse!(*)
|
19
|
+
raise NotImplementedError
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
raise NotImplementedError
|
24
|
+
end
|
25
|
+
|
26
|
+
def log(msg)
|
27
|
+
return unless CLI.verbose
|
28
|
+
$stdout.puts msg
|
29
|
+
end
|
30
|
+
|
31
|
+
def base_parser
|
32
|
+
OptionParser.new do |opts|
|
33
|
+
yield opts
|
34
|
+
|
35
|
+
opts.on("-V", "Turn on verbose mode") do
|
36
|
+
CLI.verbose = true
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,118 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "fileutils"
|
4
|
+
require "pathname"
|
5
|
+
|
6
|
+
using RubyNext
|
7
|
+
|
8
|
+
module RubyNext
|
9
|
+
module Commands
|
10
|
+
class Nextify < Base
|
11
|
+
attr_reader :lib_path, :paths, :out_path, :min_version, :single_version
|
12
|
+
|
13
|
+
def run
|
14
|
+
paths.each do |path|
|
15
|
+
contents = File.read(path)
|
16
|
+
transpile path, contents
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def parse!(args)
|
21
|
+
@min_version = MIN_SUPPORTED_VERSION
|
22
|
+
@single_version = false
|
23
|
+
|
24
|
+
optparser = base_parser do |opts|
|
25
|
+
opts.banner = "Usage: ruby-next nextify DIRECTORY_OR_FILE [options]"
|
26
|
+
|
27
|
+
opts.on("-o", "--output=OUTPUT", "Specify output directory or file or stdout") do |val|
|
28
|
+
@out_path = val
|
29
|
+
end
|
30
|
+
|
31
|
+
opts.on("--min-version=VERSION", "Specify the minimum Ruby version to support") do |val|
|
32
|
+
@min_version = Gem::Version.new(val)
|
33
|
+
end
|
34
|
+
|
35
|
+
opts.on("--single-version", "Only create one version of a file (for the earliest Ruby version)") do
|
36
|
+
@single_version = true
|
37
|
+
end
|
38
|
+
|
39
|
+
opts.on("--enable-method-reference", "Enable reverted method reference syntax (requires custom parser)") do
|
40
|
+
require "ruby-next/language/rewriters/method_reference"
|
41
|
+
Language.rewriters << Language::Rewriters::MethodReference
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
@lib_path = args[0]
|
46
|
+
|
47
|
+
unless lib_path&.then(&File.method(:exist?))
|
48
|
+
$stdout.puts optparser.help
|
49
|
+
exit 0
|
50
|
+
end
|
51
|
+
|
52
|
+
optparser.parse!(args)
|
53
|
+
|
54
|
+
@paths =
|
55
|
+
if File.directory?(lib_path)
|
56
|
+
Dir[File.join(lib_path, "**/*.rb")]
|
57
|
+
elsif File.file?(lib_path)
|
58
|
+
[lib_path].tap do |_|
|
59
|
+
@lib_path = File.dirname(lib_path)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
def transpile(path, contents, version: min_version)
|
67
|
+
rewriters = Language.rewriters.select { |rw| rw.unsupported_version?(version) }
|
68
|
+
|
69
|
+
context = Language::TransformContext.new
|
70
|
+
new_contents = Language.transform contents, context: context, rewriters: rewriters
|
71
|
+
|
72
|
+
return unless context.dirty?
|
73
|
+
|
74
|
+
versions = context.sorted_versions
|
75
|
+
version = versions.shift
|
76
|
+
|
77
|
+
# First, store already transpiled contents in the minimum required version dir
|
78
|
+
save new_contents, path, version
|
79
|
+
|
80
|
+
return if versions.empty? || single_version?
|
81
|
+
|
82
|
+
# Then, generate the source code for the next version
|
83
|
+
transpile path, contents, version: version
|
84
|
+
end
|
85
|
+
|
86
|
+
def save(contents, path, version)
|
87
|
+
return $stdout.puts(contents) if out_path == "stdout"
|
88
|
+
|
89
|
+
paths = [Pathname.new(path).relative_path_from(Pathname.new(lib_path))]
|
90
|
+
|
91
|
+
paths.unshift(version.segments[0..1].join(".")) unless single_version?
|
92
|
+
|
93
|
+
next_path =
|
94
|
+
if out_path
|
95
|
+
if out_path.end_with?(".rb")
|
96
|
+
out_path
|
97
|
+
else
|
98
|
+
File.join(out_path, *paths)
|
99
|
+
end
|
100
|
+
else
|
101
|
+
File.join(
|
102
|
+
lib_path,
|
103
|
+
RUBY_NEXT_DIR,
|
104
|
+
*paths
|
105
|
+
)
|
106
|
+
end
|
107
|
+
|
108
|
+
FileUtils.mkdir_p File.dirname(next_path)
|
109
|
+
|
110
|
+
File.write(next_path, contents)
|
111
|
+
|
112
|
+
log "Generated: #{next_path}"
|
113
|
+
end
|
114
|
+
|
115
|
+
alias single_version? single_version
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module RubyNext
|
4
|
+
module Core
|
5
|
+
class << self
|
6
|
+
# Inject `using RubyNext` at the top of the source code
|
7
|
+
def inject!(contents)
|
8
|
+
if contents.frozen?
|
9
|
+
contents = contents.sub(/^(\s*[^#\s].*)/, 'using RubyNext;\1')
|
10
|
+
else
|
11
|
+
contents.sub!(/^(\s*[^#\s].*)/, 'using RubyNext;\1')
|
12
|
+
end
|
13
|
+
contents
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
require_relative "core/kernel/then"
|
20
|
+
|
21
|
+
require_relative "core/proc/compose"
|
22
|
+
|
23
|
+
require_relative "core/enumerable/tally"
|
24
|
+
require_relative "core/enumerable/filter"
|
25
|
+
require_relative "core/enumerable/filter_map"
|
26
|
+
|
27
|
+
require_relative "core/enumerator/produce"
|
28
|
+
|
29
|
+
require_relative "core/array/difference_union_intersection"
|
30
|
+
|
31
|
+
require_relative "core/hash/merge"
|
32
|
+
|
33
|
+
# Core extensions required for pattern matching
|
34
|
+
require_relative "core/pattern_matching"
|
@@ -0,0 +1,31 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
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
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
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
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
unless [].respond_to?(:filter)
|
4
|
+
RubyNext.module_eval do
|
5
|
+
refine Enumerable do
|
6
|
+
alias filter select
|
7
|
+
end
|
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
|
17
|
+
|
18
|
+
refine Hash do
|
19
|
+
alias filter select
|
20
|
+
alias filter! select!
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
RubyNext.module_eval do
|
31
|
+
refine Enumerable do
|
32
|
+
include RubyNext::Core::EnumerableFilterMap
|
33
|
+
end
|
34
|
+
|
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
|
41
|
+
end
|
42
|
+
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
|
50
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
13
|
+
end
|
14
|
+
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
|
28
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
# frozen_string_literal: true
|
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
|
10
|
+
|
11
|
+
Enumerator.new(Float::INFINITY) do |y|
|
12
|
+
val = rest.empty? ? yield() : rest.pop
|
13
|
+
|
14
|
+
loop do
|
15
|
+
y << val
|
16
|
+
val = yield(val)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
unless {}.method(:merge).arity == -1
|
4
|
+
RubyNext.module_eval do
|
5
|
+
refine Hash do
|
6
|
+
def merge(*others)
|
7
|
+
return super if others.size == 1
|
8
|
+
return dup if others.size == 0
|
9
|
+
|
10
|
+
merge(others.shift).tap do |new_h|
|
11
|
+
others.each { |h| new_h.merge!(h) }
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
unless Object.new.respond_to?(:then)
|
4
|
+
RubyNext.module_eval do
|
5
|
+
# Refine object, 'cause refining modules (Kernel) is vulnerable to prepend:
|
6
|
+
# - https://bugs.ruby-lang.org/issues/13446
|
7
|
+
# - Rails added `Kernel.prepend` in 6.1: https://github.com/rails/rails/commit/3124007bd674dcdc9c3b5c6b2964dfb7a1a0733c
|
8
|
+
refine Object do
|
9
|
+
alias then yield_self
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|