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,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ unless defined?(NoMatchingPatternError)
4
+ class NoMatchingPatternError < RuntimeError
5
+ end
6
+ end
7
+
8
+ # Add `#deconstruct` and `#deconstruct_keys` to core classes
9
+ unless [].respond_to?(:deconstruct)
10
+ RubyNext.module_eval do
11
+ refine Array do
12
+ def deconstruct
13
+ self
14
+ end
15
+ end
16
+
17
+ refine Struct do
18
+ alias deconstruct to_a
19
+ end
20
+ end
21
+ end
22
+
23
+ unless {}.respond_to?(:deconstruct_keys)
24
+ RubyNext.module_eval do
25
+ refine Hash do
26
+ def deconstruct_keys(_)
27
+ self
28
+ end
29
+ end
30
+
31
+ refine Struct do
32
+ def deconstruct_keys(_)
33
+ to_h
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Style/LambdaCall
4
+ unless proc {}.respond_to?(:<<)
5
+ RubyNext.module_eval do
6
+ refine Proc do
7
+ def <<(other)
8
+ raise TypeError, "callable object is expected" unless other.respond_to?(:call)
9
+ this = self
10
+ proc { |*args, &block| this.(other.(*args, &block)) }
11
+ end
12
+
13
+ def >>(other)
14
+ raise TypeError, "callable object is expected" unless other.respond_to?(:call)
15
+ this = self
16
+ proc { |*args, &block| other.(this.(*args, &block)) }
17
+ end
18
+ end
19
+ end
20
+ end
21
+ # rubocop:enable Style/LambdaCall
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Extend `Language.transform` to inject `using RubyNext` to every file
4
+ RubyNext::Language.singleton_class.prepend(Module.new do
5
+ def transform(contents, using: true, **hargs)
6
+ # We cannot activate refinements in eval
7
+ new_contents = RubyNext::Core.inject!(contents) if using
8
+ super(new_contents || contents, using: using, **hargs)
9
+ end
10
+ end)
@@ -0,0 +1,117 @@
1
+ # frozen_string_literal: true
2
+
3
+ gem "parser", ">= 2.7.0.0"
4
+ gem "unparser", ">= 0.4.7"
5
+
6
+ require "set"
7
+
8
+ require "ruby-next"
9
+ using RubyNext
10
+
11
+ module RubyNext
12
+ # Language module contains tools to transpile newer Ruby syntax
13
+ # into an older one.
14
+ #
15
+ # It works the following way:
16
+ # - Takes a Ruby source code as input
17
+ # - Generates the AST using the edge parser (via the `parser` gem)
18
+ # - Pass this AST through the list of processors (one feature = one processor)
19
+ # - Each processor may modify the AST
20
+ # - Generates a transpiled source code from the transformed AST (via the `unparser` gem)
21
+ module Language
22
+ require "ruby-next/language/parser"
23
+ require "ruby-next/language/unparser"
24
+
25
+ class TransformContext
26
+ attr_reader :versions, :use_ruby_next
27
+
28
+ def initialize
29
+ # Minimum supported RubyNext version
30
+ @min_version = MIN_SUPPORTED_VERSION
31
+ @dirty = false
32
+ @versions = Set.new
33
+ @use_ruby_next = false
34
+ end
35
+
36
+ # Called by rewriter when it performs transfomrations
37
+ def track!(rewriter)
38
+ @dirty = true
39
+ versions << rewriter.class::MIN_SUPPORTED_VERSION
40
+ end
41
+
42
+ def use_ruby_next!
43
+ @use_ruby_next = true
44
+ end
45
+
46
+ alias use_ruby_next? use_ruby_next
47
+
48
+ def dirty?
49
+ @dirty == true
50
+ end
51
+
52
+ def min_version
53
+ versions.min
54
+ end
55
+
56
+ def sorted_versions
57
+ versions.to_a.sort
58
+ end
59
+ end
60
+
61
+ class << self
62
+ attr_accessor :rewriters
63
+ attr_reader :watch_dirs
64
+
65
+ def transform(source, rewriters: self.rewriters, using: true, context: TransformContext.new)
66
+ parse(source).then do |ast|
67
+ rewriters.inject(ast) do |tree, rewriter|
68
+ rewriter.new(context).process(tree)
69
+ end.then do |new_ast|
70
+ next source unless context.dirty?
71
+
72
+ Unparser.unparse(new_ast)
73
+ end.then do |source|
74
+ next source unless using && context.use_ruby_next?
75
+
76
+ Core.inject! source.dup
77
+ end
78
+ end
79
+ end
80
+
81
+ def transformable?(path)
82
+ watch_dirs.any? { |dir| path.start_with?(dir) }
83
+ end
84
+
85
+ # Rewriters required for the current version
86
+ def current_rewriters
87
+ @current_rewriters ||= rewriters.select(&:unsupported_syntax?)
88
+ end
89
+
90
+ private
91
+
92
+ attr_writer :watch_dirs
93
+ end
94
+
95
+ self.rewriters = []
96
+ self.watch_dirs = %w[app lib spec test].map { |path| File.join(Dir.pwd, path) }
97
+
98
+ require "ruby-next/language/rewriters/base"
99
+
100
+ require "ruby-next/language/rewriters/endless_range"
101
+ rewriters << Rewriters::EndlessRange
102
+
103
+ require "ruby-next/language/rewriters/pattern_matching"
104
+ rewriters << Rewriters::PatternMatching
105
+
106
+ require "ruby-next/language/rewriters/args_forward"
107
+ rewriters << Rewriters::ArgsForward
108
+
109
+ require "ruby-next/language/rewriters/numbered_params"
110
+ rewriters << Rewriters::NumberedParams
111
+
112
+ if ENV["RUBY_NEXT_ENABLE_METHOD_REFERENCE"] == "1"
113
+ require "ruby-next/language/rewriters/method_reference"
114
+ RubyNext::Language.rewriters << RubyNext::Language::Rewriters::MethodReference
115
+ end
116
+ end
117
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ruby-next"
4
+ require "ruby-next/utils"
5
+ require "ruby-next/language"
6
+
7
+ # Patch bootsnap to transform source code.
8
+ # Based on https://github.com/kddeisz/preval/blob/master/lib/preval.rb
9
+ load_iseq = RubyVM::InstructionSequence.method(:load_iseq)
10
+
11
+ if load_iseq.source_location[0].include?("/bootsnap/")
12
+ Bootsnap::CompileCache::ISeq.singleton_class.prepend(
13
+ Module.new do
14
+ def input_to_storage(source, path)
15
+ return super unless RubyNext::Language.transformable?(path)
16
+ source = RubyNext::Language.transform(source, rewriters: RubyNext::Language.current_rewriters)
17
+
18
+ $stdout.puts ::RubyNext::Utils.source_with_lines(source, path) if ENV["RUBY_NEXT_DEBUG"] == "1"
19
+
20
+ RubyVM::InstructionSequence.compile(source, path, path).to_binary
21
+ rescue SyntaxError
22
+ raise Bootsnap::CompileCache::Uncompilable, "syntax error"
23
+ end
24
+ end
25
+ )
26
+ end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module KernelEval
6
+ refine Kernel do
7
+ def eval(source, bind = nil, *args)
8
+ new_source = ::RubyNext::Language::Runtime.transform(
9
+ source,
10
+ using: bind&.receiver == TOPLEVEL_BINDING.receiver || bind&.receiver&.is_a?(Module)
11
+ )
12
+ $stdout.puts ::RubyNext::Utils.source_with_lines(new_source, "(#{caller_locations(1, 1).first})") if ENV["RUBY_NEXT_DEBUG"] == "1"
13
+ super new_source, bind, *args
14
+ end
15
+ end
16
+ end
17
+
18
+ module InstanceEval # :nodoc:
19
+ refine Object do
20
+ def instance_eval(*args, &block)
21
+ return super(*args, &block) if block_given?
22
+
23
+ source = args.shift
24
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
25
+ $stdout.puts ::RubyNext::Utils.source_with_lines(new_source, "(#{caller_locations(1, 1).first})") if ENV["RUBY_NEXT_DEBUG"] == "1"
26
+ super new_source, *args
27
+ end
28
+ end
29
+ end
30
+
31
+ module ClassEval
32
+ refine Module do
33
+ def module_eval(*args, &block)
34
+ return super(*args, &block) if block_given?
35
+
36
+ source = args.shift
37
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
38
+ $stdout.puts ::RubyNext::Utils.source_with_lines(new_source, "(#{caller_locations(1, 1).first})") if ENV["RUBY_NEXT_DEBUG"] == "1"
39
+ super new_source, *args
40
+ end
41
+
42
+ def class_eval(*args, &block)
43
+ return super(*args, &block) if block_given?
44
+
45
+ source = args.shift
46
+ new_source = ::RubyNext::Language::Runtime.transform(source, using: false)
47
+ $stdout.puts ::RubyNext::Utils.source_with_lines(new_source, "(#{caller_locations(1, 1).first})") if ENV["RUBY_NEXT_DEBUG"] == "1"
48
+ super new_source, *args
49
+ end
50
+ end
51
+ end
52
+
53
+ # Refinements for `eval`-like methods.
54
+ # Transpiling eval is only possible if we do not use local from the binding,
55
+ # because we cannot access the binding of caller (without non-production ready hacks).
56
+ #
57
+ # This module is meant mainly for testing purposes.
58
+ module Eval
59
+ include InstanceEval
60
+ include ClassEval
61
+ include KernelEval
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "parser/ruby27"
4
+
5
+ module RubyNext
6
+ module Language
7
+ class Builder < ::Parser::Builders::Default
8
+ modernize
9
+ end
10
+
11
+ class << self
12
+ def parser
13
+ ::Parser::Ruby27.new(Builder.new)
14
+ end
15
+
16
+ def parse(source, file = "(string)")
17
+ buffer = ::Parser::Source::Buffer.new(file).tap do |buffer|
18
+ buffer.source = source
19
+ end
20
+ parser.parse(buffer)
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module RubyNext
4
+ module Language
5
+ module Rewriters
6
+ class ArgsForward < Base
7
+ SYNTAX_PROBE = "obj = Object.new; def obj.foo(...) super(...); end"
8
+ MIN_SUPPORTED_VERSION = Gem::Version.new("2.7.0")
9
+
10
+ REST = :__rest__
11
+ BLOCK = :__block__
12
+
13
+ def on_forward_args(node)
14
+ context.track! self
15
+
16
+ node.updated(
17
+ :args,
18
+ [
19
+ s(:restarg, REST),
20
+ s(:blockarg, BLOCK)
21
+ ]
22
+ )
23
+ end
24
+
25
+ def on_send(node)
26
+ return unless node.children[2]&.type == :forwarded_args
27
+
28
+ node.updated(
29
+ nil,
30
+ [
31
+ *node.children[0..1],
32
+ *forwarded_args
33
+ ]
34
+ )
35
+ end
36
+
37
+ def on_super(node)
38
+ return unless node.children[0]&.type == :forwarded_args
39
+
40
+ node.updated(
41
+ nil,
42
+ forwarded_args
43
+ )
44
+ end
45
+
46
+ private
47
+
48
+ def forwarded_args
49
+ [
50
+ s(:splat, s(:lvar, REST)),
51
+ s(:block_pass, s(:lvar, BLOCK))
52
+ ]
53
+ end
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,105 @@
1
+ # frozen_string_literal: true
2
+
3
+ using RubyNext
4
+
5
+ module RubyNext
6
+ module Language
7
+ module Rewriters
8
+ CUSTOM_PARSER_REQUIRED = <<~MSG
9
+ The %s feature is not a part of the latest stable Ruby release
10
+ and is not supported by your Parser gem version.
11
+
12
+ Use RubyNext's parser to use it: https://github.com/ruby-next/parser
13
+
14
+ MSG
15
+
16
+ class Base < ::Parser::TreeRewriter
17
+ class LocalsTracker
18
+ attr_reader :stacks
19
+
20
+ def initialize
21
+ @stacks = []
22
+ end
23
+
24
+ def with(**locals)
25
+ stacks << locals
26
+ yield.tap { stacks.pop }
27
+ end
28
+
29
+ def [](name, suffix = nil)
30
+ fetch(name).then do |name|
31
+ next name unless suffix
32
+ :"#{name}#{suffix}__"
33
+ end
34
+ end
35
+
36
+ def key?(name)
37
+ !!fetch(name) { false }
38
+ end
39
+
40
+ def fetch(name)
41
+ ind = -1
42
+
43
+ loop do
44
+ break stacks[ind][name] if stacks[ind].key?(name)
45
+ ind -= 1
46
+ break if stacks[ind].nil?
47
+ end.then do |name|
48
+ next name unless name.nil?
49
+
50
+ return yield if block_given?
51
+ raise ArgumentError, "Local var not found in scope: #{name}"
52
+ end
53
+ end
54
+ end
55
+
56
+ class << self
57
+ # Returns true if the syntax is supported
58
+ # by the current Ruby (performs syntax check, not version check)
59
+ def unsupported_syntax?
60
+ save_verbose, $VERBOSE = $VERBOSE, nil
61
+ eval_mid = Kernel.respond_to?(:eval_without_ruby_next) ? :eval_without_ruby_next : :eval
62
+ Kernel.send eval_mid, self::SYNTAX_PROBE
63
+ false
64
+ rescue SyntaxError, NameError
65
+ true
66
+ ensure
67
+ $VERBOSE = save_verbose
68
+ end
69
+
70
+ # Returns true if the syntax is supported
71
+ # by the specified version
72
+ def unsupported_version?(version)
73
+ self::MIN_SUPPORTED_VERSION > version
74
+ end
75
+
76
+ private
77
+
78
+ def transform(source)
79
+ Language.transform(source, rewriters: [self], using: false)
80
+ end
81
+
82
+ def warn_custom_parser_required_for(feature)
83
+ warn(CUSTOM_PARSER_REQUIRED % feature)
84
+ end
85
+ end
86
+
87
+ attr_reader :locals
88
+
89
+ def initialize(context)
90
+ @context = context
91
+ @locals = LocalsTracker.new
92
+ super()
93
+ end
94
+
95
+ def s(type, *children)
96
+ ::Parser::AST::Node.new(type, children)
97
+ end
98
+
99
+ private
100
+
101
+ attr_reader :context
102
+ end
103
+ end
104
+ end
105
+ end