tco_method 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.gitignore +0 -0
- data/.travis.yml +0 -0
- data/Gemfile +0 -0
- data/Guardfile +0 -0
- data/LICENSE +0 -0
- data/README.md +102 -27
- data/Rakefile +0 -0
- data/lib/tco_method.rb +33 -43
- data/lib/tco_method/ambiguous_source_error.rb +42 -0
- data/lib/tco_method/block_extractor.rb +107 -0
- data/lib/tco_method/block_with_tco.rb +22 -0
- data/lib/tco_method/method_info.rb +0 -0
- data/lib/tco_method/method_reevaluator.rb +45 -0
- data/lib/tco_method/mixin.rb +20 -4
- data/lib/tco_method/version.rb +1 -1
- data/tco_method.gemspec +0 -0
- data/test/test_helper.rb +2 -2
- data/test/test_helpers/assertions.rb +0 -0
- data/test/test_helpers/fibbers.rb +37 -0
- data/test/unit/block_extractor_test.rb +150 -0
- data/test/unit/block_with_tco_test.rb +53 -0
- data/test/unit/method_info_test.rb +0 -0
- data/test/unit/method_reevaluator_test.rb +112 -0
- data/test/unit/mixin_test.rb +66 -46
- data/test/unit/tco_method_test.rb +42 -156
- metadata +15 -3
@@ -0,0 +1,22 @@
|
|
1
|
+
require "tco_method/block_extractor"
|
2
|
+
|
3
|
+
module TCOMethod
|
4
|
+
class BlockWithTCO
|
5
|
+
attr_reader :result
|
6
|
+
|
7
|
+
def initialize(&block)
|
8
|
+
raise ArgumentError, "Block required" unless block
|
9
|
+
@result = eval(block)
|
10
|
+
end
|
11
|
+
|
12
|
+
private
|
13
|
+
|
14
|
+
def extract_source(block)
|
15
|
+
BlockExtractor.new(block).source
|
16
|
+
end
|
17
|
+
|
18
|
+
def eval(block)
|
19
|
+
TCOMethod.tco_eval(extract_source(block)).call
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
File without changes
|
@@ -0,0 +1,45 @@
|
|
1
|
+
require "tco_method/method_info"
|
2
|
+
|
3
|
+
module TCOMethod
|
4
|
+
class MethodReevaluator
|
5
|
+
# Reevaluates a method with tail call optimization enabled.
|
6
|
+
#
|
7
|
+
# @note This class is not part of the public API and should not be used
|
8
|
+
# directly. See {TCOMethod::Mixin} for a module that provides publicly
|
9
|
+
# supported access to the behaviors provided by this method.
|
10
|
+
# @param [Class, Module] receiver The Class or Module for which the specified
|
11
|
+
# module, class, or instance method should be reevaluated with tail call
|
12
|
+
# optimization enabled.
|
13
|
+
# @param [String, Symbol] method_name The name of the method that should be
|
14
|
+
# reevaluated with tail call optimization enabled.
|
15
|
+
# @param [Symbol] method_owner A symbol representing whether the specified
|
16
|
+
# method is expected to be owned by a class, module, or instance.
|
17
|
+
# @raise [ArgumentError] Raised if receiver, method_name, or method_owner
|
18
|
+
# argument is omitted.
|
19
|
+
# @raise [TypeError] Raised if the specified method is not a method that can
|
20
|
+
# be reevaluated with tail call optimization enabled.
|
21
|
+
def initialize(receiver, method_name, method_owner)
|
22
|
+
raise ArgumentError, "Receiver required!" unless receiver
|
23
|
+
raise ArgumentError, "Method name required!" unless method_name
|
24
|
+
raise ArgumentError, "Method owner required!" unless method_owner
|
25
|
+
if method_owner == :instance
|
26
|
+
existing_method = receiver.instance_method(method_name)
|
27
|
+
elsif method_owner == :class || method_owner== :module
|
28
|
+
existing_method = receiver.method(method_name)
|
29
|
+
end
|
30
|
+
method_info = MethodInfo.new(existing_method)
|
31
|
+
if method_info.type != :method
|
32
|
+
raise TypeError, "Invalid method type: #{method_info.type}"
|
33
|
+
end
|
34
|
+
receiver_class = receiver.is_a?(Class) ? :class : :module
|
35
|
+
code = <<-CODE
|
36
|
+
#{receiver_class} #{receiver.name}
|
37
|
+
#{existing_method.source}
|
38
|
+
end
|
39
|
+
CODE
|
40
|
+
|
41
|
+
file, line = existing_method.source_location
|
42
|
+
TCOMethod.tco_eval(code, file, File.dirname(file), line - 1)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
data/lib/tco_method/mixin.rb
CHANGED
@@ -1,3 +1,5 @@
|
|
1
|
+
require "tco_method/method_reevaluator"
|
2
|
+
|
1
3
|
module TCOMethod
|
2
4
|
# Mixin providing tail call optimization eval and class annotations. When
|
3
5
|
# extended by a Class or Module adds methods for evaluating code with tail
|
@@ -11,9 +13,9 @@ module TCOMethod
|
|
11
13
|
# @param [String, Symbol] method_name The name of the class or module method
|
12
14
|
# that should be reeevaluated with tail call optimization enabled.
|
13
15
|
# @return [Symbol] The symbolized method name.
|
14
|
-
# @see TCOMethod
|
16
|
+
# @see TCOMethod::MethodReevaluator
|
15
17
|
def tco_module_method(method_name)
|
16
|
-
|
18
|
+
MethodReevaluator.new(self, method_name, :module)
|
17
19
|
end
|
18
20
|
alias_method :tco_class_method, :tco_module_method
|
19
21
|
|
@@ -36,9 +38,23 @@ module TCOMethod
|
|
36
38
|
# @param [String, Symbol] method_name The name of the instance method that
|
37
39
|
# should be reeevaluated with tail call optimization enabled.
|
38
40
|
# @return [Symbol] The symbolized method name.
|
39
|
-
# @see TCOMethod
|
41
|
+
# @see TCOMethod::MethodReevaluator
|
40
42
|
def tco_method(method_name)
|
41
|
-
|
43
|
+
MethodReevaluator.new(self, method_name, :instance)
|
44
|
+
end
|
45
|
+
|
46
|
+
# Allows for executing a block of code with tail call optimization enabled.
|
47
|
+
#
|
48
|
+
# All code that is evaluated in the block will be evaluated with tail call
|
49
|
+
# optimization enabled, however here be dragons, so make sure to read the
|
50
|
+
# docs for {TCOMethod.with_tco} before getting too crazy.
|
51
|
+
#
|
52
|
+
# @param [Proc] block The proc to evaluate with tail call optimization
|
53
|
+
# enabled.
|
54
|
+
# @return [Object] Returns whatever the result of evaluating the given block.
|
55
|
+
# @see TCOMethod.with_tco
|
56
|
+
def with_tco(&block)
|
57
|
+
TCOMethod.with_tco(&block)
|
42
58
|
end
|
43
59
|
end
|
44
60
|
end
|
data/lib/tco_method/version.rb
CHANGED
data/tco_method.gemspec
CHANGED
File without changes
|
data/test/test_helper.rb
CHANGED
@@ -11,8 +11,8 @@ end
|
|
11
11
|
require "minitest/autorun"
|
12
12
|
require "mocha/setup"
|
13
13
|
require "tco_method"
|
14
|
-
|
15
|
-
|
14
|
+
require "test_helpers/assertions"
|
15
|
+
require "test_helpers/fibbers"
|
16
16
|
|
17
17
|
# Use alternate shoulda-style DSL for tests
|
18
18
|
class TCOMethod::TestCase < Minitest::Spec
|
File without changes
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# A couple of test classes used for testing tail call optimization in various
|
2
|
+
# contexts.
|
3
|
+
module TCOMethod
|
4
|
+
test_subject_builder = proc do
|
5
|
+
extend TCOMethod::Mixin
|
6
|
+
|
7
|
+
class << self
|
8
|
+
define_method(:singleton_block_method) { }
|
9
|
+
end
|
10
|
+
|
11
|
+
# Equivalent to the below, but provides a target for verifying that
|
12
|
+
# tco_module_method works on Classes and tco_class_method works on Modules.
|
13
|
+
def self.module_fib_yielder(index, back_one = 1, back_two = 0, &block)
|
14
|
+
yield back_two if index > 0
|
15
|
+
index < 1 ? back_two : module_fib_yielder(index - 1, back_one + back_two, back_one, &block)
|
16
|
+
end
|
17
|
+
|
18
|
+
# Equivalent to the above, but provides a target for verifying that
|
19
|
+
# tco_module_method works on Classes and tco_class_method works on Modules.
|
20
|
+
def self.class_fib_yielder(index, back_one = 1, back_two = 0, &block)
|
21
|
+
yield back_two if index > 0
|
22
|
+
index < 1 ? back_two : class_fib_yielder(index - 1, back_one + back_two, back_one, &block)
|
23
|
+
end
|
24
|
+
|
25
|
+
define_method(:instance_block_method) { }
|
26
|
+
|
27
|
+
# Equivalent to the above, but provides a target for verifying that
|
28
|
+
# instance methods work for both Classes and Modules
|
29
|
+
def instance_fib_yielder(index, back_one = 1, back_two = 0, &block)
|
30
|
+
yield back_two if index > 0
|
31
|
+
index < 1 ? back_two : instance_fib_yielder(index - 1, back_one + back_two, back_one, &block)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
TestModule = Module.new(&test_subject_builder)
|
36
|
+
TestClass = Class.new(&test_subject_builder)
|
37
|
+
end
|
@@ -0,0 +1,150 @@
|
|
1
|
+
require "pry"
|
2
|
+
require "test_helper"
|
3
|
+
|
4
|
+
module TCOMethod
|
5
|
+
class BlockExtractorTest < TestCase
|
6
|
+
Subject = BlockExtractor
|
7
|
+
subject { Subject }
|
8
|
+
|
9
|
+
blocks = [
|
10
|
+
:lambda_brace_inline,
|
11
|
+
:lambda_brace_multi,
|
12
|
+
:lambda_do_inline,
|
13
|
+
:lambda_do_multi,
|
14
|
+
:method_brace_inline,
|
15
|
+
:method_brace_multi,
|
16
|
+
:method_do_inline,
|
17
|
+
:method_do_multi,
|
18
|
+
:proc_brace_inline,
|
19
|
+
:proc_brace_multi,
|
20
|
+
:proc_do_inline,
|
21
|
+
:proc_do_multi,
|
22
|
+
]
|
23
|
+
|
24
|
+
unsourceable_blocks = [
|
25
|
+
:ambiguous_procs,
|
26
|
+
:a_hash_with_an_ambiguous_proc,
|
27
|
+
:an_ambiguous_proc_with_hash,
|
28
|
+
:an_unsourceable_proc,
|
29
|
+
]
|
30
|
+
|
31
|
+
context "block extraction" do
|
32
|
+
blocks.each do |meth|
|
33
|
+
should "extract block in #{meth} form" do
|
34
|
+
block = send(meth)
|
35
|
+
block_source = subject.new(block).source
|
36
|
+
reblock = eval(block_source)
|
37
|
+
reblock_result = reblock.call
|
38
|
+
|
39
|
+
# Ensure both blocks return the same result
|
40
|
+
assert_equal block.call, reblock_result
|
41
|
+
|
42
|
+
# Ensure a lambda is used where appropriate
|
43
|
+
assert_equal reblock_result == :lambda, reblock.lambda?
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
unsourceable_blocks.each do |meth|
|
48
|
+
should "raise when given a #{meth}" do
|
49
|
+
block = send(meth)
|
50
|
+
assert_raises(AmbiguousSourceError) { subject.new(block).source }
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
should "correctly strip trailing code at the end of the block" do
|
55
|
+
# The ').source' below should be plenty to test this concern.
|
56
|
+
block_source = subject.new(lambda do
|
57
|
+
"Hold on to your butts"
|
58
|
+
end).source
|
59
|
+
begin
|
60
|
+
eval(block_source)
|
61
|
+
rescue SyntaxError
|
62
|
+
assert false, "Syntax error in block source"
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# This ambiguity could be handled, but encourages poorly formatted code and
|
68
|
+
# doesn't seem worth the effort presently.
|
69
|
+
def a_hash_with_an_ambiguous_proc
|
70
|
+
{}; proc { :proc }
|
71
|
+
end
|
72
|
+
|
73
|
+
def ambiguous_procs
|
74
|
+
proc { :please }; proc { :dont_do_this }
|
75
|
+
end
|
76
|
+
|
77
|
+
def an_unsourceable_proc
|
78
|
+
{
|
79
|
+
:block => proc { :method_source_error }
|
80
|
+
}[:block]
|
81
|
+
end
|
82
|
+
|
83
|
+
# This ambiguity could be handled, but encourages poorly formatted code and
|
84
|
+
# doesn't seem worth the effort presently.
|
85
|
+
def an_ambiguous_proc_with_hash
|
86
|
+
block = proc { :proc }; {}
|
87
|
+
block
|
88
|
+
end
|
89
|
+
|
90
|
+
def lambda_brace_inline
|
91
|
+
lambda { :lambda }
|
92
|
+
end
|
93
|
+
|
94
|
+
def lambda_brace_multi
|
95
|
+
lambda {
|
96
|
+
:lambda
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
def lambda_do_inline
|
101
|
+
lambda do; :lambda; end
|
102
|
+
end
|
103
|
+
|
104
|
+
def lambda_do_multi
|
105
|
+
lambda do
|
106
|
+
:lambda
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def method_brace_inline
|
111
|
+
Proc.new { :proc }
|
112
|
+
end
|
113
|
+
|
114
|
+
def method_brace_multi
|
115
|
+
Proc.new {
|
116
|
+
:proc
|
117
|
+
}
|
118
|
+
end
|
119
|
+
|
120
|
+
def method_do_inline
|
121
|
+
Proc.new do; :proc; end
|
122
|
+
end
|
123
|
+
|
124
|
+
def method_do_multi
|
125
|
+
Proc.new do
|
126
|
+
:proc
|
127
|
+
end
|
128
|
+
end
|
129
|
+
|
130
|
+
def proc_do_inline
|
131
|
+
proc do; :proc; end
|
132
|
+
end
|
133
|
+
|
134
|
+
def proc_do_multi
|
135
|
+
proc do
|
136
|
+
:proc
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
def proc_brace_inline
|
141
|
+
proc { :proc }
|
142
|
+
end
|
143
|
+
|
144
|
+
def proc_brace_multi
|
145
|
+
proc {
|
146
|
+
:proc
|
147
|
+
}
|
148
|
+
end
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module TCOMethod
|
4
|
+
class BlockWithTCOTest < TestCase
|
5
|
+
include TCOMethod::TestHelpers::Assertions
|
6
|
+
|
7
|
+
Subject = BlockWithTCO
|
8
|
+
|
9
|
+
context "::with_tco" do
|
10
|
+
subject { Subject }
|
11
|
+
|
12
|
+
should "raise ArgumentError if a block is not given" do
|
13
|
+
exception = assert_raises(ArgumentError) { subject.new }
|
14
|
+
assert_match(/block required/i, exception.message)
|
15
|
+
end
|
16
|
+
|
17
|
+
# It would be nice if it could evaluate the block with the same binding, but
|
18
|
+
# I haven't been able to find a way to make that work.
|
19
|
+
should "evaluate the provided block with a different binding" do
|
20
|
+
some_variable = "Hello, world!"
|
21
|
+
assert_raises(NameError) do
|
22
|
+
subject.new { some_variable }
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
should "work with a proc in block form" do
|
27
|
+
tco_block = subject.new do
|
28
|
+
"Hello, world!"
|
29
|
+
end
|
30
|
+
assert_equal "Hello, world!", tco_block.result
|
31
|
+
end
|
32
|
+
|
33
|
+
should "work with a proc in curly-brace form" do
|
34
|
+
tco_block = subject.new { "Hello, world!" }
|
35
|
+
assert_equal "Hello, world!", tco_block.result
|
36
|
+
end
|
37
|
+
|
38
|
+
should "be tail call optimized" do
|
39
|
+
subject.new do
|
40
|
+
class ::TCOTester
|
41
|
+
def fib_yielder(index, back_one = 1, back_two = 0, &block)
|
42
|
+
index > 0 ? yield(back_two) : (return back_two)
|
43
|
+
fib_yielder(index - 1, back_one + back_two, back_one, &block)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
meth = ::TCOTester.new.method(:fib_yielder)
|
49
|
+
assert_equal true, tail_call_optimized?(meth, 5)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
File without changes
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require "test_helper"
|
2
|
+
|
3
|
+
module TCOMethod
|
4
|
+
class MethodReevaluatorTest < TestCase
|
5
|
+
include TCOMethod::TestHelpers::Assertions
|
6
|
+
|
7
|
+
Subject = MethodReevaluator
|
8
|
+
|
9
|
+
context "#initialize" do
|
10
|
+
subject { Subject.method(:new) }
|
11
|
+
|
12
|
+
[TestClass, TestModule].each do |method_owner|
|
13
|
+
method_owner_class = method_owner.class.name.downcase.to_sym
|
14
|
+
|
15
|
+
context "validation" do
|
16
|
+
should "raise ArgumentError unless receiver given" do
|
17
|
+
assert_raises(ArgumentError) do
|
18
|
+
subject.call(nil, :nil?, :instance)
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
should "raise ArgumentError unless method name given" do
|
23
|
+
assert_raises(ArgumentError) do
|
24
|
+
subject.call(method_owner, nil, :instance)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
should "raise ArgumentError unless method owner given" do
|
29
|
+
assert_raises(ArgumentError) do
|
30
|
+
subject.call(method_owner, :class_factorial, nil)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
should "raise TypeError for block methods" do
|
35
|
+
assert_raises(TypeError) do
|
36
|
+
subject.call(method_owner, :singleton_block_method, :class)
|
37
|
+
end
|
38
|
+
assert_raises(TypeError) do
|
39
|
+
subject.call(method_owner, :instance_block_method, :instance)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context "#{method_owner_class} receiver" do
|
45
|
+
context "with module method" do
|
46
|
+
should "raise NameError if no #{method_owner_class} method with given name defined" do
|
47
|
+
assert_raises(NameError) do
|
48
|
+
subject.call(method_owner, :marmalade, method_owner_class)
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
should "re-compile the given method with tail call optimization" do
|
53
|
+
fib_yielder = method_owner.method(:module_fib_yielder)
|
54
|
+
refute tail_call_optimized?(fib_yielder, 5)
|
55
|
+
|
56
|
+
subject.call(method_owner, :module_fib_yielder, :module)
|
57
|
+
tco_fib_yielder = method_owner.method(:module_fib_yielder)
|
58
|
+
assert tail_call_optimized?(tco_fib_yielder, 5)
|
59
|
+
|
60
|
+
assert_equal fib_yielder.source_location, tco_fib_yielder.source_location
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
context "with class method" do
|
65
|
+
should "raise NameError if no class method with given name defined" do
|
66
|
+
assert_raises(NameError) do
|
67
|
+
subject.call(method_owner, :marmalade, :class)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
should "re-compile the given method with tail call optimization" do
|
72
|
+
fib_yielder = method_owner.method(:class_fib_yielder)
|
73
|
+
refute tail_call_optimized?(fib_yielder, 5)
|
74
|
+
|
75
|
+
subject.call(method_owner, :class_fib_yielder, :module)
|
76
|
+
tco_fib_yielder = method_owner.method(:class_fib_yielder)
|
77
|
+
assert tail_call_optimized?(tco_fib_yielder, 5)
|
78
|
+
|
79
|
+
assert_equal fib_yielder.source_location, tco_fib_yielder.source_location
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
context "with instance method" do
|
84
|
+
should "raise NameError if no instance method with given name defined" do
|
85
|
+
assert_raises(NameError) do
|
86
|
+
subject.call(method_owner, :marmalade, :instance)
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
should "re-compile the given method with tail call optimization" do
|
91
|
+
instance_class = instance_class_for_receiver(method_owner)
|
92
|
+
|
93
|
+
fib_yielder = instance_class.new.method(:instance_fib_yielder)
|
94
|
+
refute tail_call_optimized?(fib_yielder, 5)
|
95
|
+
|
96
|
+
subject.call(method_owner, :instance_fib_yielder, :instance)
|
97
|
+
tco_fib_yielder = instance_class.new.method(:instance_fib_yielder)
|
98
|
+
assert tail_call_optimized?(tco_fib_yielder, 5)
|
99
|
+
|
100
|
+
assert_equal fib_yielder.source_location, tco_fib_yielder.source_location
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def instance_class_for_receiver(receiver)
|
108
|
+
return receiver if receiver.is_a?(Class)
|
109
|
+
Class.new { include receiver }
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|