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