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,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