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