tco_method 0.0.1

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