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