Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/zjit-macos.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ jobs:
rustup install ${{ matrix.rust_version }} --profile minimal
rustup default ${{ matrix.rust_version }}
- uses: taiki-e/install-action@bfadeaba214680fb4ab63e710bcb2a6a17019fdc # v2.70.4
- uses: taiki-e/install-action@0cccd59f03b32c54f0db097c518c320bfc8c73b3 # v2.71.1
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/zjit-ubuntu.yml
Original file line number Diff line number Diff line change
Expand Up @@ -119,7 +119,7 @@ jobs:
ruby-version: '3.1'
bundler: none

- uses: taiki-e/install-action@bfadeaba214680fb4ab63e710bcb2a6a17019fdc # v2.70.4
- uses: taiki-e/install-action@0cccd59f03b32c54f0db097c518c320bfc8c73b3 # v2.71.1
with:
tool: nextest@0.9
if: ${{ matrix.test_task == 'zjit-check' }}
Expand Down
38 changes: 35 additions & 3 deletions compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -6090,6 +6090,23 @@ compile_const_prefix(rb_iseq_t *iseq, const NODE *const node,
return COMPILE_OK;
}

static int
cpath_const_p(const NODE *node)
{
switch (nd_type(node)) {
case NODE_CONST:
case NODE_COLON3:
return TRUE;
case NODE_COLON2:
if (RNODE_COLON2(node)->nd_head) {
return cpath_const_p(RNODE_COLON2(node)->nd_head);
}
return TRUE;
default:
return FALSE;
}
}

static int
compile_cpath(LINK_ANCHOR *const ret, rb_iseq_t *iseq, const NODE *cpath)
{
Expand All @@ -6099,9 +6116,13 @@ compile_cpath(LINK_ANCHOR *const ret, rb_iseq_t *iseq, const NODE *cpath)
return VM_DEFINECLASS_FLAG_SCOPED;
}
else if (nd_type_p(cpath, NODE_COLON2) && RNODE_COLON2(cpath)->nd_head) {
/* Bar::Foo */
/* Bar::Foo or expr::Foo */
NO_CHECK(COMPILE(ret, "nd_else->nd_head", RNODE_COLON2(cpath)->nd_head));
return VM_DEFINECLASS_FLAG_SCOPED;
int flags = VM_DEFINECLASS_FLAG_SCOPED;
if (!cpath_const_p(RNODE_COLON2(cpath)->nd_head)) {
flags |= VM_DEFINECLASS_FLAG_DYNAMIC_CREF;
}
return flags;
}
else {
/* class at cbase Foo */
Expand Down Expand Up @@ -11477,9 +11498,20 @@ iseq_compile_each0(rb_iseq_t *iseq, LINK_ANCHOR *const ret, const NODE *const no
CHECK(COMPILE(ret, "sclass#recv", RNODE_SCLASS(node)->nd_recv));
ADD_INSN (ret, node, putnil);
CONST_ID(singletonclass, "singletonclass");

/* `class << self` in a class body and `class << Foo` (constant
receiver) are stable. All other forms are potentially dynamic. */
int sclass_flags = VM_DEFINECLASS_TYPE_SINGLETON_CLASS;
const NODE *recv = RNODE_SCLASS(node)->nd_recv;
if (!(nd_type_p(recv, NODE_SELF) &&
ISEQ_BODY(iseq)->type == ISEQ_TYPE_CLASS) &&
!cpath_const_p(recv)) {
sclass_flags |= VM_DEFINECLASS_FLAG_DYNAMIC_CREF;
}

ADD_INSN3(ret, node, defineclass,
ID2SYM(singletonclass), singleton_class,
INT2FIX(VM_DEFINECLASS_TYPE_SINGLETON_CLASS));
INT2FIX(sclass_flags));
RB_OBJ_WRITTEN(iseq, Qundef, (VALUE)singleton_class);

if (popped) {
Expand Down
19 changes: 16 additions & 3 deletions eval_intern.h
Original file line number Diff line number Diff line change
Expand Up @@ -172,9 +172,10 @@ rb_ec_tag_jump(const rb_execution_context_t *ec, enum ruby_tag_type st)

/* CREF operators */

#define CREF_FL_PUSHED_BY_EVAL IMEMO_FL_USER1
#define CREF_FL_OMOD_SHARED IMEMO_FL_USER2
#define CREF_FL_SINGLETON IMEMO_FL_USER3
#define CREF_FL_PUSHED_BY_EVAL IMEMO_FL_USER1
#define CREF_FL_OMOD_SHARED IMEMO_FL_USER2
#define CREF_FL_SINGLETON IMEMO_FL_USER3
#define CREF_FL_DYNAMIC_CREF IMEMO_FL_USER4

static inline int CREF_SINGLETON(const rb_cref_t *cref);

Expand Down Expand Up @@ -260,6 +261,18 @@ CREF_OMOD_SHARED_SET(rb_cref_t *cref)
cref->flags |= CREF_FL_OMOD_SHARED;
}

static inline int
CREF_DYNAMIC(const rb_cref_t *cref)
{
return cref->flags & CREF_FL_DYNAMIC_CREF;
}

static inline void
CREF_DYNAMIC_SET(rb_cref_t *cref)
{
cref->flags |= CREF_FL_DYNAMIC_CREF;
}

static inline void
CREF_OMOD_SHARED_UNSET(rb_cref_t *cref)
{
Expand Down
8 changes: 7 additions & 1 deletion insns.def
Original file line number Diff line number Diff line change
Expand Up @@ -811,10 +811,16 @@ defineclass

rb_iseq_check(class_iseq);

rb_cref_t *cref = vm_cref_push(ec, klass, NULL, FALSE, FALSE);

if (VM_DEFINECLASS_DYNAMIC_CREF_P(flags)) {
CREF_DYNAMIC_SET(cref);
}

/* enter scope */
vm_push_frame(ec, class_iseq, VM_FRAME_MAGIC_CLASS | VM_ENV_FLAG_LOCAL, klass,
GC_GUARDED_PTR(box),
(VALUE)vm_cref_push(ec, klass, NULL, FALSE, FALSE),
(VALUE)cref,
ISEQ_BODY(class_iseq)->iseq_encoded, GET_SP(),
ISEQ_BODY(class_iseq)->local_table_size,
ISEQ_BODY(class_iseq)->stack_max);
Expand Down
1 change: 1 addition & 0 deletions lib/bundler/stub_specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ def manually_installed?

# This is defined directly to avoid having to loading the full spec
def missing_extensions?
return false if RUBY_ENGINE == "jruby"
return false if default_gem?
return false if extensions.empty?
return false if File.exist? gem_build_complete_path
Expand Down
5 changes: 5 additions & 0 deletions lib/rubygems/commands/pristine_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,11 @@ def execute
specs = specs.select {|spec| spec.platform == RUBY_ENGINE || Gem::Platform.local === spec.platform || spec.platform == Gem::Platform::RUBY }

if specs.to_a.empty?
if options[:only_missing_extensions]
say "No gems with missing extensions to restore"
return
end

raise Gem::Exception,
"Failed to find gems #{options[:args]} #{options[:version]}"
end
Expand Down
9 changes: 9 additions & 0 deletions lib/rubygems/package.rb
Original file line number Diff line number Diff line change
Expand Up @@ -556,6 +556,15 @@ def open_tar_gz(io) # :nodoc:
tar = Gem::Package::TarReader.new gzio

yield tar
ensure
# Consume remaining gzip data to prevent the
# "attempt to close unfinished zstream; reset forced" warning
# when the GzipReader is closed with unconsumed compressed data.
begin
IO.copy_stream(gzio, IO::NULL)
rescue Zlib::GzipFile::Error, IOError
nil
end
end
end

Expand Down
1 change: 1 addition & 0 deletions lib/rubygems/specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2070,6 +2070,7 @@ def method_missing(sym, *a, &b) # :nodoc:
# probably want to build_extensions

def missing_extensions?
return false if RUBY_ENGINE == "jruby"
return false if extensions.empty?
return false if default_gem?
return false if File.exist? gem_build_complete_path
Expand Down
1 change: 1 addition & 0 deletions lib/rubygems/stub_specification.rb
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ def raw_require_paths # :nodoc:
end

def missing_extensions?
return false if RUBY_ENGINE == "jruby"
return false if default_gem?
return false if extensions.empty?
return false if File.exist? gem_build_complete_path
Expand Down
37 changes: 34 additions & 3 deletions prism_compile.c
Original file line number Diff line number Diff line change
Expand Up @@ -1377,16 +1377,37 @@ pm_new_child_iseq(rb_iseq_t *iseq, pm_scope_node_t *node, VALUE name, const rb_i
return ret_iseq;
}

static int
pm_cpath_const_p(const pm_node_t *node)
{
switch (PM_NODE_TYPE(node)) {
case PM_CONSTANT_READ_NODE:
return TRUE;
case PM_CONSTANT_PATH_NODE:
{
const pm_node_t *parent = ((const pm_constant_path_node_t *) node)->parent;
if (!parent) return TRUE; /* ::Foo */
return pm_cpath_const_p(parent);
}
default:
return FALSE;
}
}

static int
pm_compile_class_path(rb_iseq_t *iseq, const pm_node_t *node, const pm_node_location_t *node_location, LINK_ANCHOR *const ret, bool popped, pm_scope_node_t *scope_node)
{
if (PM_NODE_TYPE_P(node, PM_CONSTANT_PATH_NODE)) {
const pm_node_t *parent = ((const pm_constant_path_node_t *) node)->parent;

if (parent) {
/* Bar::Foo */
/* Bar::Foo or expr::Foo */
PM_COMPILE(parent);
return VM_DEFINECLASS_FLAG_SCOPED;
int flags = VM_DEFINECLASS_FLAG_SCOPED;
if (!pm_cpath_const_p(parent)) {
flags |= VM_DEFINECLASS_FLAG_DYNAMIC_CREF;
}
return flags;
}
else {
/* toplevel class ::Foo */
Expand Down Expand Up @@ -10342,7 +10363,17 @@ pm_compile_node(rb_iseq_t *iseq, const pm_node_t *node, LINK_ANCHOR *const ret,

ID singletonclass;
CONST_ID(singletonclass, "singletonclass");
PUSH_INSN3(ret, location, defineclass, ID2SYM(singletonclass), child_iseq, INT2FIX(VM_DEFINECLASS_TYPE_SINGLETON_CLASS));

/* `class << self` in a class body and `class << Foo` (constant
receiver) are stable. All other forms are potentially dynamic. */
int sclass_flags = VM_DEFINECLASS_TYPE_SINGLETON_CLASS;
if (!(PM_NODE_TYPE_P(cast->expression, PM_SELF_NODE) &&
ISEQ_BODY(iseq)->type == ISEQ_TYPE_CLASS) &&
!pm_cpath_const_p(cast->expression)) {
sclass_flags |= VM_DEFINECLASS_FLAG_DYNAMIC_CREF;
}

PUSH_INSN3(ret, location, defineclass, ID2SYM(singletonclass), child_iseq, INT2FIX(sclass_flags));

if (popped) PUSH_INSN(ret, location, pop);
RB_OBJ_WRITTEN(iseq, Qundef, (VALUE) child_iseq);
Expand Down
8 changes: 6 additions & 2 deletions spec/bundler/bundler/stub_specification_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -49,10 +49,14 @@
expect(stub.missing_extensions?).to be false
end

it "returns true if not manually_installed?" do
it "returns #{RUBY_ENGINE == "jruby" ? "false" : "true"} if not manually_installed?" do
stub = described_class.from_stub(with_bundler_stub_spec)
stub.installed_by_version = Gem::Version.new(1)
expect(stub.missing_extensions?).to be true
if RUBY_ENGINE == "jruby"
expect(stub.missing_extensions?).to be false
else
expect(stub.missing_extensions?).to be true
end
end
end

Expand Down
2 changes: 1 addition & 1 deletion spec/bundler/install/gemfile/git_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1404,7 +1404,7 @@
File.open(git_reader.path.join("ext/foo.c"), "w") do |file|
file.write <<-C
#include "ruby.h"
VALUE foo() { return INT2FIX(#{i}); }
VALUE foo(VALUE self) { return INT2FIX(#{i}); }
void Init_foo() { rb_define_global_function("foo", &foo, 0); }
C
end
Expand Down
23 changes: 23 additions & 0 deletions test/ruby/test_class.rb
Original file line number Diff line number Diff line change
Expand Up @@ -733,6 +733,29 @@ def xyzzy
}
end

def test_dynamic_module_cpath_constant_namespace # [Bug #20948]
assert_separately([], <<~'RUBY')
module M1
module Foo
X = 1
end
end

module M2
module Foo
X = 2
end
end

results = [M1, M2].map do
module it::Foo
X
end
end
assert_equal([1, 2], results)
RUBY
end

def test_namescope_error_message
m = Module.new
o = m.module_eval "class A\u{3042}; self; end.new"
Expand Down
28 changes: 28 additions & 0 deletions test/ruby/test_variable.rb
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,34 @@ class Parent
TestVariable.send(:remove_const, :Parent) rescue nil
end

def test_cvar_cache_invalidated_by_parent_class_variable_set
m = Module.new { class_variable_set(:@@x, 1) }
a = Class.new
b = Class.new(a) do
include m
class_eval "def self.x; @@x; end"
end
assert_equal 1, b.x # warm cache
a.class_variable_set(:@@x, 2)
error = assert_raise(RuntimeError) { b.x }
assert_match(/class variable @@x of .+ is overtaken by .+/, error.message)
end

def test_cvar_cache_invalidated_by_module_class_variable_set
m = Module.new
n = Module.new
b = Class.new do
include m
include n
class_eval "def self.x; @@x; end"
end
m.class_variable_set(:@@x, 1)
assert_equal 1, b.x # warm cache
n.class_variable_set(:@@x, 2)
error = assert_raise(RuntimeError) { b.x }
assert_match(/class variable @@x of .+ is overtaken by .+/, error.message)
end

def test_cvar_overtaken_by_module
error = eval <<~EORB
class ParentForModule
Expand Down
2 changes: 1 addition & 1 deletion test/ruby/test_yjit.rb
Original file line number Diff line number Diff line change
Expand Up @@ -547,7 +547,7 @@ def get_foo
end

def test_opt_getconstant_path_slowpath
assert_compiles(<<~RUBY, exits: { opt_getconstant_path: 1 }, result: [42, 42, 1, 1], call_threshold: 2)
assert_compiles(<<~RUBY, result: [42, 42, 1, 1], call_threshold: 2)
class A
FOO = 42
class << self
Expand Down
10 changes: 7 additions & 3 deletions test/rubygems/test_gem.rb
Original file line number Diff line number Diff line change
Expand Up @@ -1304,10 +1304,14 @@ def test_self_try_activate_missing_extensions
refute Gem.try_activate "nonexistent"
end

expected = "Ignoring ext-1 because its extensions are not built. " \
"Try: gem pristine ext --version 1\n"
if RUBY_ENGINE == "jruby"
assert_equal "", err
else
expected = "Ignoring ext-1 because its extensions are not built. " \
"Try: gem pristine ext --version 1\n"

assert_equal expected, err
assert_equal expected, err
end
end

def test_self_use_paths_with_nils
Expand Down
8 changes: 7 additions & 1 deletion test/rubygems/test_gem_commands_pristine_command.rb
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,13 @@ def test_execute_extensions_only_missing_extensions
end

refute_includes @ui.output, "Restored #{a.full_name}"
assert_includes @ui.output, "Restored #{b.full_name}"

if Gem.java_platform?
refute_includes @ui.output, "Restored #{b.full_name}"
assert_includes @ui.output, "No gems with missing extensions to restore"
else
assert_includes @ui.output, "Restored #{b.full_name}"
end
end

def test_execute_no_extension
Expand Down
Loading