ruby-next-core 0.2.0

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