ractorize 0.0.3 → 0.0.4

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
  SHA256:
3
- metadata.gz: f4125b52255fca881ea212e46e55e4c795e79b39f8d8369580022e67db2eb9a8
4
- data.tar.gz: 91f58df84879ac297d0111f5880faf8ea95804fdb4ae17345f7702e579fbfb42
3
+ metadata.gz: 65a392a1b2b1f65b1886a6650dd712848aa8707f4ab76617e14ba614684660cc
4
+ data.tar.gz: 330eca315af85099f83374687a8526a60a239f9c84e0908b7a77a33b69130722
5
5
  SHA512:
6
- metadata.gz: 691e4eb2b5a1d436b366e93256e7af917308ea3ca3292205791023f92948202464279d4e11e3251c9d842ac63e79a1d5e1a3c377747e07ced9956722a67e2d68
7
- data.tar.gz: ef0e4f07687797d2036f326674150ba9ff1e5e07a604936109f4ff0e0811bb19de85d22a5834c8684ed49860cc4f27943d3a292960378c46a2c661f561e91b05
6
+ metadata.gz: ec5f2a85e7efcc3c045051cb7a45b83900c82bbc1830deb744fed115c63acf0850067ea03e1b6a18198f19a927a6240ae09999c2fdbf9b316f924eb6fdc48557
7
+ data.tar.gz: c2840a3f37c61353ff66f70996043b78d997cfb969fd6946fc549aa6f572f9257d207e043fe63a388ef486ca58cd21021909b1755c5c06973cecc4d5490b6a56
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ ## [0.0.4] - 2026-05-23
2
+
3
+ - Prevent thunks from crossing ractor boundaries
4
+ - Create instances of ractorized classes inside the ractor instead of moving them
5
+ - Add support for methods that take blocks
6
+ - Add #to_s/#inspect to RactorizedObject to help with debugging
7
+ - Use #__send__ instead of #send to work with BasicObject
8
+
1
9
  ## [0.0.3] - 2026-05-21
2
10
 
3
11
  - Treat ==, !=, and ! as predicates and delegate them to the ractor (as well as #equal?)
@@ -1,16 +1,14 @@
1
1
  module Ractorize
2
2
  class RactorizedClass
3
3
  class << self
4
- attr_accessor :target_class
5
-
6
4
  def [](klass)
7
5
  ractorized_class = Class.new(RactorizedClass)
8
- ractorized_class.target_class = klass
6
+ ractorized_class.define_singleton_method(:target_class, Ractor.shareable_proc { klass })
9
7
  ractorized_class
10
8
  end
11
9
 
12
10
  def new(...)
13
- Ractorize.ractorize_object(target_class.new(...))
11
+ RactorizedObject.new(:class, target_class, ...)
14
12
  end
15
13
 
16
14
  def method_missing(method_name, ...)
@@ -3,18 +3,37 @@ require_relative "thunk"
3
3
 
4
4
  module Ractorize
5
5
  class RactorizedObject < BasicObject
6
- def initialize(outside_object)
6
+ def initialize(mode, *args, **opts, &block)
7
7
  @ractor = ::Ractor.new(&RACTOR_PROC)
8
8
 
9
- # It doesn't seem like we have a way to move the object into the ractor via its constructor so do
10
- # it with #<< instead.
11
- if ::Ractor.shareable?(outside_object)
12
- @ractor << outside_object
9
+ case mode
10
+ when :object
11
+ @ractor << :object
12
+
13
+ outside_object = args.first
14
+
15
+ ::Ractorize.resolve_all_thunks(outside_object)
16
+
17
+ if ::Ractor.shareable?(outside_object)
18
+ @ractor << outside_object
19
+ else
20
+ @ractor.send(outside_object, move: true)
21
+ end
22
+ when :class
23
+ @ractor << :class
24
+
25
+ klass, *args = args
26
+
27
+ ::Ractorize.resolve_all_thunks(args)
28
+ ::Ractorize.resolve_all_thunks(opts)
29
+
30
+ @ractor << [klass, args.freeze, opts.dup.freeze, block].freeze
13
31
  else
14
- @ractor.send(outside_object, move: true)
32
+ # :nocov:
33
+ ::Kernel.raise "Invalid mode #{mode}"
34
+ # :nocov:
15
35
  end
16
36
 
17
- # Wow, this works! Scary?
18
37
  ::Object.instance_method(:freeze).bind(self).call
19
38
  end
20
39
 
@@ -26,7 +45,7 @@ module Ractorize
26
45
  self
27
46
  end
28
47
 
29
- def method_missing(method_name, *args, **opts)
48
+ def method_missing(method_name, *args, **opts, &block)
30
49
  if @ractor.default_port.closed?
31
50
  ::Kernel.raise ::Ractor::ClosedError,
32
51
  "You already closed this Ractorized object! No more methods can be sent to it."
@@ -34,12 +53,50 @@ module Ractorize
34
53
 
35
54
  return_port = ::Ractor::Port.new
36
55
 
37
- @ractor << [method_name, args.dup.freeze, opts.dup.freeze, return_port].freeze
56
+ ::Ractorize.resolve_all_thunks(args)
57
+ ::Ractorize.resolve_all_thunks(opts)
58
+
59
+ @ractor << [method_name, args.dup.freeze, opts.dup.freeze, return_port, !!block].freeze
60
+
61
+ if block
62
+ stop = false
63
+ value = nil
64
+
65
+ until stop
66
+ data = return_port.receive
67
+
68
+ # Seems SimpleCov branch coverage doesn't like that we don't test the non-exhaustive
69
+ # pattern path, but since that's purely defensive I have no interest in testing it.
38
70
 
71
+ # :nocov:
72
+ case data
73
+ # :nocov:
74
+ in :return, value
75
+ stop = true
76
+ in :yield, [yielded_args, yielded_opts, yielded_block], block_result_port
77
+ # TODO: yielded_block likely won't work when actually used
78
+ # so we should probably instead just raise an exception
79
+ # TODO: handle break and also raise in the block
80
+ block_result = block.call(*yielded_args.freeze, **yielded_opts.freeze, &yielded_block)
81
+
82
+ block_result = block_result.__value__ while ::Ractorize::Thunk === block_result
83
+
84
+ block_result_port << [:normal, block_result].freeze
85
+ end
86
+ end
87
+
88
+ value
39
89
  # Let's assume the user would rather block on all predicate methods than
40
90
  # incorrectly get a non-truthy value (thunk is always truthy even if it evaluates as nil/false)
41
- if method_name == :== || method_name == :! || method_name == :!= || method_name.end_with?("?")
42
- return_port.receive
91
+ elsif method_name == :== || method_name == :! || method_name == :!= ||
92
+ method_name == :inspect || method_name == :to_s || method_name.end_with?("?")
93
+ value = return_port.receive
94
+
95
+ # :nocov:
96
+ ::Kernel.raise ::Ractorize::Thunk::EscapingRactorError if ::Ractorize::Thunk === value
97
+ # :nocov:
98
+
99
+ value
43
100
  else
44
101
  Thunk.new(return_port)
45
102
  end
@@ -62,5 +119,14 @@ module Ractorize
62
119
  def !=(other) = method_missing(:==, other)
63
120
  def ! = method_missing(:!)
64
121
  def equal?(other) = method_missing(:equal?, other)
122
+
123
+ def to_s = inspect
124
+
125
+ def inspect
126
+ object_id = ::Object.instance_method(:object_id).bind(self).call
127
+ moved_object_inspect = method_missing(:inspect)
128
+
129
+ "RactorizedObject<#{object_id}>[#{moved_object_inspect}]".freeze
130
+ end
65
131
  end
66
132
  end
@@ -1,8 +1,11 @@
1
1
  module Ractorize
2
2
  class Thunk < BasicObject
3
- attr_accessor :__return_value_port__
3
+ class EscapingRactorError < ::StandardError; end
4
+
5
+ attr_accessor :__return_value_port__, :__ractor__
4
6
 
5
7
  def initialize(return_value_port)
8
+ self.__ractor__ = ::Ractor.current
6
9
  self.__return_value_port__ = return_value_port
7
10
  end
8
11
 
@@ -10,8 +13,8 @@ module Ractorize
10
13
  # is this actually necessary?? Seems so?
11
14
  end
12
15
 
13
- def method_missing(method_name, *)
14
- __value__.send(method_name, *)
16
+ def method_missing(...)
17
+ __value__.__send__(...)
15
18
  end
16
19
 
17
20
  def respond_to_missing?(method_name, include_all = false)
@@ -21,7 +24,23 @@ module Ractorize
21
24
  def __value__
22
25
  return @__value__ if defined?(@__value__)
23
26
 
24
- @__value__ = __return_value_port__.receive
27
+ value = if ::Ractor.current == __ractor__
28
+ __return_value_port__.receive
29
+ else
30
+ # :nocov:
31
+ raise "Somehow this thunk was passed between ractors but wasn't resolved first."
32
+ # :nocov:
33
+ end
34
+
35
+ # :nocov:
36
+ ::Kernel.raise EscapingRactorError if ::Ractorize::Thunk === value
37
+ # :nocov:
38
+
39
+ @__value__ = value
40
+
41
+ ::Object.instance_method(:freeze).bind(self).call
42
+
43
+ value
25
44
  end
26
45
 
27
46
  def !
data/src/ractorize.rb CHANGED
@@ -3,13 +3,64 @@ require_relative "ractorize/ractorized_object"
3
3
  require_relative "ractorize/ractorized_class"
4
4
 
5
5
  module Ractorize
6
+ class << self
7
+ def any_thunks?(structure)
8
+ # rubocop:disable Lint/UnreachableLoop
9
+ each_thunk(structure) { return true }
10
+ # rubocop:enable Lint/UnreachableLoop
11
+ false
12
+ end
13
+
14
+ # Unfortunately, can't read from a port that a different ractor made.
15
+ # Not sure why that is but we need to handle that case.
16
+ def resolve_all_thunks(structure)
17
+ each_thunk(structure, &:__value__)
18
+ end
19
+
20
+ def each_thunk(structure, seen = Set.new, &block)
21
+ return block.call(structure) if Thunk === structure
22
+ return if seen.include?(structure)
23
+
24
+ seen << structure
25
+
26
+ case structure
27
+ when Array
28
+ structure.each { each_thunk(it, seen, &block) }
29
+ when Hash
30
+ each_thunk(structure.keys, seen, &block)
31
+ each_thunk(structure.values, seen, &block)
32
+ when Struct
33
+ each_thunk(structure.values, seen, &block)
34
+ else
35
+ ivarsget = ::Object.instance_method(:instance_variables)
36
+ iget = ::Object.instance_method(:instance_variable_get)
37
+
38
+ ivarsget.bind(structure).call.each do |var|
39
+ each_thunk(iget.bind(structure).call(var), seen, &block)
40
+ end
41
+ end
42
+ end
43
+ end
44
+
6
45
  # Putting this in a constant so we can get test coverage on it since not sure how to get coverage
7
46
  # on something inside a ractor.
8
47
  RACTOR_PROC = proc do
9
- object = receive
48
+ mode = receive
49
+
50
+ object = case mode
51
+ when :class
52
+ klass, args, opts, block = receive
53
+ klass.new(*args, **opts, &block)
54
+ when :object
55
+ receive
56
+ else
57
+ # :nocov:
58
+ ::Kernel.raise "Invalid mode #{mode}"
59
+ # :nocov:
60
+ end
10
61
 
11
62
  loop do
12
- method_name, method_args, opts, return_port = receive
63
+ method_name, method_args, opts, return_port, block_given = receive
13
64
 
14
65
  case method_name
15
66
  when :__close__
@@ -17,20 +68,55 @@ module Ractorize
17
68
  close
18
69
  break
19
70
  else
20
- value = object.__send__(method_name, *method_args, **opts)
71
+ if block_given
72
+ block_result_port = Ractor::Port.new
73
+
74
+ value = object.__send__(method_name, *method_args, **opts) do |*args, **opts, &b|
75
+ ::Ractorize.resolve_all_thunks(args)
76
+ ::Ractorize.resolve_all_thunks(opts)
21
77
 
22
- value = value.__value__ while Thunk === value
78
+ return_port << [:yield, [args.dup.freeze, opts.dup.freeze, b].freeze, block_result_port].freeze
23
79
 
24
- return_port << value
80
+ outcome_type, return_value = block_result_port.receive
81
+
82
+ case outcome_type
83
+ when :normal
84
+ return_value
85
+ when :break
86
+ break return_value
87
+ else
88
+ # :nocov:
89
+ ::Kernel.raise "Not sure how to handle outcome_type #{outcome_type}"
90
+ # :nocov:
91
+ end
92
+ end
93
+
94
+ return_port << [:return, value].freeze
95
+ else
96
+ value = object.__send__(method_name, *method_args, **opts)
97
+
98
+ value = value.__value__ while Thunk === value
99
+
100
+ return_port << value
101
+ end
25
102
  end
26
103
  end
27
104
 
28
105
  object
106
+ rescue => e
107
+ # :nocov:
108
+ ::Kernel.puts
109
+ ::Kernel.puts "an error!!! #{e.class} #{e.message} #{e}"
110
+ ::Kernel.puts e.backtrace
111
+ ::Kernel.puts
112
+
113
+ raise
114
+ # :nocov:
29
115
  end
30
116
 
31
117
  class << self
32
118
  def ractorize_object(object)
33
- RactorizedObject.new(object)
119
+ RactorizedObject.new(:object, object)
34
120
  end
35
121
 
36
122
  def ractorize_class(klass)
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ractorize
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.3
4
+ version: 0.0.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - Miles Georgi