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