tco_method 0.1.0 → 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 +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
|