tco_method 0.0.4 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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