tco_method 0.0.4 → 0.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 11960ee06eb5b374d740739ce2c713ac378614a7
4
- data.tar.gz: 1f745c27ee5c95ab29a04e9880167f3d57ba3083
3
+ metadata.gz: 5b38db19eea395f824f107cdf26128b24f4a0de7
4
+ data.tar.gz: 061f302f00571f8d3290b2c33deb15c8e3e12c85
5
5
  SHA512:
6
- metadata.gz: ada2e8cd1fc60a513afe1d8b41648af9ad8d7c180671f952569f1b39d6913fde93be9a91dec4456d7de48706c08503b87aded56e0056f3f5d3d1f58356b60de6
7
- data.tar.gz: 26ff23af6d83115e64012b09e2d7c00dc2632a9aed6d280a7dec52c4ab5f225d415a73562e360ce7f78fc7ad8b8ffa585d7ef86ec120d9bddb26227b8bd6d0d9
6
+ metadata.gz: 5582d4e9e64fc235526dc5c5504ad041e1736252f9017c45934990f0bbd8c005327dc12ecad192373a1cce42a00e76209b5abff4038248170105860b79222987
7
+ data.tar.gz: 8e38fa82f97b0cb9f60678ef5342f59f7c7042ed5cd4104f90b49a66cb82de4e9c6571cd36aaa9ea8695d2f3c4d885c718794372c19a8f15bd5f7a0e21d7503f
@@ -1,4 +1,4 @@
1
1
  module TCOMethod
2
2
  # The version of the TCOMethod gem.
3
- VERSION = "0.0.4"
3
+ VERSION = "0.1.0"
4
4
  end
data/lib/tco_method.rb CHANGED
@@ -53,7 +53,9 @@ module TCOMethod
53
53
  #{existing_method.source}
54
54
  end
55
55
  CODE
56
- tco_eval(code)
56
+
57
+ file, line = existing_method.source_location
58
+ tco_eval(code, file, File.dirname(file), line - 1)
57
59
  method_name.to_sym
58
60
  end
59
61
 
@@ -65,8 +67,8 @@ module TCOMethod
65
67
  # @return [Object] Returns the value of the final expression of the provided
66
68
  # code String.
67
69
  # @raise [ArgumentError] if the provided code argument is not a String.
68
- def self.tco_eval(code)
70
+ def self.tco_eval(code, file = nil, path = nil, line = nil)
69
71
  raise ArgumentError, "Invalid code string!" unless code.is_a?(String)
70
- RubyVM::InstructionSequence.new(code, nil, nil, nil, ISEQ_OPTIONS).eval
72
+ RubyVM::InstructionSequence.new(code, file, path, line, ISEQ_OPTIONS).eval
71
73
  end
72
74
  end
data/test/test_helper.rb CHANGED
@@ -12,9 +12,7 @@ require "minitest/autorun"
12
12
  require "mocha/setup"
13
13
  require "tco_method"
14
14
 
15
- require "test_helpers/vm_stack_helper"
16
- require "test_helpers/factorial_stack_buster_helper"
17
- require "test_helpers/vanilla_stack_buster_helper"
15
+ require_relative "test_helpers/assertions"
18
16
 
19
17
  # Use alternate shoulda-style DSL for tests
20
18
  class TCOMethod::TestCase < Minitest::Spec
@@ -0,0 +1,28 @@
1
+ module TCOMethod
2
+ module TestHelpers
3
+ module Assertions
4
+ def assert_tail_call_optimized(method, *args)
5
+ is_tco = tail_call_optimized(method, *args)
6
+ msg = "Expected method #{method.name} to be tail call optimized"
7
+ assert is_tco, msg
8
+ end
9
+
10
+ def refute_tail_call_optimized(method, *args)
11
+ is_tco = tail_call_optimized(method, *args)
12
+ msg = "Expected method #{method.name} not to be tail call optimized"
13
+ refute is_tco, msg
14
+ end
15
+
16
+ def tail_call_optimized?(method, *args)
17
+ initial_length = nil
18
+ method.call(*args) do
19
+ if initial_length.nil?
20
+ initial_length = caller.length
21
+ else
22
+ break initial_length == caller.length
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
28
+ end
@@ -1,8 +1,6 @@
1
1
  require "test_helper"
2
2
 
3
3
  class MixinTest < TCOMethod::TestCase
4
- include TCOMethod::TestHelpers::FactorialStackBusterHelper
5
-
6
4
  TestClass = Class.new { extend TCOMethod::Mixin }
7
5
  TestModule = Module.new { extend TCOMethod::Mixin }
8
6
 
@@ -1,7 +1,7 @@
1
1
  require "test_helper"
2
2
 
3
3
  class TCOMethodTest < TCOMethod::TestCase
4
- include TCOMethod::TestHelpers::FactorialStackBusterHelper
4
+ include TCOMethod::TestHelpers::Assertions
5
5
 
6
6
  Subject = TCOMethod
7
7
 
@@ -14,26 +14,34 @@ class TCOMethodTest < TCOMethod::TestCase
14
14
 
15
15
  # Equivalent to the below, but provides a target for verifying that
16
16
  # tco_module_method works on Classes and tco_class_method works on Modules.
17
- def self.module_factorial(n, acc = 1)
18
- n <= 1 ? acc : module_factorial(n - 1, n * acc)
17
+ def self.module_fib_yielder(index, back_one = 1, back_two = 0, &block)
18
+ yield back_two if index > 0
19
+ index < 1 ? back_two : module_fib_yielder(index - 1, back_one + back_two, back_one, &block)
19
20
  end
20
21
 
21
22
  # Equivalent to the above, but provides a target for verifying that
22
23
  # tco_module_method works on Classes and tco_class_method works on Modules.
23
- def self.class_factorial(n, acc = 1)
24
- n <= 1 ? acc : class_factorial(n - 1, n * acc)
24
+ def self.class_fib_yielder(index, back_one = 1, back_two = 0, &block)
25
+ yield back_two if index > 0
26
+ index < 1 ? back_two : class_fib_yielder(index - 1, back_one + back_two, back_one, &block)
25
27
  end
26
28
 
27
29
  define_method(:instance_block_method) { }
28
30
 
29
- def instance_factorial(n, acc = 1)
30
- n <= 1 ? acc : instance_factorial(n - 1, n * acc)
31
+ # Equivalent to the above, but provides a target for verifying that
32
+ # instance methods work for both Classes and Modules
33
+ def instance_fib_yielder(index, back_one = 1, back_two = 0, &block)
34
+ yield back_two if index > 0
35
+ index < 1 ? back_two : instance_fib_yielder(index - 1, back_one + back_two, back_one, &block)
31
36
  end
32
37
  end
33
38
 
34
39
  TestModule = Module.new(&test_subject_builder)
35
40
  TestClass = Class.new(&test_subject_builder)
36
41
 
42
+ # Grab source before it's recompiled for use later
43
+ InstanceFibYielderSource = TestClass.instance_method(:instance_fib_yielder).source
44
+
37
45
  subject { Subject }
38
46
 
39
47
  context Subject.name do
@@ -57,21 +65,15 @@ class TCOMethodTest < TCOMethod::TestCase
57
65
  end
58
66
 
59
67
  should "compile the given code with tail call optimization" do
60
- FactorialEvalDummy = dummy_class = Class.new
68
+ EvalDummy = dummy_class = Class.new
61
69
  subject.tco_eval(<<-CODE)
62
70
  class #{dummy_class.name}
63
- def factorial(n, acc = 1)
64
- n <= 1 ? acc : factorial(n - 1, n * acc)
65
- end
71
+ #{InstanceFibYielderSource}
66
72
  end
67
73
  CODE
68
74
 
69
- # Exceed maximum available stack depth by 100 for good measure
70
- factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
71
- assert_unoptimized_factorial_stack_overflow(factorial_seed)
72
-
73
- expected_result = iterative_factorial(factorial_seed)
74
- assert_equal expected_result, dummy_class.new.factorial(factorial_seed)
75
+ fib_yielder = dummy_class.new.method(:instance_fib_yielder)
76
+ assert tail_call_optimized?(fib_yielder, 5)
75
77
  end
76
78
  end
77
79
 
@@ -119,15 +121,14 @@ class TCOMethodTest < TCOMethod::TestCase
119
121
  end
120
122
 
121
123
  should "re-compile the given method with tail call optimization" do
122
- # Exceed maximum available stack depth by 100 for good measure
123
- factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
124
- assert_raises(SystemStackError) do
125
- method_owner.module_factorial(factorial_seed)
126
- end
124
+ fib_yielder = method_owner.method(:module_fib_yielder)
125
+ refute tail_call_optimized?(fib_yielder, 5)
126
+
127
+ subject.call(method_owner, :module_fib_yielder, :module)
128
+ tco_fib_yielder = method_owner.method(:module_fib_yielder)
129
+ assert tail_call_optimized?(tco_fib_yielder, 5)
127
130
 
128
- subject.call(method_owner, :module_factorial, :module)
129
- expected_result = iterative_factorial(factorial_seed)
130
- assert_equal expected_result, method_owner.module_factorial(factorial_seed)
131
+ assert_equal fib_yielder.source_location, tco_fib_yielder.source_location
131
132
  end
132
133
  end
133
134
 
@@ -139,15 +140,14 @@ class TCOMethodTest < TCOMethod::TestCase
139
140
  end
140
141
 
141
142
  should "re-compile the given method with tail call optimization" do
142
- # Exceed maximum available stack depth by 100 for good measure
143
- factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
144
- assert_raises(SystemStackError) do
145
- method_owner.class_factorial(factorial_seed)
146
- end
143
+ fib_yielder = method_owner.method(:class_fib_yielder)
144
+ refute tail_call_optimized?(fib_yielder, 5)
145
+
146
+ subject.call(method_owner, :class_fib_yielder, :module)
147
+ tco_fib_yielder = method_owner.method(:class_fib_yielder)
148
+ assert tail_call_optimized?(tco_fib_yielder, 5)
147
149
 
148
- subject.call(method_owner, :class_factorial, method_owner_class)
149
- expected_result = iterative_factorial(factorial_seed)
150
- assert_equal expected_result, method_owner.class_factorial(factorial_seed)
150
+ assert_equal fib_yielder.source_location, tco_fib_yielder.source_location
151
151
  end
152
152
  end
153
153
 
@@ -159,16 +159,16 @@ class TCOMethodTest < TCOMethod::TestCase
159
159
  end
160
160
 
161
161
  should "re-compile the given method with tail call optimization" do
162
- # Exceed maximum available stack depth by 100 for good measure
163
- factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
164
162
  instance_class = instance_class_for_receiver(method_owner)
165
- assert_raises(SystemStackError) do
166
- instance_class.new.instance_factorial(factorial_seed)
167
- end
168
163
 
169
- subject.call(method_owner, :instance_factorial, :instance)
170
- expected_result = iterative_factorial(factorial_seed)
171
- assert_equal expected_result, instance_class.new.instance_factorial(factorial_seed)
164
+ fib_yielder = instance_class.new.method(:instance_fib_yielder)
165
+ refute tail_call_optimized?(fib_yielder, 5)
166
+
167
+ subject.call(method_owner, :instance_fib_yielder, :instance)
168
+ tco_fib_yielder = instance_class.new.method(:instance_fib_yielder)
169
+ assert tail_call_optimized?(tco_fib_yielder, 5)
170
+
171
+ assert_equal fib_yielder.source_location, tco_fib_yielder.source_location
172
172
  end
173
173
  end
174
174
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: tco_method
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.4
4
+ version: 0.1.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Danny Guinther
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-03-14 00:00:00.000000000 Z
11
+ date: 2015-10-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: method_source
@@ -72,11 +72,7 @@ files:
72
72
  - lib/tco_method/version.rb
73
73
  - tco_method.gemspec
74
74
  - test/test_helper.rb
75
- - test/test_helpers/factorial_stack_buster_helper.rb
76
- - test/test_helpers/stack_busters/factorial_stack_buster.rb
77
- - test/test_helpers/stack_busters/vanilla_stack_buster.rb
78
- - test/test_helpers/vanilla_stack_buster_helper.rb
79
- - test/test_helpers/vm_stack_helper.rb
75
+ - test/test_helpers/assertions.rb
80
76
  - test/unit/method_info_test.rb
81
77
  - test/unit/mixin_test.rb
82
78
  - test/unit/tco_method_test.rb
@@ -106,11 +102,7 @@ specification_version: 4
106
102
  summary: Simplifies compiling code with tail call optimization in MRI Ruby.
107
103
  test_files:
108
104
  - test/test_helper.rb
109
- - test/test_helpers/factorial_stack_buster_helper.rb
110
- - test/test_helpers/stack_busters/factorial_stack_buster.rb
111
- - test/test_helpers/stack_busters/vanilla_stack_buster.rb
112
- - test/test_helpers/vanilla_stack_buster_helper.rb
113
- - test/test_helpers/vm_stack_helper.rb
105
+ - test/test_helpers/assertions.rb
114
106
  - test/unit/method_info_test.rb
115
107
  - test/unit/mixin_test.rb
116
108
  - test/unit/tco_method_test.rb
@@ -1,31 +0,0 @@
1
- require "test_helpers/stack_busters/factorial_stack_buster"
2
-
3
- module TCOMethod
4
- module TestHelpers
5
- module FactorialStackBusterHelper
6
- extend Forwardable
7
-
8
- def_delegators "#{self.name}.stack_buster", :unoptimized_factorial
9
-
10
- long_alias = :factorial_stack_buster_stack_depth_remaining
11
- def_delegator "#{self.name}.stack_buster", :stack_depth_remaining, long_alias
12
-
13
- def self.stack_buster
14
- @@stack_buster ||= StackBusters::FactorialStackBuster.new
15
- end
16
-
17
- def assert_unoptimized_factorial_stack_overflow(depth)
18
- # Subtract 1 to account for this methiod call since result should be
19
- # relative to caller
20
- unoptimized_factorial(depth - 1)
21
- assert false, "Factorial for depth #{depth} did not overflow!"
22
- rescue SystemStackError
23
- assert true
24
- end
25
-
26
- def iterative_factorial(n)
27
- (2..n).inject(1, :*)
28
- end
29
- end
30
- end
31
- end
@@ -1,60 +0,0 @@
1
- require "pry"
2
-
3
- module TCOMethod
4
- module TestHelpers
5
- module StackBusters
6
- class FactorialStackBuster
7
- include VMStackHelper
8
-
9
- def frame_size
10
- @frame_size ||= vm_stack_size / stack_overflow_threshold
11
- end
12
-
13
- def unoptimized_factorial(n, acc = 1)
14
- n <= 1 ? acc : unoptimized_factorial(n - 1, n * acc)
15
- end
16
-
17
- def stack_depth_remaining
18
- stack_depth_remaining_for_frame_size(frame_size)
19
- end
20
-
21
- private
22
-
23
- # Stack buster based on binary search for point of stack oveflow of
24
- # unoptimized_factorial
25
- def stack_overflow_threshold
26
- # Use a frame size that's larger than the expected frame size to ensure
27
- # that limit is less than the point of overflow
28
- limit = last_good_limit = stack_depth_limit_for_frame_size(LARGEST_VM_STACK_SIZE * 2)
29
- last_overflow_limit = nil
30
- # Determine an upper-bound for binary search
31
- loop do
32
- begin
33
- unoptimized_factorial(limit)
34
- last_good_limit = limit
35
- limit *= 2
36
- rescue SystemStackError
37
- last_overflow_limit = limit
38
- break
39
- end
40
- end
41
-
42
- # Reset for binary search for point of stack overflow
43
- limit = last_good_limit
44
- loop do
45
- return last_overflow_limit if last_overflow_limit == last_good_limit + 1
46
- begin
47
- half_the_distance_to_overflow = (last_overflow_limit - limit) / 2
48
- limit += half_the_distance_to_overflow
49
- unoptimized_factorial(limit)
50
- last_good_limit = limit
51
- rescue SystemStackError
52
- last_overflow_limit = limit
53
- limit = last_good_limit
54
- end
55
- end
56
- end
57
- end
58
- end
59
- end
60
- end
@@ -1,25 +0,0 @@
1
- module TCOMethod
2
- module TestHelpers
3
- module StackBusters
4
- class VanillaStackBuster
5
- include VMStackHelper
6
-
7
- def frame_size
8
- @frame_size ||= vm_stack_size / stack_overflow_threshold
9
- end
10
-
11
- def stack_depth_remaining
12
- stack_depth_remaining_for_frame_size(frame_size)
13
- end
14
-
15
- private
16
-
17
- def stack_overflow_threshold(depth = 1)
18
- stack_overflow_threshold(depth + 1)
19
- rescue SystemStackError
20
- depth
21
- end
22
- end
23
- end
24
- end
25
- end
@@ -1,16 +0,0 @@
1
- require "test_helpers/stack_busters/vanilla_stack_buster"
2
-
3
- module TCOMethod
4
- module TestHelpers
5
- module VanillaStackBusterHelper
6
- extend Forwardable
7
-
8
- def_delegator "#{self.name}.stack_buster", :stack_depth_remaining, :vanilla_stack_depth_remaining
9
-
10
- def self.stack_buster
11
- @@stack_buster ||= StackBusters::VanillaStackBuster.new
12
- end
13
- private_class_method :stack_buster
14
- end
15
- end
16
- end
@@ -1,28 +0,0 @@
1
- module TCOMethod
2
- module TestHelpers
3
- module VMStackHelper
4
- LARGEST_VM_STACK_SIZE = 128
5
-
6
- # Calculate the maximum number of times a stack frame of a given size could
7
- # be repeated before running out of room on the stack.
8
- def stack_depth_limit_for_frame_size(frame_size)
9
- vm_stack_size / frame_size
10
- end
11
- module_function :stack_depth_limit_for_frame_size
12
-
13
- # Calculate how many more times a frame could be repeated before running out of
14
- # room on the stack.
15
- def stack_depth_remaining_for_frame_size(frame_size)
16
- stack_depth_limit_for_frame_size(frame_size) - caller.length + 1
17
- end
18
- module_function :stack_depth_remaining_for_frame_size
19
-
20
- # Determine maximum size of VM stack for a single thread based on
21
- # environment or default value.
22
- def vm_stack_size
23
- ENV["RUBY_THREAD_VM_STACK_SIZE"] || RubyVM::DEFAULT_PARAMS[:thread_vm_stack_size]
24
- end
25
- module_function :vm_stack_size
26
- end
27
- end
28
- end