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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +68 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +279 -0
  5. data/bin/parse +19 -0
  6. data/bin/ruby-next +16 -0
  7. data/bin/transform +21 -0
  8. data/lib/ruby-next.rb +37 -0
  9. data/lib/ruby-next/cli.rb +55 -0
  10. data/lib/ruby-next/commands/base.rb +42 -0
  11. data/lib/ruby-next/commands/nextify.rb +118 -0
  12. data/lib/ruby-next/core.rb +34 -0
  13. data/lib/ruby-next/core/array/difference_union_intersection.rb +31 -0
  14. data/lib/ruby-next/core/enumerable/filter.rb +23 -0
  15. data/lib/ruby-next/core/enumerable/filter_map.rb +50 -0
  16. data/lib/ruby-next/core/enumerable/tally.rb +28 -0
  17. data/lib/ruby-next/core/enumerator/produce.rb +22 -0
  18. data/lib/ruby-next/core/hash/merge.rb +16 -0
  19. data/lib/ruby-next/core/kernel/then.rb +12 -0
  20. data/lib/ruby-next/core/pattern_matching.rb +37 -0
  21. data/lib/ruby-next/core/proc/compose.rb +21 -0
  22. data/lib/ruby-next/core/runtime.rb +10 -0
  23. data/lib/ruby-next/language.rb +117 -0
  24. data/lib/ruby-next/language/bootsnap.rb +26 -0
  25. data/lib/ruby-next/language/eval.rb +64 -0
  26. data/lib/ruby-next/language/parser.rb +24 -0
  27. data/lib/ruby-next/language/rewriters/args_forward.rb +57 -0
  28. data/lib/ruby-next/language/rewriters/base.rb +105 -0
  29. data/lib/ruby-next/language/rewriters/endless_range.rb +60 -0
  30. data/lib/ruby-next/language/rewriters/method_reference.rb +31 -0
  31. data/lib/ruby-next/language/rewriters/numbered_params.rb +41 -0
  32. data/lib/ruby-next/language/rewriters/pattern_matching.rb +522 -0
  33. data/lib/ruby-next/language/runtime.rb +96 -0
  34. data/lib/ruby-next/language/setup.rb +43 -0
  35. data/lib/ruby-next/language/unparser.rb +8 -0
  36. data/lib/ruby-next/utils.rb +36 -0
  37. data/lib/ruby-next/version.rb +5 -0
  38. data/lib/uby-next.rb +68 -0
  39. 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