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.
@@ -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
@@ -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.reevaluate_method_with_tco
16
+ # @see TCOMethod::MethodReevaluator
15
17
  def tco_module_method(method_name)
16
- TCOMethod.reevaluate_method_with_tco(self, method_name, :module)
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.reevaluate_method_with_tco
41
+ # @see TCOMethod::MethodReevaluator
40
42
  def tco_method(method_name)
41
- TCOMethod.reevaluate_method_with_tco(self, method_name, :instance)
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
@@ -1,4 +1,4 @@
1
1
  module TCOMethod
2
2
  # The version of the TCOMethod gem.
3
- VERSION = "0.1.0"
3
+ VERSION = "0.2.0"
4
4
  end
File without changes
@@ -11,8 +11,8 @@ end
11
11
  require "minitest/autorun"
12
12
  require "mocha/setup"
13
13
  require "tco_method"
14
-
15
- require_relative "test_helpers/assertions"
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