tco_method 0.0.1

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 74462b42288aee51eb1533d2a233c9a39c4bea07
4
+ data.tar.gz: 8a4ebe308889ee0edf86489e4491ea3d9d656f52
5
+ SHA512:
6
+ metadata.gz: b3cdc64cac48fd98ca99c52fdc076b9e58e2f209bd8bbb7d5b10bf4feb81af392c4077a891652fadad165d5c3086e200254ef51753eabd45e1e01ec0049210c1
7
+ data.tar.gz: 807f4ae4f07dbaf15412c9b4dbfe66e1e856e06e647dcd2028b498bac587f4b8dddba890de212ef4250f188ea51a162e796164d3010c09ce22a6a6e50564f32c
data/.gitignore ADDED
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/.travis.yml ADDED
@@ -0,0 +1,6 @@
1
+ language: ruby
2
+
3
+ rvm:
4
+ - 2.0.0
5
+ - 2.1.0
6
+ - 2.2.0
data/Gemfile ADDED
@@ -0,0 +1,16 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
4
+
5
+ group :doc do
6
+ gem "yard"
7
+ end
8
+
9
+ group :test do
10
+ gem "coveralls", :require => false
11
+ gem "guard"
12
+ gem "guard-minitest"
13
+ gem "minitest", ">= 3.0"
14
+ gem "mocha"
15
+ gem "simplecov", :require => false
16
+ end
data/Guardfile ADDED
@@ -0,0 +1,5 @@
1
+ guard(:minitest, :all_after_pass => false, :all_on_start => false) do
2
+ watch(%r{^lib/tco_method/(.+)\.rb$}) { |m| "test/unit/#{m[1]}_test.rb" }
3
+ watch(%r{^test/.+_test\.rb$})
4
+ watch(%r{^(?:test/test_helper(.*)|lib/tco_method)\.rb$}) { "test" }
5
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015 Danny
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
data/README.md ADDED
@@ -0,0 +1,92 @@
1
+ # TCOMethod
2
+ [![Gem Version](https://badge.fury.io/rb/tco_method.svg)](http://badge.fury.io/rb/tco_method)
3
+ [![Yard Docs](http://img.shields.io/badge/yard-docs-blue.svg)](http://www.rubydoc.info/gems/tco_method)
4
+ [![Build Status](https://travis-ci.org/tdg5/tco_method.svg)](https://travis-ci.org/tdg5/tco_method)
5
+ [![Coverage Status](https://coveralls.io/repos/tdg5/tco_method/badge.svg)](https://coveralls.io/r/tdg5/tco_method)
6
+ [![Code Climate](https://codeclimate.com/github/tdg5/tco_method/badges/gpa.svg)](https://codeclimate.com/github/tdg5/tco_method)
7
+ [![Dependency Status](https://gemnasium.com/tdg5/tco_method.svg)](https://gemnasium.com/tdg5/tco_method)
8
+
9
+ Provides `TCOMethod::Mixin` for extending Classes and Modules with helper methods
10
+ to facilitate evaluating code and some types of methods with tail call
11
+ optimization enabled. Also provides `TCOMethod.tco_eval` providing an easy means
12
+ to evaluate code strings with tail call optimization enabled.
13
+
14
+ ## Installation
15
+
16
+ Add this line to your application's Gemfile:
17
+
18
+ ```bash
19
+ gem 'tco_method'
20
+ ```
21
+
22
+ And then execute:
23
+
24
+ ```bash
25
+ $ bundle
26
+ ```
27
+
28
+ Or install it yourself as:
29
+
30
+ ```bash
31
+ $ gem install tco_method
32
+ ```
33
+
34
+ ## Usage
35
+
36
+ Require the `TCOMethod` library:
37
+
38
+ ```ruby
39
+ require "tco_method"
40
+ ```
41
+
42
+ Extend a class with the `TCOMethod::Mixin` and enjoy!
43
+
44
+ ```ruby
45
+ class MyClass
46
+ extend TCOMethod::Mixin
47
+
48
+ def factorial(n, acc = 1)
49
+ n <= 1 ? acc : factorial(n - 1, n * acc)
50
+ end
51
+ tco_method :factorial
52
+ end
53
+
54
+ MyClass.new.factorial(10_000).to_s.length
55
+ # => 35660
56
+ ```
57
+
58
+ Or, use `TCOMethod.tco_eval` directly. Cumbersome, but much more flexible and
59
+ powerful:
60
+
61
+ ```ruby
62
+ TCOMethod.tco_eval(<<-CODE)
63
+ class MyClass
64
+ def factorial(n, acc = 1)
65
+ n <= 1 ? acc : factorial(n - 1, n * acc)
66
+ end
67
+ end
68
+ CODE
69
+
70
+ MyClass.new.factorial(10_000).to_s.length
71
+ # => 35660
72
+ ```
73
+
74
+ ## Gotchas
75
+
76
+ The list so far:
77
+
78
+ - Currently only works with methods defined using the `def` keyword.
79
+ - Class annotations use the [`method_source` gem](https://github.com/banister/method_source)
80
+ to retrieve the method source to reevaluate. As a result, class annotations
81
+ can act strangely when used in more dynamic contexts like `irb` or `pry`.
82
+
83
+
84
+ I'm sure there are more and I will document them here as I come across them.
85
+
86
+ ## Contributing
87
+
88
+ 1. Fork it ( https://github.com/tdg5/tco_method/fork )
89
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
90
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
91
+ 4. Push to the branch (`git push origin my-new-feature`)
92
+ 5. Create a new Pull Request
data/Rakefile ADDED
@@ -0,0 +1,9 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new do |t|
5
+ t.libs << "test"
6
+ t.pattern = "test/**/*_test.rb"
7
+ end
8
+
9
+ task :default => :test
data/lib/tco_method.rb ADDED
@@ -0,0 +1,71 @@
1
+ require "method_source"
2
+ require "tco_method/version"
3
+ require "tco_method/method_info"
4
+ require "tco_method/mixin"
5
+
6
+ # The namespace for the TCOMethod gem. Home to private API methods employed by
7
+ # the {TCOMethod::Mixin} module to provide tail call optimized behavior to
8
+ # extending Classes and Modules.
9
+ module TCOMethod
10
+ # Options that must be provided to RubyVM::InstructionSequence in order to
11
+ # compile code with tail call optimization enabled. Beyond simply enabling the
12
+ # `tailcall optimization` option, the `trace_instruction` option must also be
13
+ # disabled because the RubyVM doesn't currently support `set_trace_func` for
14
+ # code that is compiled with tail call optimization.
15
+ ISEQ_OPTIONS = {
16
+ tailcall_optimization: true,
17
+ trace_instruction: false,
18
+ }.freeze
19
+
20
+ # Reevaluates a method with tail call optimization enabled.
21
+ #
22
+ # @note This method is not part of the public API and should not be used
23
+ # directly. See {TCOMethod::Mixin} for a module that provides publicly
24
+ # supported access to the behaviors provided by this method.
25
+ # @param [Class, Module] receiver The Class or Module for which the specified
26
+ # module, class, or instance method should be reevaluated with tail call
27
+ # optimization enabled.
28
+ # @param [String, Symbol] method_name The name of the method that should be
29
+ # reevaluated with tail call optimization enabled.
30
+ # @param [Symbol] method_owner A symbol representing whether the specified
31
+ # method is expected to be owned by a class, module, or instance.
32
+ # @return [Symbol] The symbolized method name.
33
+ # @raise [ArgumentError] Raised if receiver, method_name, or method_owner
34
+ # argument is omitted.
35
+ # @raise [TypeError] Raised if the specified method is not a method that can
36
+ # be reevaluated with tail call optimization enabled.
37
+ def self.reevaluate_method_with_tco(receiver, method_name, method_owner)
38
+ raise ArgumentError, "Receiver required!" unless receiver
39
+ raise ArgumentError, "Method name required!" unless method_name
40
+ raise ArgumentError, "Method owner required!" unless method_owner
41
+ if method_owner == :instance
42
+ existing_method = receiver.instance_method(method_name)
43
+ elsif method_owner == :class || method_owner== :module
44
+ existing_method = receiver.method(method_name)
45
+ end
46
+ method_info = MethodInfo.new(existing_method)
47
+ if method_info.type != :method
48
+ raise TypeError, "Invalid method type: #{method_info.type}"
49
+ end
50
+ code = <<-CODE
51
+ class #{receiver.name}
52
+ #{existing_method.source}
53
+ end
54
+ CODE
55
+ tco_eval(code)
56
+ method_name.to_sym
57
+ end
58
+
59
+ # Provides a mechanism for evaluating Strings of code with tail call
60
+ # optimization enabled.
61
+ #
62
+ # @param [String] code The code to evaluate with tail call optimization
63
+ # enabled.
64
+ # @return [Object] Returns the value of the final expression of the provided
65
+ # code String.
66
+ # @raise [ArgumentError] if the provided code argument is not a String.
67
+ def self.tco_eval(code)
68
+ raise ArgumentError, "Invalid code string!" unless code.is_a?(String)
69
+ RubyVM::InstructionSequence.new(code, nil, nil, nil, ISEQ_OPTIONS).eval
70
+ end
71
+ end
@@ -0,0 +1,39 @@
1
+ module TCOMethod
2
+ # Class encapsulating the behaviors required to extract information about a
3
+ # method from the 14-element Array of data representing the instruction
4
+ # sequence of that method.
5
+ class MethodInfo
6
+ # A collection of those classes that will be recognized as methods and can
7
+ # be used effectively with this class.
8
+ VALID_METHOD_CLASSES = [
9
+ Method,
10
+ UnboundMethod,
11
+ ].freeze
12
+
13
+ # Creates a new MethodInfo instance.
14
+ #
15
+ # @param [Method] method_obj The Method or UnboundMethod object representing
16
+ # the method for which more information should be retrieved.
17
+ # @raise [TypeError] Raised if the provided method object is not a Method or
18
+ # Unbound method.
19
+ # @see VALID_METHOD_CLASSES
20
+ def initialize(method_obj)
21
+ unless VALID_METHOD_CLASSES.any? { |klass| method_obj.is_a?(klass) }
22
+ msg = "Invalid argument! Method or UnboundMethod expected, received #{method_obj.class.name}"
23
+ raise TypeError, msg
24
+ end
25
+ @info = RubyVM::InstructionSequence.of(method_obj).to_a
26
+ end
27
+
28
+ # Returns the type of the method object as reported by the Array of data
29
+ # describing the instruction sequence representing the method.
30
+ #
31
+ # @return [Symbol] A Symbol identifying the type of the instruction
32
+ # sequence. Typical values will be :method or :block, but all of the
33
+ # following are valid return values: :top, :method, :block, :class,
34
+ # :rescue, :ensure, :eval, :main, and :defined_guard.
35
+ def type
36
+ @info[9]
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,45 @@
1
+ module TCOMethod
2
+ # Mixin providing tail call optimization eval and class annotations. When
3
+ # extended by a Class or Module adds methods for evaluating code with tail
4
+ # call optimization enabled and re-evaluating existing methods with tail call
5
+ # optimization enabled.
6
+ module Mixin
7
+ # Class annotation causing the class or module method identified by the
8
+ # given method name to be reevaluated with tail call optimization enabled.
9
+ # Only works for methods defined using the `def` keyword.
10
+ #
11
+ # @param [String, Symbol] method_name The name of the class or module method
12
+ # that should be reeevaluated with tail call optimization enabled.
13
+ # @return [Symbol] The symbolized method name.
14
+ # @see TCOMethod.reevaluate_method_with_tco
15
+ def tco_class_method(method_name)
16
+ TCOMethod.reevaluate_method_with_tco(self, method_name, :class)
17
+ end
18
+ alias_method :tco_module_method, :tco_class_method
19
+
20
+ # Evaluate the given code String with tail call optimization enabled.
21
+ #
22
+ # @param [String] code The code to evaluate with tail call optimization
23
+ # enabled.
24
+ # @return [Object] Returns the value of the final expression of the provided
25
+ # code String.
26
+ # @raise [ArgumentError] if the provided code argument is not a String.
27
+ # @see TCOMethod.tco_eval
28
+ def tco_eval(code)
29
+ TCOMethod.tco_eval(code)
30
+ end
31
+
32
+ # Class annotation causing the instance method identified by the given
33
+ # method name to be reevaluated with tail call optimization enabled. Only
34
+ # works for methods defined using the `def` keyword.
35
+ #
36
+ # @param [String, Symbol] method_name The name of the instance method that
37
+ # should be reeevaluated with tail call optimization enabled.
38
+ # @return [Symbol] The symbolized method name.
39
+ # @see TCOMethod.reevaluate_method_with_tco
40
+ def tco_method(method_name)
41
+ TCOMethod.reevaluate_method_with_tco(self, method_name, :instance)
42
+ method_name.to_sym
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,4 @@
1
+ module TCOMethod
2
+ # The version of the TCOMethod gem.
3
+ VERSION = "0.0.1"
4
+ end
@@ -0,0 +1,27 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path("../lib", __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require "tco_method/version"
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "tco_method"
8
+ spec.version = TCOMethod::VERSION
9
+ spec.authors = ["Danny Guinther"]
10
+ spec.email = ["dannyguinther@gmail.com"]
11
+ spec.summary = %q{Simplifies creating tail call optimized procs/lambdas/methods in Ruby.}
12
+ spec.description = %q{Simplifies creating tail call optimized procs/lambdas/methods in Ruby.}
13
+ spec.homepage = "https://github.com/tdg5/tco_method"
14
+ spec.license = "MIT"
15
+
16
+ spec.required_ruby_version = "~> 2"
17
+
18
+ spec.files = `git ls-files -z`.split("\x0")
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
20
+ spec.test_files = spec.files.grep(%r{^test/})
21
+ spec.require_paths = ["lib"]
22
+
23
+ spec.add_dependency "method_source", "~> 0"
24
+
25
+ spec.add_development_dependency "bundler", "~> 1.6"
26
+ spec.add_development_dependency "rake", "~> 0"
27
+ end
@@ -0,0 +1,25 @@
1
+ if ENV["CI"]
2
+ require "simplecov"
3
+ require "coveralls"
4
+ Coveralls.wear!
5
+ SimpleCov.formatter = Coveralls::SimpleCov::Formatter
6
+ SimpleCov.root(File.expand_path("../..", __FILE__))
7
+ end
8
+
9
+ require "minitest/autorun"
10
+ require "mocha/setup"
11
+ require "tco_method"
12
+
13
+ require "test_helpers/vm_stack_helper"
14
+ require "test_helpers/factorial_stack_buster_helper"
15
+ require "test_helpers/vanilla_stack_buster_helper"
16
+
17
+ # Use alternate shoulda-style DSL for tests
18
+ class TCOMethod::TestCase < Minitest::Spec
19
+ class << self
20
+ alias :setup :before
21
+ alias :teardown :after
22
+ alias :context :describe
23
+ alias :should :it
24
+ end
25
+ end
@@ -0,0 +1,31 @@
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
@@ -0,0 +1,60 @@
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
@@ -0,0 +1,25 @@
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
@@ -0,0 +1,16 @@
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
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,64 @@
1
+ require "test_helper"
2
+
3
+ class MethodInfoTest < TCOMethod::TestCase
4
+ Subject = TCOMethod::MethodInfo
5
+
6
+ TestClass = Class.new do
7
+ class << self; define_method(:class_block_method) { }; end
8
+ def self.class_def_method; end
9
+ define_method(:instance_block_method) { }
10
+ def instance_def_method; end
11
+ end
12
+
13
+ context "#initialize" do
14
+ should "raise TypeError unless given a Method object" do
15
+ non_methods = [
16
+ proc { },
17
+ lambda { },
18
+ Proc.new { },
19
+ ]
20
+ non_methods.each do |non_method|
21
+ assert_raises(TypeError) do
22
+ Subject.new(non_method)
23
+ end
24
+ end
25
+ end
26
+
27
+ should "accept Method objects defined on a class using def" do
28
+ method_obj = TestClass.method(:class_def_method)
29
+ assert_kind_of Method, method_obj
30
+ assert_kind_of Subject, Subject.new(method_obj)
31
+ end
32
+
33
+ should "accept UnboundMethod objects defined on an instance using def" do
34
+ method_obj = TestClass.instance_method(:instance_def_method)
35
+ assert_kind_of UnboundMethod, method_obj
36
+ assert_kind_of Subject, Subject.new(method_obj)
37
+ end
38
+
39
+ should "accept Method objects defined on a class using define_method" do
40
+ method_obj = TestClass.method(:class_block_method)
41
+ assert_kind_of Method, method_obj
42
+ assert_kind_of Subject, Subject.new(method_obj)
43
+ end
44
+
45
+ should "accept UnboundMethod objects defined on an instance using define_method" do
46
+ method_obj = TestClass.instance_method(:instance_block_method)
47
+ assert_kind_of UnboundMethod, method_obj
48
+ assert_kind_of Subject, Subject.new(method_obj)
49
+ end
50
+ end
51
+
52
+ context "#type" do
53
+ subject { TestClass.new }
54
+ should "return :method for methods defined using def" do
55
+ assert_equal :method, Subject.new(TestClass.method(:class_def_method)).type
56
+ assert_equal :method, Subject.new(subject.method(:instance_def_method)).type
57
+ end
58
+
59
+ should "return :block for methods defined using define_method" do
60
+ assert_equal :block, Subject.new(TestClass.method(:class_block_method)).type
61
+ assert_equal :block, Subject.new(subject.method(:instance_block_method)).type
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,39 @@
1
+ require "test_helper"
2
+
3
+ class MixinTest < TCOMethod::TestCase
4
+ include TCOMethod::TestHelpers::FactorialStackBusterHelper
5
+
6
+ TestClass = Class.new do
7
+ extend TCOMethod::Mixin
8
+ end
9
+
10
+ subject { TestClass }
11
+
12
+ [:tco_class_method, :tco_module_method].each do |method_alias|
13
+ context "##{method_alias}" do
14
+ should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
15
+ method_name = :some_method
16
+ args = [TestClass, method_name, :class]
17
+ TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
18
+ subject.send(method_alias, method_name)
19
+ end
20
+ end
21
+ end
22
+
23
+ context "#tco_eval" do
24
+ should "call TCOMethod.eval with expected arguments" do
25
+ code = "some_code"
26
+ TCOMethod.expects(:tco_eval).with(code)
27
+ subject.tco_eval(code)
28
+ end
29
+ end
30
+
31
+ context "#tco_method" do
32
+ should "call TCOMethod.reevaluate_method_with_tco with expected arguments" do
33
+ method_name = :some_method
34
+ args = [TestClass, method_name, :instance]
35
+ TCOMethod.expects(:reevaluate_method_with_tco).with(*args)
36
+ subject.tco_method(method_name)
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,139 @@
1
+ require "test_helper"
2
+
3
+ class TCOMethodTest < TCOMethod::TestCase
4
+ include TCOMethod::TestHelpers::FactorialStackBusterHelper
5
+
6
+ Subject = TCOMethod
7
+
8
+ TestClass = Class.new do
9
+ extend TCOMethod::Mixin
10
+
11
+ class << self;
12
+ define_method(:class_block_method) { }
13
+ end
14
+
15
+ def self.class_factorial(n, acc = 1)
16
+ n <= 1 ? acc : class_factorial(n - 1, n * acc)
17
+ end
18
+
19
+ define_method(:instance_block_method) { }
20
+
21
+ def instance_factorial(n, acc = 1)
22
+ n <= 1 ? acc : instance_factorial(n - 1, n * acc)
23
+ end
24
+ end
25
+
26
+ subject { Subject }
27
+
28
+ context Subject.name do
29
+ should "be defined" do
30
+ assert defined?(subject), "Expected #{subject.name} to be defined!"
31
+ end
32
+ end
33
+
34
+ context "::tco_eval" do
35
+ should "raise ArgumentError unless code is a String" do
36
+ bad_code = [
37
+ :bad_code,
38
+ 5,
39
+ proc { puts "hello" },
40
+ ]
41
+ bad_code.each do |non_code|
42
+ assert_raises(ArgumentError) do
43
+ subject.tco_eval(non_code)
44
+ end
45
+ end
46
+ end
47
+
48
+ should "compile the given code with tail call optimization" do
49
+ FactorialEvalDummy = dummy_class = Class.new
50
+ subject.tco_eval(<<-CODE)
51
+ class #{dummy_class.name}
52
+ def factorial(n, acc = 1)
53
+ n <= 1 ? acc : factorial(n - 1, n * acc)
54
+ end
55
+ end
56
+ CODE
57
+
58
+ # Exceed maximum available stack depth by 100 for good measure
59
+ factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
60
+ assert_unoptimized_factorial_stack_overflow(factorial_seed)
61
+
62
+ expected_result = iterative_factorial(factorial_seed)
63
+ assert_equal expected_result, dummy_class.new.factorial(factorial_seed)
64
+ end
65
+ end
66
+
67
+ context "::reevaluate_method_with_tco" do
68
+ subject { Subject.method(:reevaluate_method_with_tco) }
69
+
70
+ context "validation" do
71
+ should "raise ArgumentError unless receiver given" do
72
+ assert_raises(ArgumentError) do
73
+ subject.call(nil, :nil?, :instance)
74
+ end
75
+ end
76
+
77
+ should "raise ArgumentError unless method name given" do
78
+ assert_raises(ArgumentError) do
79
+ subject.call(TestClass, nil, :instance)
80
+ end
81
+ end
82
+
83
+ should "raise ArgumentError unless method owner given" do
84
+ assert_raises(ArgumentError) do
85
+ subject.call(TestClass, :class_factorial, nil)
86
+ end
87
+ end
88
+
89
+ should "raise TypeError for block methods" do
90
+ assert_raises(TypeError) do
91
+ subject.call(TestClass, :class_block_method, :class)
92
+ end
93
+ assert_raises(TypeError) do
94
+ subject.call(TestClass, :instance_block_method, :instance)
95
+ end
96
+ end
97
+ end
98
+
99
+ context "with class method" do
100
+ should "raise NameError if no class method with given name defined" do
101
+ assert_raises(NameError) do
102
+ subject.call(TestClass, :marmalade, :class)
103
+ end
104
+ end
105
+
106
+ should "re-compile the given method with tail call optimization" do
107
+ # Exceed maximum available stack depth by 100 for good measure
108
+ factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
109
+ assert_raises(SystemStackError) do
110
+ TestClass.class_factorial(factorial_seed)
111
+ end
112
+
113
+ subject.call(TestClass, :class_factorial, :class)
114
+ expected_result = iterative_factorial(factorial_seed)
115
+ assert_equal expected_result, TestClass.class_factorial(factorial_seed)
116
+ end
117
+ end
118
+
119
+ context "with instance method" do
120
+ should "raise NameError if no instance method with given name defined" do
121
+ assert_raises(NameError) do
122
+ subject.call(TestClass, :marmalade, :instance)
123
+ end
124
+ end
125
+
126
+ should "re-compile the given method with tail call optimization" do
127
+ # Exceed maximum available stack depth by 100 for good measure
128
+ factorial_seed = factorial_stack_buster_stack_depth_remaining + 100
129
+ assert_raises(SystemStackError) do
130
+ TestClass.new.instance_factorial(factorial_seed)
131
+ end
132
+
133
+ subject.call(TestClass, :instance_factorial, :instance)
134
+ expected_result = iterative_factorial(factorial_seed)
135
+ assert_equal expected_result, TestClass.new.instance_factorial(factorial_seed)
136
+ end
137
+ end
138
+ end
139
+ end
metadata ADDED
@@ -0,0 +1,117 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: tco_method
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Danny Guinther
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-03-12 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: method_source
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '1.6'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '1.6'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ description: Simplifies creating tail call optimized procs/lambdas/methods in Ruby.
56
+ email:
57
+ - dannyguinther@gmail.com
58
+ executables: []
59
+ extensions: []
60
+ extra_rdoc_files: []
61
+ files:
62
+ - ".gitignore"
63
+ - ".travis.yml"
64
+ - Gemfile
65
+ - Guardfile
66
+ - LICENSE
67
+ - README.md
68
+ - Rakefile
69
+ - lib/tco_method.rb
70
+ - lib/tco_method/method_info.rb
71
+ - lib/tco_method/mixin.rb
72
+ - lib/tco_method/version.rb
73
+ - tco_method.gemspec
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
80
+ - test/unit/method_info_test.rb
81
+ - test/unit/mixin_test.rb
82
+ - test/unit/tco_method_test.rb
83
+ homepage: https://github.com/tdg5/tco_method
84
+ licenses:
85
+ - MIT
86
+ metadata: {}
87
+ post_install_message:
88
+ rdoc_options: []
89
+ require_paths:
90
+ - lib
91
+ required_ruby_version: !ruby/object:Gem::Requirement
92
+ requirements:
93
+ - - "~>"
94
+ - !ruby/object:Gem::Version
95
+ version: '2'
96
+ required_rubygems_version: !ruby/object:Gem::Requirement
97
+ requirements:
98
+ - - ">="
99
+ - !ruby/object:Gem::Version
100
+ version: '0'
101
+ requirements: []
102
+ rubyforge_project:
103
+ rubygems_version: 2.2.2
104
+ signing_key:
105
+ specification_version: 4
106
+ summary: Simplifies creating tail call optimized procs/lambdas/methods in Ruby.
107
+ test_files:
108
+ - 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
114
+ - test/unit/method_info_test.rb
115
+ - test/unit/mixin_test.rb
116
+ - test/unit/tco_method_test.rb
117
+ has_rdoc: