summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKoichi Sasada <ko1@atdot.net>2025-07-17 15:38:54 +0900
committerKoichi Sasada <ko1@atdot.net>2025-09-24 03:59:03 +0900
commit55b1ba3bf276ba82173bd961fb8e0f08bf4182a6 (patch)
tree9da723c4412af61ef221993fa021da97df9be1b0
parentc05ea920cef5991ca6163d12a436a61219a234a6 (diff)
Ractor.shareable_proc
call-seq: Ractor.sharable_proc(self: nil){} -> sharable proc It returns shareable Proc object. The Proc object is shareable and the self in a block will be replaced with the value passed via `self:` keyword. In a shareable Proc, the outer variables should * (1) refer shareable objects * (2) be not be overwritten ```ruby a = 42 Ractor.shareable_proc{ p a } #=> OK b = 43 Ractor.shareable_proc{ p b; b = 44 } #=> Ractor::IsolationError because 'b' is reassigned in the block. c = 44 Ractor.shareable_proc{ p c } #=> Ractor::IsolationError because 'c' will be reassigned outside of the block. c = 45 d = 45 d = 46 if cond Ractor.shareable_proc{ p d } #=> Ractor::IsolationError because 'd' was reassigned outside of the block. ``` The last `d`'s case can be relaxed in a future version. The above check will be done in a static analysis at compile time, so the reflection feature such as `Binding#local_varaible_set` can not be detected. ```ruby e = 42 shpr = Ractor.shareable_proc{ p e } #=> OK binding.local_variable_set(:e, 43) shpr.call #=> 42 (returns captured timing value) ``` Ractor.sharaeble_lambda is also introduced. [Feature #21550] [Feature #21557]
-rw-r--r--NEWS.md6
-rw-r--r--bootstraptest/test_ractor.rb163
-rw-r--r--compile.c91
-rw-r--r--iseq.c5
-rw-r--r--prism_compile.c3
-rw-r--r--ractor.c16
-rw-r--r--ractor.rb43
-rw-r--r--test/ruby/test_iseq.rb23
-rw-r--r--test/ruby/test_ractor.rb30
-rw-r--r--thread.c2
-rw-r--r--vm.c35
-rw-r--r--vm_core.h10
12 files changed, 254 insertions, 173 deletions
diff --git a/NEWS.md b/NEWS.md
index 5a9277b399..7d66e1aaa5 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -117,6 +117,10 @@ Note: We're only listing outstanding class updates.
* `Ractor#close_incoming` and `Ractor#close_outgoing` were removed.
+ * `Ractor.sharealbe_proc` and `Ractor.shareable_lambda` is introduced
+ to make shareable Proc or lambda.
+ [[Feature #21550]], [[Feature #21557]]
+
* `Set`
* `Set` is now a core class, instead of an autoloaded stdlib class.
@@ -316,3 +320,5 @@ A lot of work has gone into making Ractors more stable, performant, and usable.
[Feature #21347]: https://bugs.ruby-lang.org/issues/21347
[Feature #21360]: https://bugs.ruby-lang.org/issues/21360
[Feature #21527]: https://bugs.ruby-lang.org/issues/21527
+[Feature #21550]: https://bugs.ruby-lang.org/issues/21550
+[Feature #21557]: https://bugs.ruby-lang.org/issues/21557
diff --git a/bootstraptest/test_ractor.rb b/bootstraptest/test_ractor.rb
index 74fee197f8..634a3e3e61 100644
--- a/bootstraptest/test_ractor.rb
+++ b/bootstraptest/test_ractor.rb
@@ -145,28 +145,47 @@ assert_equal '[:ok, :ok, :ok]', %q{
}.map(&:value)
}
+assert_equal "42", %q{
+ a = 42
+ Ractor.shareable_lambda{ a }.call
+}
+
# Ractor.make_shareable issue for locals in proc [Bug #18023]
assert_equal '[:a, :b, :c, :d, :e]', %q{
v1, v2, v3, v4, v5 = :a, :b, :c, :d, :e
- closure = Ractor.current.instance_eval{ Proc.new { [v1, v2, v3, v4, v5] } }
-
- Ractor.make_shareable(closure).call
+ closure = Proc.new { [v1, v2, v3, v4, v5] }
+ Ractor.shareable_proc(&closure).call
}
-# Ractor.make_shareable issue for locals in proc [Bug #18023]
-assert_equal '[:a, :b, :c, :d, :e, :f, :g]', %q{
- a = :a
- closure = Ractor.current.instance_eval do
- -> {
- b, c, d = :b, :c, :d
- -> {
- e, f, g = :e, :f, :g
- -> { [a, b, c, d, e, f, g] }
- }.call
- }.call
+# Ractor::IsolationError cases
+assert_equal '3', %q{
+ ok = 0
+
+ begin
+ a = 1
+ Ractor.shareable_proc{a}
+ a = 2
+ rescue Ractor::IsolationError => e
+ ok += 1
end
- Ractor.make_shareable(closure).call
+ begin
+ cond = false
+ a = 1
+ a = 2 if cond
+ Ractor.shareable_proc{a}
+ rescue Ractor::IsolationError => e
+ ok += 1
+ end
+
+ begin
+ 1.times{|i|
+ i = 2
+ Ractor.shareable_proc{i}
+ }
+ rescue Ractor::IsolationError => e
+ ok += 1
+ end
}
###
@@ -967,7 +986,7 @@ assert_equal 'can not access non-shareable objects in constant C::CONST by non-m
end
RUBY
-# Constant cache should care about non-sharable constants
+# Constant cache should care about non-shareable constants
assert_equal "can not access non-shareable objects in constant Object::STR by non-main Ractor.", <<~'RUBY', frozen_string_literal: false
STR = "hello"
def str; STR; end
@@ -1137,41 +1156,17 @@ assert_equal 'true', %q{
[a.frozen?, a[0].frozen?] == [true, false]
}
-# Ractor.make_shareable(a_proc) makes a proc shareable.
+# Ractor.make_shareable(a_proc) is not supported now.
assert_equal 'true', %q{
- a = [1, [2, 3], {a: "4"}]
-
- pr = Ractor.current.instance_eval do
- Proc.new do
- a
- end
- end
+ pr = Proc.new{}
- Ractor.make_shareable(a) # referred value should be shareable
- Ractor.make_shareable(pr)
- Ractor.shareable?(pr)
-}
-
-# Ractor.make_shareable(a_proc) makes inner structure shareable and freezes it
-assert_equal 'true,true,true,true', %q{
- class Proc
- attr_reader :obj
- def initialize
- @obj = Object.new
- end
- end
-
- pr = Ractor.current.instance_eval do
- Proc.new {}
+ begin
+ Ractor.make_shareable(pr)
+ rescue Ractor::Error
+ true
+ else
+ false
end
-
- results = []
- Ractor.make_shareable(pr)
- results << Ractor.shareable?(pr)
- results << pr.frozen?
- results << Ractor.shareable?(pr.obj)
- results << pr.obj.frozen?
- results.map(&:to_s).join(',')
}
# Ractor.shareable?(recursive_objects)
@@ -1202,50 +1197,16 @@ assert_equal '[C, M]', %q{
Ractor.make_shareable(ary = [C, M])
}
-# Ractor.make_shareable with curried proc checks isolation of original proc
-assert_equal 'isolation error', %q{
- a = Object.new
- orig = proc { a }
- curried = orig.curry
-
- begin
- Ractor.make_shareable(curried)
- rescue Ractor::IsolationError
- 'isolation error'
- else
- 'no error'
- end
-}
-
# define_method() can invoke different Ractor's proc if the proc is shareable.
assert_equal '1', %q{
class C
a = 1
- define_method "foo", Ractor.make_shareable(Proc.new{ a })
- a = 2
+ define_method "foo", Ractor.shareable_proc{ a }
end
Ractor.new{ C.new.foo }.value
}
-# Ractor.make_shareable(a_proc) makes a proc shareable.
-assert_equal 'can not make a Proc shareable because it accesses outer variables (a).', %q{
- a = b = nil
- pr = Ractor.current.instance_eval do
- Proc.new do
- c = b # assign to a is okay because c is block local variable
- # reading b is okay
- a = b # assign to a is not allowed #=> Ractor::Error
- end
- end
-
- begin
- Ractor.make_shareable(pr)
- rescue => e
- e.message
- end
-}
-
# Ractor.make_shareable(obj, copy: true) makes copied shareable object.
assert_equal '[false, false, true, true]', %q{
r = []
@@ -1471,42 +1432,6 @@ assert_equal "ok", %q{
"ok"
} if !yjit_enabled? && ENV['GITHUB_WORKFLOW'] != 'ModGC' # flaky
-assert_equal "ok", %q{
- def foo(*); ->{ super }; end
- begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
- end
-}
-
-assert_equal "ok", %q{
- def foo(**); ->{ super }; end
- begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
- end
-}
-
-assert_equal "ok", %q{
- def foo(...); ->{ super }; end
- begin
- Ractor.make_shareable(foo)
- rescue Ractor::IsolationError
- "ok"
- end
-}
-
-assert_equal "ok", %q{
- def foo((x), (y)); ->{ super }; end
- begin
- Ractor.make_shareable(foo([], []))
- rescue Ractor::IsolationError
- "ok"
- end
-}
-
# check method cache invalidation
assert_equal "ok", %q{
module M
diff --git a/compile.c b/compile.c
index 0ef0d1b31a..fb269721f3 100644
--- a/compile.c
+++ b/compile.c
@@ -494,6 +494,7 @@ static int iseq_set_arguments(rb_iseq_t *iseq, LINK_ANCHOR *const anchor, const
static int iseq_set_sequence(rb_iseq_t *iseq, LINK_ANCHOR *const anchor);
static int iseq_set_exception_table(rb_iseq_t *iseq);
static int iseq_set_optargs_table(rb_iseq_t *iseq);
+static int iseq_set_parameters_lvar_state(const rb_iseq_t *iseq);
static int compile_defined_expr(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const node, VALUE needstr, bool ignore);
static int compile_hash(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *node, int method_call_keywords, int popped);
@@ -876,6 +877,7 @@ rb_iseq_compile_node(rb_iseq_t *iseq, const NODE *node)
/* iseq type of top, method, class, block */
iseq_set_local_table(iseq, RNODE_SCOPE(node)->nd_tbl, (NODE *)RNODE_SCOPE(node)->nd_args);
iseq_set_arguments(iseq, ret, (NODE *)RNODE_SCOPE(node)->nd_args);
+ iseq_set_parameters_lvar_state(iseq);
switch (ISEQ_BODY(iseq)->type) {
case ISEQ_TYPE_BLOCK:
@@ -1715,6 +1717,7 @@ iseq_set_exception_local_table(rb_iseq_t *iseq)
{
ISEQ_BODY(iseq)->local_table_size = numberof(rb_iseq_shared_exc_local_tbl);
ISEQ_BODY(iseq)->local_table = rb_iseq_shared_exc_local_tbl;
+ ISEQ_BODY(iseq)->lvar_states = NULL; // $! is read-only, so don't need lvar_states
return COMPILE_OK;
}
@@ -1862,6 +1865,46 @@ iseq_lvar_id(const rb_iseq_t *iseq, int idx, int level)
}
static void
+update_lvar_state(const rb_iseq_t *iseq, int level, int idx)
+{
+ for (int i=0; i<level; i++) {
+ iseq = ISEQ_BODY(iseq)->parent_iseq;
+ }
+
+ enum lvar_state *states = ISEQ_BODY(iseq)->lvar_states;
+ int table_idx = ISEQ_BODY(iseq)->local_table_size - idx;
+ switch (states[table_idx]) {
+ case lvar_uninitialized:
+ states[table_idx] = lvar_initialized;
+ break;
+ case lvar_initialized:
+ states[table_idx] = lvar_reassigned;
+ break;
+ case lvar_reassigned:
+ /* nothing */
+ break;
+ default:
+ rb_bug("unreachable");
+ }
+}
+
+static int
+iseq_set_parameters_lvar_state(const rb_iseq_t *iseq)
+{
+ for (unsigned int i=0; i<ISEQ_BODY(iseq)->param.size; i++) {
+ ISEQ_BODY(iseq)->lvar_states[i] = lvar_initialized;
+ }
+
+ int lead_num = ISEQ_BODY(iseq)->param.lead_num;
+ int opt_num = ISEQ_BODY(iseq)->param.opt_num;
+ for (int i=0; i<opt_num; i++) {
+ ISEQ_BODY(iseq)->lvar_states[lead_num + i] = lvar_uninitialized;
+ }
+
+ return COMPILE_OK;
+}
+
+static void
iseq_add_getlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, const NODE *const line_node, int idx, int level)
{
if (iseq_local_block_param_p(iseq, idx, level)) {
@@ -1882,6 +1925,7 @@ iseq_add_setlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, const NODE *const lin
else {
ADD_INSN2(seq, line_node, setlocal, INT2FIX((idx) + VM_ENV_DATA_SIZE - 1), INT2FIX(level));
}
+ update_lvar_state(iseq, level, idx);
if (level > 0) access_outer_variables(iseq, level, iseq_lvar_id(iseq, idx, level), Qtrue);
}
@@ -2212,6 +2256,14 @@ iseq_set_local_table(rb_iseq_t *iseq, const rb_ast_id_table_t *tbl, const NODE *
ID *ids = ALLOC_N(ID, size);
MEMCPY(ids, tbl->ids + offset, ID, size);
ISEQ_BODY(iseq)->local_table = ids;
+
+ enum lvar_state *states = ALLOC_N(enum lvar_state, size);
+ // fprintf(stderr, "iseq:%p states:%p size:%d\n", iseq, states, (int)size);
+ for (unsigned int i=0; i<size; i++) {
+ states[i] = lvar_uninitialized;
+ // fprintf(stderr, "id:%s\n", rb_id2name(ISEQ_BODY(iseq)->local_table[i]));
+ }
+ ISEQ_BODY(iseq)->lvar_states = states;
}
ISEQ_BODY(iseq)->local_table_size = size;
@@ -12379,7 +12431,7 @@ typedef uint32_t ibf_offset_t;
#define IBF_MAJOR_VERSION ISEQ_MAJOR_VERSION
#ifdef RUBY_DEVEL
-#define IBF_DEVEL_VERSION 4
+#define IBF_DEVEL_VERSION 5
#define IBF_MINOR_VERSION (ISEQ_MINOR_VERSION * 10000 + IBF_DEVEL_VERSION)
#else
#define IBF_MINOR_VERSION ISEQ_MINOR_VERSION
@@ -13190,7 +13242,7 @@ ibf_dump_local_table(struct ibf_dump *dump, const rb_iseq_t *iseq)
return ibf_dump_write(dump, table, sizeof(ID) * size);
}
-static ID *
+static const ID *
ibf_load_local_table(const struct ibf_load *load, ibf_offset_t local_table_offset, int size)
{
if (size > 0) {
@@ -13200,7 +13252,14 @@ ibf_load_local_table(const struct ibf_load *load, ibf_offset_t local_table_offse
for (i=0; i<size; i++) {
table[i] = ibf_load_id(load, table[i]);
}
- return table;
+
+ if (size == 1 && table[0] == idERROR_INFO) {
+ xfree(table);
+ return rb_iseq_shared_exc_local_tbl;
+ }
+ else {
+ return table;
+ }
}
else {
return NULL;
@@ -13208,6 +13267,28 @@ ibf_load_local_table(const struct ibf_load *load, ibf_offset_t local_table_offse
}
static ibf_offset_t
+ibf_dump_lvar_states(struct ibf_dump *dump, const rb_iseq_t *iseq)
+{
+ const struct rb_iseq_constant_body *const body = ISEQ_BODY(iseq);
+ const int size = body->local_table_size;
+ IBF_W_ALIGN(enum lvar_state);
+ return ibf_dump_write(dump, body->lvar_states, body->lvar_states ? size : 0);
+}
+
+static enum lvar_state *
+ibf_load_lvar_states(const struct ibf_load *load, ibf_offset_t lvar_states_offset, int size, const ID *local_table)
+{
+ if (local_table == rb_iseq_shared_exc_local_tbl ||
+ size <= 0) {
+ return NULL;
+ }
+ else {
+ enum lvar_state *states = IBF_R(lvar_states_offset, enum lvar_state, size);
+ return states;
+ }
+}
+
+static ibf_offset_t
ibf_dump_catch_table(struct ibf_dump *dump, const rb_iseq_t *iseq)
{
const struct iseq_catch_table *table = ISEQ_BODY(iseq)->catch_table;
@@ -13480,6 +13561,7 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq)
ruby_xfree(positions);
const ibf_offset_t local_table_offset = ibf_dump_local_table(dump, iseq);
+ const ibf_offset_t lvar_states_offset = ibf_dump_lvar_states(dump, iseq);
const unsigned int catch_table_size = body->catch_table ? body->catch_table->size : 0;
const ibf_offset_t catch_table_offset = ibf_dump_catch_table(dump, iseq);
const int parent_iseq_index = ibf_dump_iseq(dump, ISEQ_BODY(iseq)->parent_iseq);
@@ -13547,6 +13629,7 @@ ibf_dump_iseq_each(struct ibf_dump *dump, const rb_iseq_t *iseq)
ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(insns_info_positions_offset));
ibf_dump_write_small_value(dump, body->insns_info.size);
ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(local_table_offset));
+ ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(lvar_states_offset));
ibf_dump_write_small_value(dump, catch_table_size);
ibf_dump_write_small_value(dump, IBF_BODY_OFFSET(catch_table_offset));
ibf_dump_write_small_value(dump, parent_iseq_index);
@@ -13658,6 +13741,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset)
const ibf_offset_t insns_info_positions_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos));
const unsigned int insns_info_size = (unsigned int)ibf_load_small_value(load, &reading_pos);
const ibf_offset_t local_table_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos));
+ const ibf_offset_t lvar_states_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos));
const unsigned int catch_table_size = (unsigned int)ibf_load_small_value(load, &reading_pos);
const ibf_offset_t catch_table_offset = (ibf_offset_t)IBF_BODY_OFFSET(ibf_load_small_value(load, &reading_pos));
const int parent_iseq_index = (int)ibf_load_small_value(load, &reading_pos);
@@ -13774,6 +13858,7 @@ ibf_load_iseq_each(struct ibf_load *load, rb_iseq_t *iseq, ibf_offset_t offset)
load_body->insns_info.body = ibf_load_insns_info_body(load, insns_info_body_offset, insns_info_size);
load_body->insns_info.positions = ibf_load_insns_info_positions(load, insns_info_positions_offset, insns_info_size);
load_body->local_table = ibf_load_local_table(load, local_table_offset, local_table_size);
+ load_body->lvar_states = ibf_load_lvar_states(load, lvar_states_offset, local_table_size, load_body->local_table);
ibf_load_catch_table(load, catch_table_offset, catch_table_size, iseq);
const rb_iseq_t *parent_iseq = ibf_load_iseq(load, (const rb_iseq_t *)(VALUE)parent_iseq_index);
diff --git a/iseq.c b/iseq.c
index 33b37bade5..d8891353f1 100644
--- a/iseq.c
+++ b/iseq.c
@@ -199,8 +199,11 @@ rb_iseq_free(const rb_iseq_t *iseq)
}
ruby_xfree((void *)body->param.keyword);
}
- if (LIKELY(body->local_table != rb_iseq_shared_exc_local_tbl))
+ if (LIKELY(body->local_table != rb_iseq_shared_exc_local_tbl)) {
ruby_xfree((void *)body->local_table);
+ }
+ ruby_xfree((void *)body->lvar_states);
+
compile_data_free(ISEQ_COMPILE_DATA(iseq));
if (body->outer_variables) rb_id_table_free(body->outer_variables);
ruby_xfree(body);
diff --git a/prism_compile.c b/prism_compile.c
index 578e6f240f..cd8287c762 100644
--- a/prism_compile.c
+++ b/prism_compile.c
@@ -102,6 +102,7 @@ pm_iseq_add_setlocal(rb_iseq_t *iseq, LINK_ANCHOR *const seq, int line, int node
else {
ADD_ELEM(seq, (LINK_ELEMENT *) new_insn_body(iseq, line, node_id, BIN(setlocal), 2, INT2FIX((idx) + VM_ENV_DATA_SIZE - 1), INT2FIX(level)));
}
+ update_lvar_state(iseq, level, idx);
if (level > 0) access_outer_variables(iseq, level, iseq_lvar_id(iseq, idx, level), Qtrue);
}
@@ -6796,6 +6797,8 @@ pm_compile_scope_node(rb_iseq_t *iseq, pm_scope_node_t *scope_node, const pm_nod
// FIXME: args?
iseq_set_local_table(iseq, local_table_for_iseq, 0);
+ iseq_set_parameters_lvar_state(iseq);
+
scope_node->local_table_for_iseq_size = local_table_for_iseq->size;
if (keyword != NULL) {
diff --git a/ractor.c b/ractor.c
index b514210638..c439f25e85 100644
--- a/ractor.c
+++ b/ractor.c
@@ -1374,7 +1374,7 @@ make_shareable_check_shareable(VALUE obj)
}
else if (!allow_frozen_shareable_p(obj)) {
if (rb_obj_is_proc(obj)) {
- rb_proc_ractor_make_shareable(obj);
+ rb_proc_ractor_make_shareable(obj, Qundef);
return traverse_cont;
}
else {
@@ -2273,6 +2273,20 @@ ractor_local_value_store_if_absent(rb_execution_context_t *ec, VALUE self, VALUE
return rb_mutex_synchronize(cr->local_storage_store_lock, ractor_local_value_store_i, (VALUE)&data);
}
+// sharable_proc
+
+static VALUE
+ractor_shareable_proc(rb_execution_context_t *ec, VALUE replace_self, bool is_lambda)
+{
+ if (!rb_ractor_shareable_p(replace_self)) {
+ rb_raise(rb_eRactorIsolationError, "self should be shareable: %" PRIsVALUE, replace_self);
+ }
+ else {
+ VALUE proc = is_lambda ? rb_block_lambda() : rb_block_proc();
+ return rb_proc_ractor_make_shareable(proc, replace_self);
+ }
+}
+
// Ractor#require
struct cross_ractor_require {
diff --git a/ractor.rb b/ractor.rb
index 87d1852876..7c00e148a1 100644
--- a/ractor.rb
+++ b/ractor.rb
@@ -1,5 +1,3 @@
-# \Ractor is an Actor-model abstraction for Ruby that provides thread-safe parallel execution.
-#
# Ractor.new makes a new \Ractor, which can run in parallel.
#
# # The simplest ractor
@@ -606,6 +604,47 @@ class Ractor
__builtin_ractor_unmonitor(port)
end
+ #
+ # call-seq:
+ # Ractor.sharable_proc(self: nil){} -> sharable proc
+ #
+ # It returns shareable Proc object. The Proc object is
+ # shareable and the self in a block will be replaced with
+ # the value passed via `self:` keyword.
+ #
+ # In a shareable Proc, you can not access to the outer variables.
+ #
+ # a = 42
+ # Ractor.shareable_proc{ p a }
+ # #=> can not isolate a Proc because it accesses outer variables (a). (ArgumentError)
+ #
+ # The `self` should be a sharable object
+ #
+ # Ractor.shareable_proc(self: self){}
+ # #=> self should be shareable: main (Ractor::IsolationError)
+ #
+ def self.shareable_proc self: nil
+ Primitive.attr! :use_block
+
+ __builtin_cexpr!(%Q{
+ ractor_shareable_proc(ec, *LOCAL_PTR(self), false)
+ })
+ end
+
+ #
+ # call-seq:
+ # Ractor.sharable_proc{} -> sharable proc
+ #
+ # Same as Ractor.sharable_proc, but returns lambda proc.
+ #
+ def self.shareable_lambda self: nil
+ Primitive.attr! :use_block
+
+ __builtin_cexpr!(%Q{
+ ractor_shareable_proc(ec, *LOCAL_PTR(self), true)
+ })
+ end
+
# \Port objects transmit messages between Ractors.
class Port
#
diff --git a/test/ruby/test_iseq.rb b/test/ruby/test_iseq.rb
index 45223c89da..fa716787fe 100644
--- a/test/ruby/test_iseq.rb
+++ b/test/ruby/test_iseq.rb
@@ -139,8 +139,7 @@ class TestISeq < Test::Unit::TestCase
def test_lambda_with_ractor_roundtrip
iseq = compile(<<~EOF, __LINE__+1)
x = 42
- y = nil.instance_eval{ lambda { x } }
- Ractor.make_shareable(y)
+ y = Ractor.shareable_lambda{x}
y.call
EOF
assert_equal(42, ISeq.load_from_binary(iseq_to_binary(iseq)).eval)
@@ -158,22 +157,18 @@ class TestISeq < Test::Unit::TestCase
def test_ractor_unshareable_outer_variable
name = "\u{2603 26a1}"
- y = nil.instance_eval do
- eval("proc {#{name} = nil; proc {|x| #{name} = x}}").call
- end
assert_raise_with_message(ArgumentError, /\(#{name}\)/) do
- Ractor.make_shareable(y)
- end
- y = nil.instance_eval do
- eval("proc {#{name} = []; proc {|x| #{name}}}").call
+ eval("#{name} = nil; Ractor.shareable_proc{#{name} = nil}")
end
- assert_raise_with_message(Ractor::IsolationError, /'#{name}'/) do
- Ractor.make_shareable(y)
+
+ assert_raise_with_message(Ractor::IsolationError, /\'#{name}\'/) do
+ eval("#{name} = []; Ractor.shareable_proc{#{name}}")
end
+
obj = Object.new
- def obj.foo(*) nil.instance_eval{ ->{super} } end
- assert_raise_with_message(Ractor::IsolationError, /refer unshareable object \[\] from variable '\*'/) do
- Ractor.make_shareable(obj.foo(*[]))
+ def obj.foo(*) Ractor.shareable_proc{super} end
+ assert_raise_with_message(Ractor::IsolationError, /cannot make a shareable Proc because it can refer unshareable object \[\]/) do
+ obj.foo(*[])
end
end
diff --git a/test/ruby/test_ractor.rb b/test/ruby/test_ractor.rb
index 70a2ca4bfb..c4154cd263 100644
--- a/test/ruby/test_ractor.rb
+++ b/test/ruby/test_ractor.rb
@@ -3,38 +3,19 @@ require 'test/unit'
class TestRactor < Test::Unit::TestCase
def test_shareability_of_iseq_proc
- y = nil.instance_eval do
+ assert_raise Ractor::IsolationError do
foo = []
- proc { foo }
+ Ractor.shareable_proc{ foo }
end
- assert_unshareable(y, /unshareable object \[\] from variable 'foo'/)
-
- y = [].instance_eval { proc { self } }
- assert_unshareable(y, /Proc's self is not shareable/)
-
- y = [].freeze.instance_eval { proc { self } }
- assert_make_shareable(y)
- end
-
- def test_shareability_of_curried_proc
- x = nil.instance_eval do
- foo = []
- proc { foo }.curry
- end
- assert_unshareable(x, /unshareable object \[\] from variable 'foo'/)
-
- x = nil.instance_eval do
- foo = 123
- proc { foo }.curry
- end
- assert_make_shareable(x)
end
def test_shareability_of_method_proc
+ # TODO: fix with Ractor.shareable_proc/lambda
+=begin
str = +""
x = str.instance_exec { proc { to_s } }
- assert_unshareable(x, /Proc's self is not shareable/)
+ assert_unshareable(x, /Proc\'s self is not shareable/)
x = str.instance_exec { method(:to_s) }
assert_unshareable(x, "can not make shareable object for #<Method: String#to_s()>", exception: Ractor::Error)
@@ -58,6 +39,7 @@ class TestRactor < Test::Unit::TestCase
x = str.instance_exec { method(:itself).to_proc }
assert_unshareable(x, "can not make shareable object for #<Method: String(Kernel)#itself()>", exception: Ractor::Error)
+=end
end
def test_shareability_error_uses_inspect
diff --git a/thread.c b/thread.c
index 97e9561f3a..bab615b9ed 100644
--- a/thread.c
+++ b/thread.c
@@ -841,7 +841,7 @@ thread_create_core(VALUE thval, struct thread_create_params *params)
th->invoke_type = thread_invoke_type_ractor_proc;
th->ractor = params->g;
th->ractor->threads.main = th;
- th->invoke_arg.proc.proc = rb_proc_isolate_bang(params->proc);
+ th->invoke_arg.proc.proc = rb_proc_isolate_bang(params->proc, Qnil);
th->invoke_arg.proc.args = INT2FIX(RARRAY_LENINT(params->args));
th->invoke_arg.proc.kw_splat = rb_keyword_given_p();
rb_ractor_send_parameters(ec, params->g, params->args);
diff --git a/vm.c b/vm.c
index da92c5bd00..154a0ba9d1 100644
--- a/vm.c
+++ b/vm.c
@@ -1325,12 +1325,22 @@ env_copy(const VALUE *src_ep, VALUE read_only_variables)
for (int i=RARRAY_LENINT(read_only_variables)-1; i>=0; i--) {
ID id = NUM2ID(RARRAY_AREF(read_only_variables, i));
- for (unsigned int j=0; j<ISEQ_BODY(src_env->iseq)->local_table_size; j++) {
- if (id == ISEQ_BODY(src_env->iseq)->local_table[j]) {
+ const struct rb_iseq_constant_body *body = ISEQ_BODY(src_env->iseq);
+ for (unsigned int j=0; j<body->local_table_size; j++) {
+ if (id == body->local_table[j]) {
+ // check reassignment
+ if (body->lvar_states[j] == lvar_reassigned) {
+ VALUE name = rb_id2str(id);
+ VALUE msg = rb_sprintf("cannot make a shareable Proc because "
+ "the outer variable '%" PRIsVALUE "' may be reassigned.", name);
+ rb_exc_raise(rb_exc_new_str(rb_eRactorIsolationError, msg));
+ }
+
+ // check shareable
VALUE v = src_env->env[j];
if (!rb_ractor_shareable_p(v)) {
VALUE name = rb_id2str(id);
- VALUE msg = rb_sprintf("can not make shareable Proc because it can refer"
+ VALUE msg = rb_sprintf("cannot make a shareable Proc because it can refer"
" unshareable object %+" PRIsVALUE " from ", v);
if (name)
rb_str_catf(msg, "variable '%" PRIsVALUE "'", name);
@@ -1403,12 +1413,18 @@ proc_shared_outer_variables(struct rb_id_table *outer_variables, bool isolate, c
}
VALUE
-rb_proc_isolate_bang(VALUE self)
+rb_proc_isolate_bang(VALUE self, VALUE replace_self)
{
const rb_iseq_t *iseq = vm_proc_iseq(self);
if (iseq) {
rb_proc_t *proc = (rb_proc_t *)RTYPEDDATA_DATA(self);
+
+ if (!UNDEF_P(replace_self)) {
+ VM_ASSERT(rb_ractor_shareable_p(replace_self));
+ RB_OBJ_WRITE(self, &proc->block.as.captured.self, replace_self);
+ }
+
if (proc->block.type != block_type_iseq) rb_raise(rb_eRuntimeError, "not supported yet");
if (ISEQ_BODY(iseq)->outer_variables) {
@@ -1427,17 +1443,22 @@ VALUE
rb_proc_isolate(VALUE self)
{
VALUE dst = rb_proc_dup(self);
- rb_proc_isolate_bang(dst);
+ rb_proc_isolate_bang(dst, Qundef);
return dst;
}
VALUE
-rb_proc_ractor_make_shareable(VALUE self)
+rb_proc_ractor_make_shareable(VALUE self, VALUE replace_self)
{
const rb_iseq_t *iseq = vm_proc_iseq(self);
if (iseq) {
rb_proc_t *proc = (rb_proc_t *)RTYPEDDATA_DATA(self);
+
+ if (!UNDEF_P(replace_self)) {
+ RB_OBJ_WRITE(self, &proc->block.as.captured.self, replace_self);
+ }
+
if (proc->block.type != block_type_iseq) rb_raise(rb_eRuntimeError, "not supported yet");
if (!rb_ractor_shareable_p(vm_block_self(&proc->block))) {
@@ -1458,6 +1479,8 @@ rb_proc_ractor_make_shareable(VALUE self)
}
rb_obj_freeze(self);
+ FL_SET_RAW(self, RUBY_FL_SHAREABLE);
+
return self;
}
diff --git a/vm_core.h b/vm_core.h
index 9156b72868..2e77e1073e 100644
--- a/vm_core.h
+++ b/vm_core.h
@@ -496,6 +496,12 @@ struct rb_iseq_constant_body {
const ID *local_table; /* must free */
+ enum lvar_state {
+ lvar_uninitialized,
+ lvar_initialized,
+ lvar_reassigned,
+ } *lvar_states;
+
/* catch table */
struct iseq_catch_table *catch_table;
@@ -1285,8 +1291,8 @@ typedef struct {
RUBY_SYMBOL_EXPORT_BEGIN
VALUE rb_proc_isolate(VALUE self);
-VALUE rb_proc_isolate_bang(VALUE self);
-VALUE rb_proc_ractor_make_shareable(VALUE self);
+VALUE rb_proc_isolate_bang(VALUE self, VALUE replace_self);
+VALUE rb_proc_ractor_make_shareable(VALUE proc, VALUE replace_self);
RUBY_SYMBOL_EXPORT_END
typedef struct {