diff --git a/core/pom.xml b/core/pom.xml
index 5dfeaa66a54..13b9f0186de 100644
--- a/core/pom.xml
+++ b/core/pom.xml
@@ -55,8 +55,8 @@ DO NOT MODIFY - GENERATED CODE
${build.dir}/test-results
provided
2019c
- 4.0.0
- 4.0
+ 4.1.0
+ 4.1
0
diff --git a/core/src/main/java/org/jruby/IncludedModuleWrapper.java b/core/src/main/java/org/jruby/IncludedModuleWrapper.java
index 340e55c1bde..79c1bc62999 100644
--- a/core/src/main/java/org/jruby/IncludedModuleWrapper.java
+++ b/core/src/main/java/org/jruby/IncludedModuleWrapper.java
@@ -77,6 +77,23 @@ public RubyModule getDelegate() {
return origin;
}
+ /**
+ * The class or module whose include or prepend created this wrapper. This may itself be a wrapper when an
+ * include into a module is propagated to the hierarchies already including that module. Null for wrappers
+ * created for refinements.
+ *
+ * MRI: RCLASS_INCLUDER
+ */
+ public RubyModule getIncluder() {
+ return includer;
+ }
+
+ void setIncluder(RubyModule includer) {
+ this.includer = includer;
+ }
+
+ private RubyModule includer;
+
@Override
public boolean isIncluded() {
return true;
diff --git a/core/src/main/java/org/jruby/RubyBignum.java b/core/src/main/java/org/jruby/RubyBignum.java
index c602ee4fe24..9d43a67fc0a 100644
--- a/core/src/main/java/org/jruby/RubyBignum.java
+++ b/core/src/main/java/org/jruby/RubyBignum.java
@@ -723,6 +723,15 @@ public IRubyObject bit_length(ThreadContext context) {
return asFixnum(context, value.bitLength());
}
+ @Override
+ public IRubyObject bit_count(ThreadContext context) {
+ if (value.signum() < 0) {
+ throw argumentError(context, "bit_count is undefined for negative integers");
+ }
+
+ return asFixnum(context, value.bitCount());
+ }
+
@CRuby(value = "rb_big_xor", file = "bignum.c")
@Override
public IRubyObject op_xor(ThreadContext context, IRubyObject other) {
@@ -735,9 +744,7 @@ public IRubyObject op_xor(ThreadContext context, IRubyObject other) {
return coerceBit(context, sites(context).checked_op_xor, other);
}
- /** rb_big_neg
- *
- */
+ @CRuby(value = "rb_big_neg", file = "bignum.c")
@Override
public IRubyObject op_neg(ThreadContext context) {
return RubyBignum.newBignum(context.runtime, value.not());
diff --git a/core/src/main/java/org/jruby/RubyFixnum.java b/core/src/main/java/org/jruby/RubyFixnum.java
index df741f6421d..59fc2c35691 100644
--- a/core/src/main/java/org/jruby/RubyFixnum.java
+++ b/core/src/main/java/org/jruby/RubyFixnum.java
@@ -1597,6 +1597,16 @@ public IRubyObject bit_length(ThreadContext context) {
return asFixnum(context, 64 - Long.numberOfLeadingZeros(tmpValue));
}
+ @Override
+ public IRubyObject bit_count(ThreadContext context) {
+ long value = getValue();
+ if (value < 0) {
+ throw argumentError(context, "bit_count is undefined for negative integers");
+ }
+
+ return asFixnum(context, Long.bitCount(value));
+ }
+
@Deprecated(since = "10.0.3.0")
@Override
public IRubyObject id() {
diff --git a/core/src/main/java/org/jruby/RubyHash.java b/core/src/main/java/org/jruby/RubyHash.java
index b83eb7e460a..daae42cca6a 100644
--- a/core/src/main/java/org/jruby/RubyHash.java
+++ b/core/src/main/java/org/jruby/RubyHash.java
@@ -1093,6 +1093,12 @@ public IRubyObject to_proc(ThreadContext context) {
return getDelegate().to_proc(context);
}
+ /**
+ * @deprecated This method will be removed in Ruby 4.5, one version after removal of the
+ * {@code ruby2_keywords} mechanism. See
+ * Feature #22205 for the schedule.
+ */
+ @Deprecated(since = "ruby 4.1", forRemoval = true)
@JRubyMethod(meta = true)
public static IRubyObject ruby2_keywords_hash(ThreadContext context, IRubyObject _self, IRubyObject arg) {
TypeConverter.checkType(context, arg, hashClass(context));
@@ -1103,6 +1109,12 @@ public static IRubyObject ruby2_keywords_hash(ThreadContext context, IRubyObject
return hash;
}
+ /**
+ * @deprecated This method will be removed in Ruby 4.5, one version after removal of the
+ * {@code ruby2_keywords} mechanism. See
+ * Feature #22205 for the schedule.
+ */
+ @Deprecated(since = "ruby 4.1", forRemoval = true)
@JRubyMethod(meta = true, name = "ruby2_keywords_hash?")
public static IRubyObject ruby2_keywords_hash_p(ThreadContext context, IRubyObject _self, IRubyObject arg) {
TypeConverter.checkType(context, arg, hashClass(context));
diff --git a/core/src/main/java/org/jruby/RubyInteger.java b/core/src/main/java/org/jruby/RubyInteger.java
index 0b53ad7aaae..751df430531 100644
--- a/core/src/main/java/org/jruby/RubyInteger.java
+++ b/core/src/main/java/org/jruby/RubyInteger.java
@@ -1081,6 +1081,9 @@ public RubyInteger op_rshift(ThreadContext context, long other) {
@JRubyMethod(name = "bit_length")
public abstract IRubyObject bit_length(ThreadContext context);
+ @JRubyMethod(name = "bit_count")
+ public abstract IRubyObject bit_count(ThreadContext context);
+
@Deprecated(since = "10.0.0.0")
boolean isOne() {
return isOne(getCurrentContext());
diff --git a/core/src/main/java/org/jruby/RubyMatchData.java b/core/src/main/java/org/jruby/RubyMatchData.java
index 9d1fd202578..3f2f7f87b9c 100644
--- a/core/src/main/java/org/jruby/RubyMatchData.java
+++ b/core/src/main/java/org/jruby/RubyMatchData.java
@@ -55,6 +55,7 @@
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
import org.jruby.util.ByteListHolder;
+import org.jruby.util.ConvertBytes;
import org.jruby.util.RegexpOptions;
import org.jruby.util.StringSupport;
@@ -577,7 +578,7 @@ private int namevToBackrefNumber(ThreadContext context, IRubyObject name) {
}
private int nameToBackrefError(ThreadContext context, String name) {
- throw indexError(context, "undefined group name reference " + name);
+ throw indexError(context, "undefined group name reference: " + name);
}
@CRuby(value = "match_ary_subseq", file = "re.c")
@@ -662,6 +663,43 @@ public final IRubyObject at(ThreadContext context, final int nth) {
return RubyRegexp.nth_match(context, nth, this);
}
+ @CRuby(value = "match_integer_at", file = "re.c")
+ @JRubyMethod
+ public IRubyObject integer_at(ThreadContext context, IRubyObject idx) {
+ return integerAt(context, idx, null);
+ }
+
+ @JRubyMethod
+ public IRubyObject integer_at(ThreadContext context, IRubyObject idx, IRubyObject base) {
+ return integerAt(context, idx, base);
+ }
+
+ private IRubyObject integerAt(ThreadContext context, IRubyObject idx, IRubyObject baseArg) {
+ check(context);
+
+ int nth;
+ if (idx instanceof RubyFixnum) {
+ nth = toInt(context, idx);
+ } else if ((nth = namevToBackrefNumber(context, idx)) < 0) {
+ nameToBackrefError(context, idx.asString().toString());
+ }
+
+ int base = baseArg == null ? 10 : toInt(context, baseArg);
+ if (base < 0 || base == 1 || base > 36) {
+ throw argumentError(context, "invalid radix " + base);
+ }
+
+ int numRegs = regs == null ? 1 : regs.getNumRegs();
+ if (nth >= numRegs) return context.nil;
+ if (nth < 0 && (nth += numRegs) <= 0) return context.nil;
+
+ int start = regs == null ? begin : regs.getBeg(nth);
+ if (start < 0) return context.nil;
+ int end = regs == null ? this.end : regs.getEnd(nth);
+
+ return ConvertBytes.byteListToInumOrNil(context.runtime, str.getByteList(), start, end, base);
+ }
+
@CRuby(value = "match_size", file = "re.c")
@JRubyMethod(name = {"size", "length"})
public IRubyObject size(ThreadContext context) {
diff --git a/core/src/main/java/org/jruby/RubyModule.java b/core/src/main/java/org/jruby/RubyModule.java
index a1fc0713a01..dedd20f8c7d 100644
--- a/core/src/main/java/org/jruby/RubyModule.java
+++ b/core/src/main/java/org/jruby/RubyModule.java
@@ -1134,6 +1134,9 @@ public IRubyObject using(ThreadContext context, IRubyObject refinedModule) {
if (context.getCurrentStaticScope().isWithinMethod()) {
throw runtimeError(context, "Module#using is not permitted in methods");
}
+ if (context.getCurrentStaticScope().isWithinRefinementsProc()) {
+ throw runtimeError(context, "Module#using is not permitted in a proc with refinements");
+ }
// I pass the cref even though I don't need to so that the concept is simpler to read
StaticScope staticScope = context.getCurrentStaticScope();
@@ -1164,7 +1167,12 @@ private static void usingModuleRecursive(ThreadContext context, RubyModule cref,
RefinementStore refinementStore = module.refinementStore;
if (refinementStore == null) return; // No refinements registered for this module
- Map refinements = refinementStore.refinements;
+ // Snapshot under the map's own lock: Proc#refined activates refinements from arbitrary threads
+ // while Module#refine on another thread may be mutating this map.
+ Map refinements;
+ synchronized (refinementStore.refinements) {
+ refinements = new IdentityHashMap<>(refinementStore.refinements);
+ }
for (Map.Entry entry: refinements.entrySet()) {
usingRefinement(context, cref, entry.getKey(), entry.getValue());
}
@@ -2534,7 +2542,7 @@ public void putAlias(ThreadContext context, String id, CacheEntry entry, String
}
private CacheEntry searchForAliasMethod(ThreadContext context, String id) {
- return deepMethodSearch(context, id);
+ return deepMethodSearch(context, id, true);
}
private void checkAliasFrameAccesses(ThreadContext context, String id, String newName, DynamicMethod method) {
@@ -2809,6 +2817,10 @@ private void exportMethod(ThreadContext context, String name, Visibility visibil
}
private CacheEntry deepMethodSearch(ThreadContext context, String id) {
+ return deepMethodSearch(context, id, false);
+ }
+
+ private CacheEntry deepMethodSearch(ThreadContext context, String id, boolean forAlias) {
CacheEntry orig = searchWithCache(id);
if (orig.method.isRefined()) {
orig = resolveRefinedMethod(null, orig, id);
@@ -2820,6 +2832,12 @@ private CacheEntry deepMethodSearch(ThreadContext context, String id) {
RubySymbol name = asSymbol(context, id);
throw nameError(context, undefinedMethodMessage(context.runtime, name, rubyName(context), isModule()), name);
}
+
+ // Bug #22276: modules aliasing methods found only via the Object fallback
+ if (forAlias) {
+ warnDeprecated(context, "the fallback to Object for alias of '" + id + "' in module '" +
+ rubyName(context) + "' is deprecated and will be removed in Ruby 4.3");
+ }
}
return orig;
@@ -2945,9 +2963,21 @@ private Visibility getCurrentVisibilityForDefineMethod(ThreadContext context) {
return context.getCurrentFrame().getSelf() == this ? context.getCurrentVisibility() : PUBLIC;
}
+ private static void checkNotRefinementsProc(ThreadContext context, Block block) {
+ // A method defined from a Proc#refined proc would silently drop its refinements (a method is invoked
+ // against its method entry, not the proc's refinement scope), so reject it.
+ if (block.getBody() instanceof IRBlockBody body) {
+ IRClosure closure = body.getScope();
+ if (closure != null && closure.isRefinementsClone()) {
+ throw argumentError(context, "can't define a method from a Proc with refinements");
+ }
+ }
+ }
+
public IRubyObject defineMethodFromBlock(ThreadContext context, IRubyObject arg0, Block block, Visibility visibility) {
RubySymbol name = checkID(context, arg0);
if (!block.isGiven()) throw argumentError(context, "tried to create Proc object without a block");
+ checkNotRefinementsProc(context, block);
if ("initialize".equals(name.idString())) visibility = PRIVATE;
@@ -2991,6 +3021,7 @@ public IRubyObject defineMethodFromCallable(ThreadContext context, IRubyObject a
if (context.runtime.getProc().isInstance(arg1)) {
// double-testing args.length here, but it avoids duplicating the proc-setup code in two places
RubyProc proc = (RubyProc)arg1;
+ checkNotRefinementsProc(context, proc.getBlock());
newMethod = createProcMethod(context.runtime, name.idString(), visibility, proc.getBlock());
} else if (arg1 instanceof AbstractRubyMethod) {
@@ -3133,6 +3164,52 @@ public RubyArray ancestors(ThreadContext context) {
return newArray(context, getAncestorList());
}
+ /**
+ * Returns the classes and modules that have this module in their ancestors, excluding this module itself,
+ * singleton classes, and refinements. The order is not defined.
+ *
+ * MRI: rb_mod_descendants
+ */
+ @JRubyMethod(name = "descendants")
+ public RubyArray descendants(ThreadContext context) {
+ var descendants = newArray(context);
+ Set visited = Collections.newSetFromMap(new IdentityHashMap<>());
+ visited.add(this);
+
+ addDescendants(context, this, descendants, visited);
+
+ return descendants;
+ }
+
+ // MRI: module_descendants_add
+ private static void addDescendants(ThreadContext context, RubyModule module, RubyArray descendants, Set visited) {
+ RubyClass.BiConsumerIgnoresSecond visitor = entry -> {
+ RubyModule descendant = descendantFor(entry);
+ if (descendant == null || !visited.add(descendant)) return;
+
+ descendants.append(context, descendant);
+ addDescendants(context, descendant, descendants, visited);
+ };
+
+ if (module instanceof RubyClass klass) klass.getSubclassesForRead().forEachClass(visitor);
+ module.getIncludingHierarchiesForRead().forEachClass(visitor);
+ }
+
+ // MRI: module_descendants_recursive, resolves hierarchy entries to the class or module they belong to
+ private static RubyModule descendantFor(RubyClass entry) {
+ RubyModule descendant = entry;
+
+ while (descendant instanceof IncludedModuleWrapper || descendant instanceof PrependedModule) {
+ descendant = descendant instanceof IncludedModuleWrapper wrapper ?
+ wrapper.getIncluder() : // an include, or one propagated through another wrapper
+ descendant.getOrigin(); // holder of a prepending class's own methods
+ }
+
+ if (descendant == null || descendant.isIncluded() || descendant.isSingleton() || descendant.isRefinement()) return null;
+
+ return descendant;
+ }
+
@Deprecated(since = "10.0.0.0")
public RubyArray ancestors() {
return ancestors(getCurrentContext());
@@ -3405,6 +3482,12 @@ public void addWriteAttribute(ThreadContext context, String name) {
addAccessor(context, newHardSymbol(context.runtime, name.intern()), PUBLIC, false, true);
}
+ /**
+ * @deprecated Use {@code ...} argument forwarding or other delegation styles instead; they work correctly on
+ * Ruby 3.0 and later. This method will be removed in Ruby 4.4. See
+ * Feature #22205 for the schedule.
+ */
+ @Deprecated(since = "ruby 4.1", forRemoval = true)
@JRubyMethod(required = 1, rest = true, checkArity = false, visibility = PRIVATE)
public IRubyObject ruby2_keywords(ThreadContext context, IRubyObject[] args) {
Arity.checkArgumentCount(context, args, 1, -1);
@@ -3989,6 +4072,12 @@ public RubyBoolean method_defined_p(ThreadContext context, IRubyObject symbol, I
return asBoolean(context, visibility != UNDEFINED && visibility != PRIVATE);
}
+ @JRubyMethod(name = "method_defined?")
+ public RubyBoolean method_defined_p(ThreadContext context, IRubyObject symbol, IRubyObject includeSuper, IRubyObject includeAll) {
+ Visibility visibility = checkMethodVisibility(context, symbol, includeSuper.isTrue());
+ return asBoolean(context, visibility != UNDEFINED && (visibility != PRIVATE || includeAll.isTrue()));
+ }
+
@JRubyMethod(name = "public_method_defined?")
public IRubyObject public_method_defined(ThreadContext context, IRubyObject symbol) {
return asBoolean(context, checkMethodVisibility(context, symbol, true) == PUBLIC);
@@ -4393,7 +4482,8 @@ private RubyModule proceedWithInclude(ThreadContext context, RubyModule insertAb
// In the current logic, if we getService here we know that module is not an
// IncludedModuleWrapper, so there's no need to fish out the delegate. But just
// in case the logic should change later, let's do it anyway
- RubyClass wrapper = new IncludedModuleWrapper(context.runtime, insertAbove.getSuperClass(), moduleToInclude, moduleToInclude.getMethodLocation());
+ IncludedModuleWrapper wrapper = new IncludedModuleWrapper(context.runtime, insertAbove.getSuperClass(), moduleToInclude, moduleToInclude.getMethodLocation());
+ wrapper.setIncluder(this);
// if the insertion point is a class, update subclass lists
if (insertAbove instanceof RubyClass insertAboveClass) {
diff --git a/core/src/main/java/org/jruby/RubyObjectSpace.java b/core/src/main/java/org/jruby/RubyObjectSpace.java
index cb04168fc22..f65f97bf415 100644
--- a/core/src/main/java/org/jruby/RubyObjectSpace.java
+++ b/core/src/main/java/org/jruby/RubyObjectSpace.java
@@ -133,47 +133,6 @@ public static IRubyObject undefine_finalizer(ThreadContext context, IRubyObject
return recv;
}
- @Deprecated(since = "10.0.0.0")
- public static IRubyObject id2ref(IRubyObject recv, IRubyObject id) {
- return id2ref(((RubyBasicObject) recv).getCurrentContext(), recv, id);
- }
-
- @Deprecated(since = "10.1.0.0")
- @JRubyMethod(name = "_id2ref", module = true, visibility = PRIVATE)
- public static IRubyObject id2ref(ThreadContext context, IRubyObject recv, IRubyObject id) {
- Warn.warnDeprecated(context, "ObjectSpace._id2ref is deprecated");
-
- long longId = castAsFixnum(context, id).getValue();
- if (longId == 0) return context.fals;
- if (longId == 20) return context.tru;
- if (longId == 8) return context.nil;
- if ((longId & 0b01) == 0b01) return asFixnum(context, (longId - 1) / 2); // fixnum
- if ((longId & 0b11) == 0b10) {
- // flonum
- double d = 0.0;
- if (longId != 0x8000000000000002L) {
- long b63 = (longId >>> 63);
- /* e: xx1... -> 011... */
- /* xx0... -> 100... */
- /* ^b63 */
- long longBits = Numeric.rotr((2 - b63) | (longId & ~0x03), 3);
- d = Double.longBitsToDouble(longBits);
- }
- return asFloat(context, d);
- } else {
- if (context.runtime.isObjectSpaceEnabled()) {
- IRubyObject object = context.runtime.getObjectSpace().id2ref(longId);
- if (object == null) {
- return context.nil;
- }
- return object;
- } else {
- warn(context, "ObjectSpace is disabled; _id2ref only supports immediates, pass -X+O to enable");
- throw rangeError(context, String.format("0x%016x is not id value", longId));
- }
- }
- }
-
public static IRubyObject each_objectInternal(final ThreadContext context, IRubyObject recv, IRubyObject[] args, final Block block) {
final Ruby runtime = context.runtime;
final RubyModule rubyClass;
diff --git a/core/src/main/java/org/jruby/RubyProc.java b/core/src/main/java/org/jruby/RubyProc.java
index 96b7e25dbc2..0fce549f3b6 100644
--- a/core/src/main/java/org/jruby/RubyProc.java
+++ b/core/src/main/java/org/jruby/RubyProc.java
@@ -39,6 +39,9 @@
import org.jruby.anno.JRubyMethod;
import org.jruby.ast.util.ArgsUtil;
+import org.jruby.ir.IRClosure;
+import org.jruby.ir.IRRefinedClosure;
+import org.jruby.ir.interpreter.Interpreter;
import org.jruby.ir.runtime.IRRuntimeHelpers;
import org.jruby.java.codegen.BlockInterfaceGenerator;
import org.jruby.javasupport.Java;
@@ -58,6 +61,7 @@
import static org.jruby.api.Convert.asBoolean;
import static org.jruby.api.Convert.asFixnum;
+import static org.jruby.api.Convert.castAsModule;
import static org.jruby.api.Create.*;
import static org.jruby.api.Define.defineClass;
import static org.jruby.api.Error.argumentError;
@@ -284,6 +288,12 @@ public IRubyObject to_s(ThreadContext context) {
return string;
}
+ /**
+ * @deprecated Use explicit delegation ({@code *args, **kwargs}) instead; it works correctly on Ruby 3.0 and
+ * later. This method will be removed in Ruby 4.4. See
+ * Feature #22205 for the schedule.
+ */
+ @Deprecated(since = "ruby 4.1", forRemoval = true)
@JRubyMethod
public IRubyObject ruby2_keywords(ThreadContext context) {
checkFrozen();
@@ -423,6 +433,52 @@ public RubyProc to_proc() {
return this;
}
+ /**
+ * Return a new Proc whose body runs with the given modules' refinements active; the receiver is unchanged
+ * and the captured environment is shared. With no arguments, returns the receiver. Chained calls merge
+ * the module lists, later modules taking precedence. The block's IR is deep-copied, but only when the
+ * returned proc is first called, and the copy is cached.
+ */
+ @JRubyMethod(name = "refined", rest = true)
+ public IRubyObject refined(ThreadContext context, IRubyObject[] args) {
+ if (args.length == 0) return this;
+
+ BlockBody body = block.getBody();
+ if (fromMethod || !(body instanceof IRBlockBody)) {
+ throw argumentError(context, "can't apply refinements to a Proc without a Ruby block");
+ }
+
+ RubyModule[] modules = new RubyModule[args.length];
+ for (int i = 0; i < args.length; i++) {
+ modules[i] = castAsModule(context, args[i]);
+ }
+
+ IRClosure source = ((IRBlockBody) body).getScope();
+ // A chained call re-clones from the original closure with the merged list, sharing the memo slot
+ // with the equivalent single call.
+ if (source instanceof IRRefinedClosure refinedScope) {
+ RubyModule[] chained = refinedScope.getRefinementsModules();
+ RubyModule[] merged = new RubyModule[chained.length + modules.length];
+ System.arraycopy(chained, 0, merged, 0, chained.length);
+ System.arraycopy(modules, 0, merged, chained.length, modules.length);
+ modules = merged;
+ source = refinedScope.getRefinementsSource();
+ }
+
+ IRClosure refinedClosure = source.refinementsClone(context, modules);
+ // Share the captured environment (binding holds the dynamic scope); clone the binding wrapper only so that
+ // proc setup (file/line/dummy-scope) does not mutate the original proc's binding.
+ Binding newBinding = block.getBinding().clone();
+ // A block from compiled top-level code has a null frame method name; the clone always runs interpreted,
+ // so give it the interpreter's top-level name or building a backtrace through it would crash.
+ if (newBinding.getMethod() == null) newBinding.setMethod(Interpreter.ROOT);
+ Block newBlock = new Block(refinedClosure.getBlockBody(), newBinding, block.type);
+
+ // The real class, not the receiver's metaclass: the refined proc must not inherit the receiver's
+ // singleton state (as in CRuby). Proc subclasses are still preserved.
+ return newProc(context.runtime, getMetaClass().getRealClass(), newBlock, type, file, line);
+ }
+
@JRubyMethod
public IRubyObject source_location(ThreadContext context) {
if (file != null) return newArray(context, newString(context, file), asFixnum(context, line + 1 /*zero-based*/));
diff --git a/core/src/main/java/org/jruby/RubyRange.java b/core/src/main/java/org/jruby/RubyRange.java
index f3596d3b051..bf229bb7793 100644
--- a/core/src/main/java/org/jruby/RubyRange.java
+++ b/core/src/main/java/org/jruby/RubyRange.java
@@ -1273,6 +1273,69 @@ public IRubyObject count(ThreadContext context, IRubyObject arg, Block block) {
return RubyEnumerable.count(context, this, arg, block);
}
+ @JRubyMethod
+ public IRubyObject clamp(ThreadContext context, IRubyObject range) {
+ RangeLike bounds = rangeValues(context, range);
+ if (bounds == null) throw typeError(context, range, "Range");
+
+ return clamp(context, bounds.begin, bounds.end, bounds.excl);
+ }
+
+ @JRubyMethod
+ public IRubyObject clamp(ThreadContext context, IRubyObject min, IRubyObject max) {
+ return clamp(context, min, max, false);
+ }
+
+ // MRI: range_clamp
+ private IRubyObject clamp(ThreadContext context, IRubyObject min, IRubyObject max, boolean excl) {
+ IRubyObject selfBeg = begin;
+ IRubyObject selfEnd = end;
+ boolean selfExcl = isExclusive;
+ int clampBeg = 0, clampEnd = 0;
+
+ if (!min.isNil() && !max.isNil() && rangeCmp(context, min, max) > 0) {
+ throw argumentError(context, "min argument must be less than or equal to max argument");
+ }
+
+ if (!min.isNil()) {
+ if (selfBeg.isNil() || rangeCmp(context, selfBeg, min) < 0) {
+ clampBeg = -1;
+ selfBeg = min;
+ }
+ if (!selfEnd.isNil() && rangeCmp(context, selfEnd, min) < 0) {
+ clampEnd = -1;
+ selfEnd = min;
+ }
+ }
+ if (!max.isNil()) {
+ if (clampBeg == 0 && !selfBeg.isNil() && rangeCmp(context, selfBeg, max) > 0) {
+ clampBeg = 1;
+ selfBeg = max;
+ }
+ if (clampEnd == 0) {
+ int cmp = selfEnd.isNil() ? 1 : rangeCmp(context, selfEnd, max);
+ if (cmp > 0) {
+ clampEnd = 1;
+ selfEnd = max;
+ selfExcl = excl;
+ } else if (cmp == 0) {
+ selfExcl |= excl;
+ }
+ }
+ }
+
+ // self is entirely outside the clamping bounds
+ if (clampBeg != 0 && clampBeg == clampEnd) selfExcl = true;
+
+ return newRange(context, selfBeg, selfEnd, selfExcl);
+ }
+
+ // MRI: r_cmp, raises ArgumentError if a and b are not comparable
+ private static int rangeCmp(ThreadContext context, IRubyObject a, IRubyObject b) {
+ JavaSites.RangeSites sites = sites(context);
+ return RubyComparable.cmpAndCmpint(context, sites.op_cmp, sites.op_gt, sites.op_lt, a, b);
+ }
+
@JRubyMethod
public IRubyObject minmax(ThreadContext context, Block block) {
if (block.isGiven()) return Helpers.invokeSuper(context, this, context.runtime.getRange(), "minmax", NULL_ARRAY, block);
@@ -1372,13 +1435,13 @@ public IRubyObject size(ThreadContext context) {
return context.nil;
}
- @JRubyMethod(name = "to_set", rest = true, frame = true)
- public IRubyObject to_set(ThreadContext context, IRubyObject[] args, Block block) {
+ @JRubyMethod(name = "to_set", frame = true)
+ public IRubyObject to_set(ThreadContext context, Block block) {
if (isEndless) {
throw context.runtime.newRangeError("cannot convert endless range to a set");
}
- return invokeSuper(context, this, context.getFrameKlazz(), context.getFrameName(), args, block);
+ return invokeSuper(context, this, context.getFrameKlazz(), context.getFrameName(), NULL_ARRAY, block);
}
private boolean discreteObject(IRubyObject object) {
diff --git a/core/src/main/java/org/jruby/RubyString.java b/core/src/main/java/org/jruby/RubyString.java
index b1d91b0a151..628ae348243 100644
--- a/core/src/main/java/org/jruby/RubyString.java
+++ b/core/src/main/java/org/jruby/RubyString.java
@@ -53,6 +53,7 @@
import org.jcodings.specific.UTF32LEEncoding;
import org.jcodings.specific.UTF8Encoding;
import org.jcodings.unicode.UnicodeEncoding;
+import org.jcodings.util.IntHash;
import org.joni.Matcher;
import org.joni.Option;
import org.joni.Regex;
@@ -100,7 +101,9 @@
import org.jruby.util.io.RubyInputStream;
import java.nio.charset.Charset;
+import java.util.ArrayList;
import java.util.Arrays;
+import java.util.List;
import java.util.Locale;
import java.util.function.Function;
@@ -129,6 +132,7 @@
import static org.jruby.api.Create.newString;
import static org.jruby.api.Define.defineClass;
import static org.jruby.api.Error.argumentError;
+import static org.jruby.api.Error.encodingCompatibilityError;
import static org.jruby.api.Error.indexError;
import static org.jruby.api.Error.rangeError;
import static org.jruby.api.Error.runtimeError;
@@ -6204,6 +6208,192 @@ public IRubyObject tr_bang(ThreadContext context, IRubyObject src, IRubyObject r
return trTrans(context, src, repl, false);
}
+ @CRuby(value = "rb_str_tr", note = "Hash form")
+ @JRubyMethod(name = "tr")
+ public IRubyObject tr(ThreadContext context, IRubyObject pairs) {
+ RubyString str = dupString(context, this);
+ str.trTransPairs(context, pairs);
+ return str;
+ }
+
+ @CRuby(value = "rb_str_tr_bang", note = "Hash form")
+ @JRubyMethod(name = "tr!")
+ public IRubyObject tr_bang(ThreadContext context, IRubyObject pairs) {
+ return trTransPairs(context, pairs);
+ }
+
+ /**
+ * Replaces each character of self that is a key of the Hash with that key's value, in one left-to-right pass,
+ * so replacement text is never itself replaced. Returns self, or nil if nothing was replaced.
+ */
+ @CRuby("tr_trans_pairs")
+ private IRubyObject trTransPairs(ThreadContext context, IRubyObject arg) {
+ if (!(arg instanceof RubyHash hash)) {
+ throw typeError(context, str(context.runtime, "wrong argument type ", TypeConverter.typeAsString(arg), " (expected Hash)"));
+ }
+
+ // MRI's order: broken string, frozen, then the empty shortcut (which skips validating the keys).
+ // Only the frozen check is needed: the result is built in a new buffer, never written into the old bytes.
+ mustnotBroken(context);
+ modifyCheck();
+ if (value.getRealSize() == 0 || hash.isEmpty()) return context.nil;
+
+ ByteList translated = TrPairs.collect(context, hash, getEncoding(), scanForCodeRange()).translate(value);
+ if (translated == null) return context.nil;
+
+ view(translated);
+ clearCodeRange();
+ return this;
+ }
+
+ /**
+ * The pairs of a tr Hash. collect() converts every key and value to a String, works out the one encoding that
+ * self and all of them can share, and indexes the values by the codepoint of their key in that encoding. The
+ * index has to wait until every pair is seen because a later pair can still change the encoding.
+ */
+ private static final class TrPairs {
+ private record Pair(RubyString key, RubyString value) {}
+
+ // Visits the live Hash: a key's to_str may delete later entries, and MRI then skips them. This visitAll
+ // overload skips them too, like Hash#each; the stateless one raises ConcurrencyError instead.
+ private static final RubyHash.VisitorWithState ADD = new RubyHash.VisitorWithState<>() {
+ @Override
+ public void visit(ThreadContext context, RubyHash self, IRubyObject key, IRubyObject value, int index, TrPairs pairs) {
+ pairs.add(context, key, value);
+ }
+ };
+
+ private final List pairs;
+ private Encoding encoding;
+ private int codeRange; // MRI's &cr: CR_7BIT while everything folded so far is ASCII and may change encoding
+
+ // values by key codepoint, like trTransHelper: an array below TRANS_SIZE, an IntHash above it
+ private ByteList[] low;
+ private IntHash high;
+
+ private TrPairs(Encoding encoding, int codeRange, int size) {
+ this.pairs = new ArrayList<>(size);
+ this.encoding = encoding;
+ this.codeRange = codeRange;
+ }
+
+ static TrPairs collect(ThreadContext context, RubyHash hash, Encoding encoding, int codeRange) {
+ TrPairs pairs = new TrPairs(encoding, codeRange, hash.size());
+ hash.visitAll(context, ADD, pairs);
+ pairs.buildTable();
+ return pairs;
+ }
+
+ private void add(ThreadContext context, IRubyObject key, IRubyObject value) {
+ RubyString search = key.convertToString();
+ RubyString replace = value.convertToString();
+
+ // one character: a single byte, or one (possibly multibyte) character in the key's own encoding
+ if (search.size() != 1 && search.strLength() != 1) {
+ throw argumentError(context, "keys must be of size 1");
+ }
+
+ fold(context, search);
+ fold(context, replace);
+ pairs.add(new Pair(search, replace));
+ }
+
+ /**
+ * Folds one more string into the shared encoding, or raises Encoding::CompatibilityError. As with
+ * Encoding.compatible?, two different encodings can only be mixed when one side is pure ASCII. Unlike a
+ * pairwise check, codeRange remembers whether *everything* so far was ASCII: "ab".tr("a" => latin1_a_grave,
+ * "b" => utf8_e_acute) must raise even though each value is compatible with "ab" on its own.
+ */
+ @CRuby(value = "rb_enc_check_multi_str", file = "encoding.c")
+ private void fold(ThreadContext context, RubyString str) {
+ if (str.size() == 0) return;
+
+ Encoding strEncoding = str.getEncoding();
+ if (encoding == strEncoding) {
+ if (codeRange == CR_7BIT) codeRange = str.scanForCodeRange();
+ return;
+ }
+
+ if (encoding.isAsciiCompatible() && strEncoding.isAsciiCompatible()) {
+ // str is ASCII, so it reads the same in the current encoding
+ if (strEncoding == USASCIIEncoding.INSTANCE || str.scanForCodeRange() == CR_7BIT) return;
+
+ // everything so far is ASCII, so it can all be read in str's encoding instead
+ if (encoding == USASCIIEncoding.INSTANCE || codeRange == CR_7BIT) {
+ encoding = strEncoding;
+ codeRange = str.scanForCodeRange();
+ return;
+ }
+ }
+
+ throw encodingCompatibilityError(context, "incompatible character encodings: " + encoding + " and " + strEncoding);
+ }
+
+ private void buildTable() {
+ low = new ByteList[StringSupport.TRANS_SIZE];
+ for (Pair pair : pairs) {
+ ByteList key = pair.key().getByteList();
+ int c = encoding.mbcToCode(key.unsafeBytes(), key.begin(), key.begin() + key.realSize());
+ if (c < 0) continue; // an invalid key can never match a character of the (valid) receiver
+
+ if (c < low.length) {
+ low[c] = pair.value().getByteList();
+ } else {
+ if (high == null) high = new IntHash<>();
+ high.put(c, pair.value().getByteList()); // later keys win, as with MRI's hash_aset
+ }
+ }
+ }
+
+ private ByteList lookup(int c) {
+ if (c < 0) return null;
+ if (c < low.length) return low[c];
+ return high == null ? null : high.get(c);
+ }
+
+ /**
+ * Returns a copy of source with every key character replaced, tagged with the shared encoding, or null if
+ * there was nothing to replace. source is read in the shared encoding. That only differs from its own
+ * encoding when source is pure ASCII, and ASCII reads the same in every encoding the fold allows.
+ */
+ ByteList translate(ByteList source) {
+ byte[] bytes = source.unsafeBytes();
+ int p = source.begin();
+ int end = p + source.realSize();
+ Encoding enc = encoding;
+ boolean asciiCompatible = enc.isAsciiCompatible(); // false for UTF-16/32, where ASCII is not one byte
+ ByteList[] low = this.low; // a local, so the JIT can keep it in a register through the loop
+
+ // out is created at the first match, so a string with no matches returns null without copying anything.
+ // bytes[checkpoint, p) is unchanged input not yet copied to out.
+ ByteList out = null;
+ int checkpoint = p;
+ while (p < end) {
+ int b = Byte.toUnsignedInt(bytes[p]);
+ int len;
+ ByteList replace;
+ if (b < 0x80 && asciiCompatible) { // an ASCII byte is its own codepoint, so skip decoding
+ len = 1;
+ replace = low[b];
+ } else {
+ len = StringSupport.length(enc, bytes, p, end);
+ replace = lookup(enc.mbcToCode(bytes, p, end));
+ }
+
+ if (replace != null) {
+ if (out == null) out = new ByteList(source.realSize() + 16, enc);
+ out.append(bytes, checkpoint, p - checkpoint);
+ out.append(replace);
+ checkpoint = p + len;
+ }
+ p += len;
+ }
+
+ if (out != null) out.append(bytes, checkpoint, end - checkpoint);
+ return out;
+ }
+ }
+
private IRubyObject trTrans(ThreadContext context, IRubyObject src, IRubyObject repl, boolean sflag) {
RubyString replStr = repl.convertToString();
ByteList replList = replStr.value;
diff --git a/core/src/main/java/org/jruby/RubyThread.java b/core/src/main/java/org/jruby/RubyThread.java
index 6a918b4a21f..299fb0aaca5 100644
--- a/core/src/main/java/org/jruby/RubyThread.java
+++ b/core/src/main/java/org/jruby/RubyThread.java
@@ -1216,11 +1216,16 @@ public IRubyObject thread_variable_get(ThreadContext context, IRubyObject key) {
@JRubyMethod(name = "thread_variable_set")
public IRubyObject thread_variable_set(ThreadContext context, IRubyObject key, IRubyObject value) {
- checkFrozen();
+ if (isFrozen()) throw frozenError(context, this, "can't modify frozen thread locals");
+
key = RubySymbol.idSymbolFromObject(context, key);
final Map locals = getThreadLocals();
synchronized (locals) {
- locals.put(key, value);
+ if (value.isNil()) {
+ locals.remove(key);
+ } else {
+ locals.put(key, value);
+ }
}
return value;
}
diff --git a/core/src/main/java/org/jruby/TopSelfFactory.java b/core/src/main/java/org/jruby/TopSelfFactory.java
index 25bcf3a5bd8..4e739d72653 100644
--- a/core/src/main/java/org/jruby/TopSelfFactory.java
+++ b/core/src/main/java/org/jruby/TopSelfFactory.java
@@ -91,8 +91,10 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
}
});
+ // TODO: Remove ruby2_keywords when JRuby moves to Ruby 4.5 support.
singletonClass.addMethod(context, "ruby2_keywords", new JavaMethod.JavaMethodN(singletonClass, Visibility.PRIVATE, "private") {
@Override
+ @SuppressWarnings("removal")
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args) {
return Object.ruby2_keywords(context, args);
}
@@ -117,6 +119,9 @@ public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule claz
@Override
public IRubyObject call(ThreadContext context, IRubyObject self, RubyModule clazz, String name, IRubyObject[] args) {
Arity.checkArgumentCount(context, args, 1, 1);
+ if (context.getCurrentStaticScope().isWithinRefinementsProc()) {
+ throw context.runtime.newRuntimeError("main.using is not permitted in a proc with refinements");
+ }
RubyModule cref = context.getCurrentStaticScope().getOverlayModuleForWrite(context);
// unclear what the equivalent check would be for us
// rb_control_frame_t * prev_cfp = previous_frame(GET_THREAD());
diff --git a/core/src/main/java/org/jruby/ext/set/EnumerableExt.java b/core/src/main/java/org/jruby/ext/set/EnumerableExt.java
index f0a46680b71..02786fa2bfe 100644
--- a/core/src/main/java/org/jruby/ext/set/EnumerableExt.java
+++ b/core/src/main/java/org/jruby/ext/set/EnumerableExt.java
@@ -29,7 +29,6 @@
package org.jruby.ext.set;
import org.jruby.Ruby;
-import org.jruby.RubyClass;
import org.jruby.anno.JRubyMethod;
import org.jruby.api.Access;
import org.jruby.runtime.Block;
@@ -43,21 +42,11 @@
*/
public abstract class EnumerableExt {
- //@JRubyMethod
+ @JRubyMethod
public static IRubyObject to_set(final ThreadContext context, final IRubyObject self, final Block block) {
RubySet set = new RubySet(context.runtime, Access.getClass(context, "Set"), false);
set.initialize(context, self, block);
return set; // return runtime.getClass("Set").newInstance(context, self, block);
}
- @JRubyMethod(rest = true) // to_set(klass = Set, *args, &block)
- public static IRubyObject to_set(final ThreadContext context, final IRubyObject self,
- final IRubyObject[] args, final Block block) {
-
- if ( args.length == 0 ) return to_set(context, self, block);
-
- final IRubyObject klass = args[0]; args[0] = self;
- return ((RubyClass) klass).newInstance(context, args, block);
- }
-
}
diff --git a/core/src/main/java/org/jruby/ext/set/RubySet.java b/core/src/main/java/org/jruby/ext/set/RubySet.java
index 468e684b5a4..25f660c6046 100644
--- a/core/src/main/java/org/jruby/ext/set/RubySet.java
+++ b/core/src/main/java/org/jruby/ext/set/RubySet.java
@@ -37,13 +37,11 @@
import org.jruby.api.Access;
import org.jruby.api.Create;
import org.jruby.api.Error;
-import org.jruby.api.Warn;
import org.jruby.javasupport.JavaUtil;
import org.jruby.runtime.*;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.runtime.marshal.MarshalDumper;
import org.jruby.runtime.marshal.MarshalLoader;
-import org.jruby.util.ArraySupport;
import org.jruby.util.io.RubyInputStream;
import org.jruby.util.io.RubyOutputStream;
@@ -213,6 +211,22 @@ RubySet newSetFast(final Ruby runtime) {
return newSet(runtime, getMetaClass());
}
+ /**
+ * Construct a new Set with the same class as this one, comparing by identity if this one does.
+ *
+ * @param context the current thread context
+ * @return a new Set
+ */
+ private RubySet newSetPreservingIdentity(final ThreadContext context) {
+ return preserveIdentity(context, newSetFast(context.runtime));
+ }
+
+ // Must be called before any elements are added to set.
+ private RubySet preserveIdentity(final ThreadContext context, final RubySet set) {
+ if (hash.isComparedByIdentity()) set.hash.compare_by_identity(context);
+ return set;
+ }
+
/**
* Construct a new Set. The Set class will be retrieved from the global namespace.
*
@@ -443,7 +457,7 @@ public RubyArray to_a(final ThreadContext context) {
return this.hash.keys(context);
}
- // Returns self if no arguments are given.
+ // Returns self if no block is given.
@JRubyMethod
public RubySet to_set(final ThreadContext context, final Block block) {
if ( block.isGiven() ) {
@@ -454,31 +468,6 @@ public RubySet to_set(final ThreadContext context, final Block block) {
return this;
}
- // Otherwise, converts the set to another with klass.new(self, *args, &block).
- @JRubyMethod(rest = true)
- public RubySet to_set(final ThreadContext context, final IRubyObject[] args, final Block block) {
- if ( args.length == 0 ) return to_set(context, block);
-
- Warn.warnDeprecated(context, "passing arguments to Set#to_set");
-
- IRubyObject klass = args[0];
- final RubyClass Set = Access.getClass(context, "Set");
-
- if (klass == Set && args.length == 1 && !block.isGiven()) return this;
-
- final IRubyObject[] rest;
- if (klass instanceof RubyClass) {
- rest = ArraySupport.newCopy(args, 1, args.length - 1);
- } else {
- klass = Set;
- rest = args;
- }
-
- RubySet set = new RubySet(context.runtime, (RubyClass) klass, false);
- set.initialize(context, rest, block);
- return set;
- }
-
@JRubyMethod
public IRubyObject compare_by_identity(ThreadContext context) {
if (hash.isComparedByIdentity()) return this;
@@ -529,7 +518,7 @@ private void addFlattened(final ThreadContext context, final IdentityHashMap see
// Returns a new set that is a copy of the set, flattening each containing set recursively.
@JRubyMethod
public RubySet flatten(final ThreadContext context) {
- return newSetFast(context.runtime).flatten_merge(context, this);
+ return newSetPreservingIdentity(context).flatten_merge(context, this);
}
@JRubyMethod(name = "flatten!")
@@ -867,17 +856,20 @@ public IRubyObject op_and(final ThreadContext context, IRubyObject enume) {
final RubySet newSet = new RubySet(context.runtime, getMetaClass(), false);
if (enume instanceof RubySet set) {
newSet.allocHash(context, set.size());
+ preserveIdentity(context, newSet);
for ( IRubyObject obj : set.elementsOrdered() ) {
if (containsImpl(obj)) newSet.addImpl(context, obj);
}
} else if (enume instanceof RubyArray ary) {
newSet.allocHash(context, ary.size());
+ preserveIdentity(context, newSet);
for ( int i = 0; i < ary.size(); i++ ) {
final IRubyObject obj = ary.eltInternal(i);
if (containsImpl(obj)) newSet.addImpl(context, obj);
}
} else {
newSet.allocHash(context);
+ preserveIdentity(context, newSet);
// do_with_enum(enum) { |o| newSet.add(o) if include?(o) }
doWithEnum(context, enume, new EachBody(context) {
IRubyObject yieldImpl(ThreadContext context, IRubyObject obj) {
@@ -901,8 +893,18 @@ public IRubyObject op_xor(final ThreadContext context, IRubyObject enume) {
if (enume instanceof RubySet set) {
otherSet = set;
} else {
- otherSet = new RubySet(context.runtime, getMetaClass(), false);
- otherSet.initialize(context, enume, Block.NULL_BLOCK); // Set.new(enum)
+ // Set.new(enum), comparing by identity like self so distinct elements of enum stay distinct
+ final RubySet tmp = otherSet = newSetPreservingIdentity(context);
+ if (enume instanceof RubyArray ary) {
+ for ( int i = 0; i < ary.size(); i++ ) tmp.addImpl(context, ary.eltInternal(i));
+ } else {
+ doWithEnum(context, enume, new EachBody(context) {
+ IRubyObject yieldImpl(ThreadContext context, IRubyObject obj) {
+ tmp.addImpl(context, obj);
+ return context.nil;
+ }
+ });
+ }
}
for (IRubyObject o : otherSet.elementsOrdered()) {
if (newSet.containsImpl(o)) {
@@ -975,7 +977,7 @@ public IRubyObject classify(ThreadContext context, final Block block) {
final IRubyObject key = block.yield(context, i);
RubySet set = (RubySet) h.fastARef(key);
if (set == null) {
- set = newSetFast(context.runtime);
+ set = newSetPreservingIdentity(context);
h.fastASet(key, set);
}
set.invokeAdd(context, i);
@@ -1056,7 +1058,7 @@ IRubyObject setDivideArity2(ThreadContext context, Block block) {
IRubyObject set = hash.op_aref(context, asFixnum(context, root));
if (set.isNil()) {
- set = newSet(context.runtime, setClass);
+ set = preserveIdentity(context, newSet(context.runtime, setClass));
hash.op_aset(context, asFixnum(context, root), set);
finalSet.add(context, set);
}
diff --git a/core/src/main/java/org/jruby/ir/IRClassBody.java b/core/src/main/java/org/jruby/ir/IRClassBody.java
index 03e7507dd59..e317d78c84b 100644
--- a/core/src/main/java/org/jruby/ir/IRClassBody.java
+++ b/core/src/main/java/org/jruby/ir/IRClassBody.java
@@ -9,6 +9,11 @@ public IRClassBody(IRManager manager, IRScope lexicalParent, ByteList name, int
super(manager, lexicalParent, name, lineNumber, scope, executesOnce);
}
+ @Override
+ protected IRModuleBody cloneInstance(IRScope lexicalParent, StaticScope scope) {
+ return new IRClassBody(getManager(), lexicalParent, getByteName(), getLine(), scope, executesOnce());
+ }
+
@Override
public IRScopeType getScopeType() {
return IRScopeType.CLASS_BODY;
diff --git a/core/src/main/java/org/jruby/ir/IRClosure.java b/core/src/main/java/org/jruby/ir/IRClosure.java
index 0fcf88765fa..14148e8b579 100644
--- a/core/src/main/java/org/jruby/ir/IRClosure.java
+++ b/core/src/main/java/org/jruby/ir/IRClosure.java
@@ -1,10 +1,13 @@
package org.jruby.ir;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.EnumSet;
import java.util.List;
+import java.util.Map;
import java.util.function.Supplier;
+import org.jruby.RubyModule;
import org.jruby.RubySymbol;
import org.jruby.ast.DefNode;
import org.jruby.ast.IterNode;
@@ -26,6 +29,7 @@
import org.jruby.runtime.MixedModeIRBlockBody;
import org.jruby.runtime.InterpretedIRBlockBody;
import org.jruby.runtime.Signature;
+import org.jruby.runtime.ThreadContext;
import org.jruby.util.ByteList;
// Closures are contexts/scopes for the purpose of IR building. They are self-contained and accumulate instructions
@@ -74,7 +78,13 @@ protected IRClosure(IRManager manager, IRScope lexicalParent, int lineNumber, St
/** Used by cloning code for inlining */
/* Inlining generates a new name and id and basic cloning will reuse the originals name */
protected IRClosure(IRClosure c, IRScope lexicalParent, int closureId, ByteList fullName) {
- super(c, lexicalParent);
+ // Inlining clones share the source's StaticScope.
+ this(c, lexicalParent, closureId, fullName, c.getStaticScope());
+ }
+
+ /** Used by Proc#refined cloning, which needs each clone to own a fresh StaticScope. */
+ protected IRClosure(IRClosure c, IRScope lexicalParent, int closureId, ByteList fullName, StaticScope staticScope) {
+ super(c, lexicalParent, staticScope);
this.closureId = closureId;
super.setByteName(fullName);
@@ -320,6 +330,12 @@ public LocalVariable getLocalVariable(RubySymbol name, int depth) {
protected IRClosure cloneForInlining(CloneInfo ii, IRClosure clone) {
SimpleCloneInfo clonedII = ii.cloneForCloningClosure(clone);
+ if (ii.isRefinementsClone()) {
+ clone.allocateInterpreterContext(new RefinementsCloneInstrs(clone, interpreterContext, clonedII),
+ interpreterContext.getTemporaryVariableCount(), interpreterContext.getFlags());
+ return clone;
+ }
+
// if (getCFG() != null) {
// clone.setCFG(getCFG().clone(clonedII, clone));
// } else {
@@ -343,24 +359,132 @@ public IRClosure cloneForInlining(CloneInfo ii) {
IRClosure clonedClosure;
IRScope lexicalParent = ii.getScope();
+ // For Proc#refined we give the clone its own StaticScope whose enclosing scope points
+ // at the clone's lexical parent.
+ StaticScope refinementsScope = null;
+ if (ii.isRefinementsClone()) {
+ refinementsScope = getStaticScope().duplicate();
+ refinementsScope.setEnclosingScope(lexicalParent.getStaticScope());
+ }
+
if (ii instanceof SimpleCloneInfo && !((SimpleCloneInfo) ii).isEnsureBlockCloneMode()) {
- clonedClosure = new IRClosure(this, lexicalParent, closureId, getByteName());
+ if (ii.isRefinementsCloneRoot()) {
+ clonedClosure = new IRRefinedClosure(this, lexicalParent, closureId, getByteName(), refinementsScope);
+ } else {
+ clonedClosure = refinementsScope != null ?
+ new IRClosure(this, lexicalParent, closureId, getByteName(), refinementsScope) :
+ new IRClosure(this, lexicalParent, closureId, getByteName());
+ }
} else {
int id = lexicalParent.getNextClosureId();
ByteList fullName = lexicalParent.getByteName();
fullName = fullName != null ? fullName.dup() : new ByteList();
fullName.append(CLOSURE_CLONE);
fullName.append(java.lang.Integer.toString(id).getBytes());
- clonedClosure = new IRClosure(this, lexicalParent, id, fullName);
+ clonedClosure = refinementsScope != null ?
+ new IRClosure(this, lexicalParent, id, fullName, refinementsScope) :
+ new IRClosure(this, lexicalParent, id, fullName);
+ }
+
+ if (refinementsScope != null) {
+ refinementsScope.setIRScope(clonedClosure);
+ clonedClosure.setIsMaybeUsingRefinements();
+ clonedClosure.inRefinementsCloneTree = true;
}
- // WrappedIRClosure should always have a single unique IRClosure in them so we should
- // not end up adding n copies of the same closure as distinct clones...
- lexicalParent.addClosure(clonedClosure);
+ // A Proc#refined root is grafted under the source's real lexical parent, not registered there.
+ if (!ii.isRefinementsCloneRoot()) {
+ // WrappedIRClosure should always have a single unique IRClosure in them so we should
+ // not end up adding n copies of the same closure as distinct clones...
+ lexicalParent.addClosure(clonedClosure);
+ }
return cloneForInlining(ii, clonedClosure);
}
+ /**
+ * Single-slot memo of the most recent refinement-aware clone of a closure (see {@link #refinementsClone}).
+ */
+ static final class RefinementsCache {
+ final RubyModule[] modules;
+ final IRClosure clone;
+
+ RefinementsCache(RubyModule[] modules, IRClosure clone) {
+ this.modules = modules;
+ this.clone = clone;
+ }
+
+ boolean matches(RubyModule[] other) {
+ return Arrays.equals(modules, other);
+ }
+ }
+
+ /**
+ * True on every closure in a Proc#refined clone tree: the top clone and all closures it lexically
+ * encloses (which are deep-copied along with it).
+ */
+ private boolean inRefinementsCloneTree;
+
+ public boolean isInRefinementsCloneTree() {
+ return inRefinementsCloneTree;
+ }
+
+ /** True only on the {@link IRRefinedClosure} backing a Proc#refined proc. */
+ public boolean isRefinementsClone() {
+ return false;
+ }
+
+ /**
+ * Return a refinement-aware clone of this closure for the given modules, reusing the memoized clone
+ * when the same modules (by identity and order) were used last time. A miss builds a lazy shell; the
+ * memo is written when the shell materializes. Lock-free: a race only reclones.
+ */
+ public IRClosure refinementsClone(ThreadContext context, RubyModule[] modules) {
+ Map cacheMap = getManager().getRefinementsCloneCache();
+ RefinementsCache cache = cacheMap.get(this);
+ if (cache != null && cache.matches(modules)) {
+ // Proc#ruby2_keywords marks the scope, possibly after the clone was memoized; a stale
+ // clone must be rebuilt, not reused (as in CRuby).
+ if (cache.clone.isRuby2Keywords() == isRuby2Keywords()) return cache.clone;
+ context.runtime.getWarnings().warnPerformance(
+ "Proc#refined re-copies the block because the ruby2_keywords flag changed after the copy was memoized");
+ }
+
+ IRRefinedClosure clone = cloneForRefinements(context, modules);
+ clone.setRefinementsRecipe(this, modules);
+ // Build the BlockBody before the clone escapes so memo sharers never race in getBlockBody().
+ clone.getBlockBody();
+
+ return clone;
+ }
+
+ /**
+ * Produce the refinement-aware copy for Proc#refined: an {@link IRRefinedClosure} whose own StaticScope
+ * has the modules' refinements activated and whose instructions are cloned lazily on first use.
+ */
+ private IRRefinedClosure cloneForRefinements(ThreadContext context, RubyModule[] modules) {
+ // Cloning reads the startup InterpreterContext as its instruction source.
+ if (getInterpreterContext() == null) {
+ throw context.runtime.newArgumentError("cannot create a refinements-aware copy of this proc");
+ }
+
+ SimpleCloneInfo ii = new SimpleCloneInfo(getLexicalParent(), false, false, true, true);
+ IRRefinedClosure clone = (IRRefinedClosure) cloneForInlining(ii);
+ clone.setArgumentDescriptors(getArgumentDescriptors());
+
+ // The constructor registered the clone in its lexical parent's child-scope list; detach the grafted
+ // top clone so the shared parent does not retain it.
+ clone.getLexicalParent().removeChildScope(clone);
+
+ // Activate the requested refinements on the clone's own overlay module so refined call sites resolve them.
+ RubyModule overlay = clone.getStaticScope().getOverlayModuleForWrite(context);
+ for (RubyModule module : modules) {
+ RubyModule.usingModule(context, overlay, module);
+ }
+
+ return clone;
+ }
+
public Signature getSignature() {
return signature;
}
diff --git a/core/src/main/java/org/jruby/ir/IRManager.java b/core/src/main/java/org/jruby/ir/IRManager.java
index 3469fe358a2..50f9d0b1b38 100644
--- a/core/src/main/java/org/jruby/ir/IRManager.java
+++ b/core/src/main/java/org/jruby/ir/IRManager.java
@@ -31,9 +31,12 @@
import org.jruby.ir.passes.CompilerPassListener;
import org.jruby.ir.passes.CompilerPassScheduler;
+import java.util.Collections;
import java.util.HashSet;
+import java.util.IdentityHashMap;
import java.util.Iterator;
import java.util.List;
+import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicLong;
@@ -98,6 +101,11 @@ public class IRManager {
private IRBuilderFactory builderFactory;
private AtomicLong callSiteCounter = new AtomicLong(1);
+ // Proc#refined cache: source closure -> its most recent refinements clone (see IRClosure#refinementsClone)
+ // Entries live for the runtime's lifetime; weak keys would never clear because the value reaches its key.
+ private final Map refinementsCloneCache =
+ Collections.synchronizedMap(new IdentityHashMap<>());
+
public IRManager(Ruby runtime, RubyInstanceConfig config) {
this.runtime = runtime;
this.config = config;
@@ -122,6 +130,10 @@ public Ruby getRuntime() {
return runtime;
}
+ Map getRefinementsCloneCache() {
+ return refinementsCloneCache;
+ }
+
public Nil getNil() {
return nil;
}
diff --git a/core/src/main/java/org/jruby/ir/IRMetaClassBody.java b/core/src/main/java/org/jruby/ir/IRMetaClassBody.java
index 61d1fe70ff1..c101a599d68 100644
--- a/core/src/main/java/org/jruby/ir/IRMetaClassBody.java
+++ b/core/src/main/java/org/jruby/ir/IRMetaClassBody.java
@@ -9,6 +9,11 @@ public IRMetaClassBody(IRManager manager, IRScope lexicalParent, ByteList name,
super(manager, lexicalParent, name, lineNumber, scope, false);
}
+ @Override
+ protected IRModuleBody cloneInstance(IRScope lexicalParent, StaticScope scope) {
+ return new IRMetaClassBody(getManager(), lexicalParent, getByteName(), getLine(), scope);
+ }
+
@Override
public IRScopeType getScopeType() {
return IRScopeType.METACLASS_BODY;
diff --git a/core/src/main/java/org/jruby/ir/IRMethod.java b/core/src/main/java/org/jruby/ir/IRMethod.java
index 8815cb664ab..7dbb7f33e2f 100644
--- a/core/src/main/java/org/jruby/ir/IRMethod.java
+++ b/core/src/main/java/org/jruby/ir/IRMethod.java
@@ -40,6 +40,7 @@
import org.jruby.ir.operands.Label;
import org.jruby.ir.operands.LocalVariable;
import org.jruby.ir.representations.BasicBlock;
+import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.ArgumentDescriptor;
import org.jruby.runtime.CallType;
@@ -80,6 +81,23 @@ public boolean hasBeenBuilt() {
return defNode == null;
}
+ /**
+ * Deep-copy this method for a Proc#refined clone: a {@code def} inside a refined proc must capture that
+ * proc's refinements (like a {@code def} inside a {@code using} scope), so the defined method keeps
+ * resolving the refined methods. For ordinary inlining the shared original is returned unchanged.
+ */
+ public IRMethod cloneForInlining(CloneInfo ii) {
+ if (!ii.isRefinementsClone()) return this;
+
+ // Cloning copies already-built instructions, so make sure ours are built first.
+ return cloneScopeForRefinements(ii.getScope(), lazilyAcquireInterpreterContext(), (parent, scope) -> {
+ IRMethod clone = new IRMethod(getManager(), parent, null, getByteName(), isInstanceMethod, getLine(),
+ scope, getCoverageMode());
+ clone.setArgumentDescriptors(getArgumentDescriptors());
+ return clone;
+ });
+ }
+
public MethodData getMethodData() {
List ivarNames;
diff --git a/core/src/main/java/org/jruby/ir/IRModuleBody.java b/core/src/main/java/org/jruby/ir/IRModuleBody.java
index 5ba4702fc0e..ac81b4265cd 100644
--- a/core/src/main/java/org/jruby/ir/IRModuleBody.java
+++ b/core/src/main/java/org/jruby/ir/IRModuleBody.java
@@ -1,5 +1,6 @@
package org.jruby.ir;
+import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.util.ByteList;
@@ -17,6 +18,23 @@ public IRModuleBody(IRManager manager, IRScope lexicalParent, ByteList name, int
}
}
+ /**
+ * Deep-copy this module/class body for a Proc#refined clone: a {@code class}/{@code module} (re)opened
+ * inside a refined proc must see that proc's refinements, in the body and in any {@code def} it contains.
+ * For ordinary inlining the shared original is returned unchanged.
+ */
+ public IRModuleBody cloneForInlining(CloneInfo ii) {
+ if (!ii.isRefinementsClone()) return this;
+
+ // Class/module bodies are built eagerly when their enclosing scope is built, so the IC is available here.
+ return cloneScopeForRefinements(ii.getScope(), builtInterpreterContext(), this::cloneInstance);
+ }
+
+ /** Construct an empty copy of this body of the correct concrete type for a refinements clone. */
+ protected IRModuleBody cloneInstance(IRScope lexicalParent, StaticScope scope) {
+ return new IRModuleBody(getManager(), lexicalParent, getByteName(), getLine(), scope, executesOnce);
+ }
+
@Override
public int getNearestModuleReferencingScopeDepth() {
return 0;
diff --git a/core/src/main/java/org/jruby/ir/IRRefinedClosure.java b/core/src/main/java/org/jruby/ir/IRRefinedClosure.java
new file mode 100644
index 00000000000..d7efe0129aa
--- /dev/null
+++ b/core/src/main/java/org/jruby/ir/IRRefinedClosure.java
@@ -0,0 +1,75 @@
+package org.jruby.ir;
+
+import java.util.Map;
+
+import org.jruby.RubyModule;
+import org.jruby.parser.StaticScope;
+import org.jruby.util.ByteList;
+
+/**
+ * The root of a Proc#refined clone tree -- the closure backing the returned proc. Carries the recipe
+ * (source closure + accumulated modules) so a chained call re-clones from the original with the merged
+ * list; the nested scopes it encloses remain ordinary IRClosure/IRFor/IRMethod clones.
+ */
+public class IRRefinedClosure extends IRClosure {
+ private IRClosure refinementsSource;
+ private RubyModule[] refinementsModules;
+
+ IRRefinedClosure(IRClosure c, IRScope lexicalParent, int closureId, ByteList fullName, StaticScope staticScope) {
+ super(c, lexicalParent, closureId, fullName, staticScope);
+ }
+
+ @Override
+ public boolean isRefinementsClone() {
+ return true;
+ }
+
+ /** Set before the proc can escape, so materialization cannot precede it. */
+ void setRefinementsRecipe(IRClosure source, RubyModule[] modules) {
+ this.refinementsSource = source;
+ this.refinementsModules = modules;
+ }
+
+ public IRClosure getRefinementsSource() {
+ return refinementsSource;
+ }
+
+ public RubyModule[] getRefinementsModules() {
+ return refinementsModules;
+ }
+
+ // Module identity, not equals/hashCode: those dispatch to Ruby code, which user modules can override.
+ public boolean sameRecipe(IRRefinedClosure other) {
+ if (refinementsSource == null || refinementsSource != other.refinementsSource) return false;
+ RubyModule[] modules = refinementsModules, otherModules = other.refinementsModules;
+ if (modules.length != otherModules.length) return false;
+ for (int i = 0; i < modules.length; i++) {
+ if (modules[i] != otherModules[i]) return false;
+ }
+ return true;
+ }
+
+ public int recipeHash() {
+ if (refinementsModules == null) return System.identityHashCode(this);
+ int hash = System.identityHashCode(refinementsSource);
+ for (RubyModule module : refinementsModules) {
+ hash = 31 * hash + System.identityHashCode(module);
+ }
+ return hash;
+ }
+
+ /**
+ * Deferring the memo write to materialization means a never-called chain intermediate cannot evict
+ * a live entry.
+ */
+ @Override
+ protected void publishRefinementsClone() {
+ Map cacheMap = getManager().getRefinementsCloneCache();
+ RefinementsCache cache = cacheMap.get(refinementsSource);
+ if (cache != null && !cache.matches(refinementsModules)) {
+ getManager().getRuntime().getWarnings().warnPerformance(
+ "Proc#refined called with different modules for the same block disables memoization");
+ }
+ cacheMap.put(refinementsSource, new RefinementsCache(refinementsModules, this));
+ }
+}
diff --git a/core/src/main/java/org/jruby/ir/IRScope.java b/core/src/main/java/org/jruby/ir/IRScope.java
index 7372e50f73e..59184ad057d 100644
--- a/core/src/main/java/org/jruby/ir/IRScope.java
+++ b/core/src/main/java/org/jruby/ir/IRScope.java
@@ -29,6 +29,7 @@
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
+import java.util.function.BiFunction;
import java.util.function.Supplier;
import org.jruby.runtime.DynamicScope;
@@ -150,12 +151,15 @@ public DynamicScope getDynamicScope() {
private boolean needsCodeCoverage;
private boolean usesEval;
- // Used by cloning code for inlining
- protected IRScope(IRScope s, IRScope lexicalParent) {
+ /**
+ * Copy constructor used by cloning code (inlining shares the source's StaticScope; Proc#refined cloning
+ * supplies a fresh one so a refinements overlay can be attached without mutating the original).
+ */
+ protected IRScope(IRScope s, IRScope lexicalParent, StaticScope staticScope) {
this.lexicalParent = lexicalParent;
this.manager = s.manager;
this.lineNumber = s.lineNumber;
- this.staticScope = s.staticScope;
+ this.staticScope = staticScope;
this.nextClosureIndex = s.nextClosureIndex;
this.interpreterContext = null;
this.coverageMode = CoverageData.NONE;
@@ -163,6 +167,7 @@ protected IRScope(IRScope s, IRScope lexicalParent) {
this.endLine = s.endLine;
this.endColumn = s.endColumn;
this.localVars = new HashMap<>(s.localVars);
+ this.ruby2Keywords = s.ruby2Keywords;
this.scopeId = globalScopeCount.getAndIncrement();
setupLexicalContainment();
@@ -224,6 +229,10 @@ protected synchronized void addChildScope(IRScope scope) {
lexicalChildren.add(scope);
}
+ protected synchronized void removeChildScope(IRScope scope) {
+ lexicalChildren.remove(scope);
+ }
+
public synchronized List getLexicalScopes() {
return lexicalChildren;
}
@@ -651,6 +660,62 @@ public InterpreterContext allocateInterpreterContext(Supplier> instr
return interpreterContext;
}
+ /**
+ * Shared body of the IRMethod/IRModuleBody Proc#refined clone: give the copy its own StaticScope reaching
+ * the enclosing clone's refinements overlay, flag it maybe-using-refinements, and re-create its instructions
+ * lazily on first use. The caller supplies the source InterpreterContext and a constructor for the empty copy.
+ */
+ protected T cloneScopeForRefinements(IRScope lexicalParent, InterpreterContext sourceIC,
+ BiFunction constructor) {
+ StaticScope refinementsScope = getStaticScope().duplicate();
+ refinementsScope.setEnclosingScope(lexicalParent.getStaticScope());
+
+ T clone = constructor.apply(lexicalParent, refinementsScope);
+ refinementsScope.setIRScope(clone);
+ clone.setIsMaybeUsingRefinements();
+
+ SimpleCloneInfo clonedII = new SimpleCloneInfo(clone, false, false, true);
+ clone.allocateInterpreterContext(new RefinementsCloneInstrs(clone, sourceIC, clonedII),
+ sourceIC.getTemporaryVariableCount(), sourceIC.getFlags());
+
+ return clone;
+ }
+
+ /**
+ * Copies a Proc#refined clone's instructions on first use. Idempotent: InterpreterContext#getEngine can
+ * force it from two threads, and a duplicate copy would re-register nested shells in their parents.
+ */
+ protected static final class RefinementsCloneInstrs implements Supplier> {
+ private final IRScope clone;
+ private final InterpreterContext sourceIC;
+ private final SimpleCloneInfo clonedII;
+ private List instrs;
+
+ RefinementsCloneInstrs(IRScope clone, InterpreterContext sourceIC, SimpleCloneInfo clonedII) {
+ this.clone = clone;
+ this.sourceIC = sourceIC;
+ this.clonedII = clonedII;
+ }
+
+ @Override
+ public synchronized List get() {
+ if (instrs == null) {
+ Instr[] source = sourceIC.getInstructions();
+ List newInstrs = new ArrayList<>(source.length);
+ for (Instr i : source) {
+ newInstrs.add(i.clone(clonedII));
+ }
+ instrs = newInstrs;
+ clone.publishRefinementsClone();
+ }
+ return instrs;
+ }
+ }
+
+ /** Hook: an IRRefinedClosure publishes itself to the memo when it materializes. */
+ protected void publishRefinementsClone() {
+ }
+
private Instr[] cloneInstrs() {
SimpleCloneInfo cloneInfo = new SimpleCloneInfo(this, false);
diff --git a/core/src/main/java/org/jruby/ir/instructions/ArrayDerefInstr.java b/core/src/main/java/org/jruby/ir/instructions/ArrayDerefInstr.java
index 14f08aabf78..debbe8f71ee 100644
--- a/core/src/main/java/org/jruby/ir/instructions/ArrayDerefInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/ArrayDerefInstr.java
@@ -45,6 +45,11 @@ public ArrayDerefInstr(IRScope scope, Variable result, Operand obj, FrozenString
@Override
public Instr clone(CloneInfo ii) {
+ // The optimized aref path bypasses refinements (dispatches through CachingCallSite.isBuiltin), so a
+ // clone into a refined scope falls back to a generic refined call, like the fixnum/float fast paths.
+ if (isPotentiallyRefined() || ii.getScope().maybeUsingRefinements()) {
+ return super.clone(ii);
+ }
return new ArrayDerefInstr(ii.getScope(), (Variable) getResult().cloneForInlining(ii),
getReceiver().cloneForInlining(ii), key, getFlags());
}
diff --git a/core/src/main/java/org/jruby/ir/instructions/CallInstr.java b/core/src/main/java/org/jruby/ir/instructions/CallInstr.java
index c9ee83169d6..75bae1149df 100644
--- a/core/src/main/java/org/jruby/ir/instructions/CallInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/CallInstr.java
@@ -27,7 +27,14 @@ public class CallInstr extends CallBase implements ResultInstr {
public static CallInstr create(IRScope scope, CallType callType, Variable result, RubySymbol name, Operand receiver,
Operand[] args, Operand closure, int flags) {
- boolean isPotentiallyRefined = scope.maybeUsingRefinements();
+ return create(scope, callType, result, name, receiver, args, closure, flags, false);
+ }
+
+ // clone() passes the source instruction's flag so a refined call stays refined even
+ // when it is cloned into a scope without the refinement flag.
+ private static CallInstr create(IRScope scope, CallType callType, Variable result, RubySymbol name, Operand receiver,
+ Operand[] args, Operand closure, int flags, boolean potentiallyRefined) {
+ boolean isPotentiallyRefined = potentiallyRefined || scope.maybeUsingRefinements();
if (!containsArgSplat(args)) {
boolean hasClosure = closure != NullBlock.INSTANCE;
@@ -121,9 +128,11 @@ public Instr discardResult() {
return NoResultCallInstr.create(getCallType(), getName(), getReceiver(), getCallArgs(), getClosureArg(), isPotentiallyRefined());
}*/
+ // Shared by the specialized subclasses: create() re-picks the specialization from the cloned operand
+ // shapes and the effective refinement flag, so a refinements clone drops the fixnum/float fast paths.
@Override
public Instr clone(CloneInfo ii) {
- return new CallInstr(ii.getScope(), getOperation(), getCallType(), ii.getRenamedVariable(result), getName(),
+ return create(ii.getScope(), getCallType(), ii.getRenamedVariable(result), getName(),
getReceiver().cloneForInlining(ii), cloneCallArgs(ii),
getClosureArg().cloneForInlining(ii),
getFlags(), isPotentiallyRefined()
diff --git a/core/src/main/java/org/jruby/ir/instructions/DefineClassInstr.java b/core/src/main/java/org/jruby/ir/instructions/DefineClassInstr.java
index a6a73be79eb..e83d4d7b72a 100644
--- a/core/src/main/java/org/jruby/ir/instructions/DefineClassInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/DefineClassInstr.java
@@ -42,7 +42,7 @@ public Operand getSuperClass() {
@Override
public Instr clone(CloneInfo ii) {
- return new DefineClassInstr(ii.getRenamedVariable(result), body,
+ return new DefineClassInstr(ii.getRenamedVariable(result), (IRClassBody) body.cloneForInlining(ii),
getContainer().cloneForInlining(ii), getSuperClass().cloneForInlining(ii));
}
diff --git a/core/src/main/java/org/jruby/ir/instructions/DefineClassMethodInstr.java b/core/src/main/java/org/jruby/ir/instructions/DefineClassMethodInstr.java
index c8076c47757..a2c054aeca2 100644
--- a/core/src/main/java/org/jruby/ir/instructions/DefineClassMethodInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/DefineClassMethodInstr.java
@@ -38,7 +38,7 @@ public String[] toStringNonOperandArgs() {
@Override
public Instr clone(CloneInfo ii) {
- return new DefineClassMethodInstr(getContainer().cloneForInlining(ii), method);
+ return new DefineClassMethodInstr(getContainer().cloneForInlining(ii), method.cloneForInlining(ii));
}
@Override
diff --git a/core/src/main/java/org/jruby/ir/instructions/DefineInstanceMethodInstr.java b/core/src/main/java/org/jruby/ir/instructions/DefineInstanceMethodInstr.java
index cd0377c59aa..1bb46be818c 100644
--- a/core/src/main/java/org/jruby/ir/instructions/DefineInstanceMethodInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/DefineInstanceMethodInstr.java
@@ -40,7 +40,7 @@ public IRMethod getMethod() {
@Override
public Instr clone(CloneInfo ii) {
- return new DefineInstanceMethodInstr(method);
+ return new DefineInstanceMethodInstr(method.cloneForInlining(ii));
}
@Override
diff --git a/core/src/main/java/org/jruby/ir/instructions/DefineMetaClassInstr.java b/core/src/main/java/org/jruby/ir/instructions/DefineMetaClassInstr.java
index cff759e58f4..c8d14c8ee16 100644
--- a/core/src/main/java/org/jruby/ir/instructions/DefineMetaClassInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/DefineMetaClassInstr.java
@@ -53,7 +53,8 @@ public String[] toStringNonOperandArgs() {
@Override
public Instr clone(CloneInfo ii) {
- return new DefineMetaClassInstr(ii.getRenamedVariable(result), getObject().cloneForInlining(ii), metaClassBody);
+ return new DefineMetaClassInstr(ii.getRenamedVariable(result), getObject().cloneForInlining(ii),
+ metaClassBody.cloneForInlining(ii));
}
@Override
diff --git a/core/src/main/java/org/jruby/ir/instructions/DefineModuleInstr.java b/core/src/main/java/org/jruby/ir/instructions/DefineModuleInstr.java
index a6a2e9c9691..33f3cf49336 100644
--- a/core/src/main/java/org/jruby/ir/instructions/DefineModuleInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/DefineModuleInstr.java
@@ -43,7 +43,8 @@ public String[] toStringNonOperandArgs() {
@Override
public Instr clone(CloneInfo ii) {
- return new DefineModuleInstr(ii.getRenamedVariable(result), getContainer().cloneForInlining(ii), body);
+ return new DefineModuleInstr(ii.getRenamedVariable(result), getContainer().cloneForInlining(ii),
+ body.cloneForInlining(ii));
}
@Override
diff --git a/core/src/main/java/org/jruby/ir/instructions/specialized/OneFixnumArgNoBlockCallInstr.java b/core/src/main/java/org/jruby/ir/instructions/specialized/OneFixnumArgNoBlockCallInstr.java
index f14f7fa74a7..d1a0dcdc3a7 100644
--- a/core/src/main/java/org/jruby/ir/instructions/specialized/OneFixnumArgNoBlockCallInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/specialized/OneFixnumArgNoBlockCallInstr.java
@@ -4,13 +4,11 @@
import org.jruby.ir.IRScope;
import org.jruby.ir.Operation;
import org.jruby.ir.instructions.CallInstr;
-import org.jruby.ir.instructions.Instr;
import org.jruby.ir.operands.Fixnum;
import org.jruby.ir.operands.NullBlock;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.runtime.IRRuntimeHelpers;
-import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.CallType;
import org.jruby.runtime.DynamicScope;
@@ -30,12 +28,6 @@ public OneFixnumArgNoBlockCallInstr(IRScope scope, CallType callType, Variable r
this.fixNum = ((Fixnum) args[0]).value;
}
- @Override
- public Instr clone(CloneInfo ii) {
- return new OneFixnumArgNoBlockCallInstr(ii.getScope(), getCallType(), ii.getRenamedVariable(result), getName(),
- getReceiver().cloneForInlining(ii), cloneCallArgs(ii), getFlags(), isPotentiallyRefined());
- }
-
public long getFixnumArg() {
return fixNum;
}
diff --git a/core/src/main/java/org/jruby/ir/instructions/specialized/OneFloatArgNoBlockCallInstr.java b/core/src/main/java/org/jruby/ir/instructions/specialized/OneFloatArgNoBlockCallInstr.java
index 7b37b1122c8..93a0767a686 100644
--- a/core/src/main/java/org/jruby/ir/instructions/specialized/OneFloatArgNoBlockCallInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/specialized/OneFloatArgNoBlockCallInstr.java
@@ -4,13 +4,11 @@
import org.jruby.ir.IRScope;
import org.jruby.ir.Operation;
import org.jruby.ir.instructions.CallInstr;
-import org.jruby.ir.instructions.Instr;
import org.jruby.ir.operands.Float;
import org.jruby.ir.operands.NullBlock;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.runtime.IRRuntimeHelpers;
-import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.CallType;
import org.jruby.runtime.DynamicScope;
@@ -30,13 +28,6 @@ public OneFloatArgNoBlockCallInstr(IRScope scope, CallType callType, Variable re
this.flote = ((Float) args[0]).value;
}
- @Override
- public Instr clone(CloneInfo ii) {
- return new OneFloatArgNoBlockCallInstr(ii.getScope(), getCallType(), ii.getRenamedVariable(result), getName(),
- getReceiver().cloneForInlining(ii), cloneCallArgs(ii), getFlags(), isPotentiallyRefined()
- );
- }
-
public double getFloatArg() {
return flote;
}
diff --git a/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgBlockCallInstr.java b/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgBlockCallInstr.java
index 9994dbc084e..3f13befef07 100644
--- a/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgBlockCallInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgBlockCallInstr.java
@@ -4,11 +4,9 @@
import org.jruby.ir.IRScope;
import org.jruby.ir.Operation;
import org.jruby.ir.instructions.CallInstr;
-import org.jruby.ir.instructions.Instr;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.runtime.IRRuntimeHelpers;
-import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallType;
@@ -24,14 +22,6 @@ public OneOperandArgBlockCallInstr(IRScope scope, CallType callType, Variable re
super(scope, Operation.CALL_1OB, callType, result, name, receiver, args, closure, flags, isPotentiallyRefined);
}
- @Override
- public Instr clone(CloneInfo ii) {
- return new OneOperandArgBlockCallInstr(ii.getScope(), getCallType(), ii.getRenamedVariable(result), getName(),
- getReceiver().cloneForInlining(ii), cloneCallArgs(ii),
- getClosureArg().cloneForInlining(ii), getFlags(), isPotentiallyRefined()
- );
- }
-
@Override
public Object interpret(ThreadContext context, StaticScope currScope, DynamicScope dynamicScope, IRubyObject self, Object[] temp) {
// NOTE: This logic shouod always match the CALL_10B logic in InterpreterEngine.processCall
diff --git a/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgNoBlockCallInstr.java b/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgNoBlockCallInstr.java
index 4b960bfe514..1789b97fef9 100644
--- a/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgNoBlockCallInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/specialized/OneOperandArgNoBlockCallInstr.java
@@ -4,12 +4,10 @@
import org.jruby.ir.IRScope;
import org.jruby.ir.Operation;
import org.jruby.ir.instructions.CallInstr;
-import org.jruby.ir.instructions.Instr;
import org.jruby.ir.operands.NullBlock;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.runtime.IRRuntimeHelpers;
-import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.CallType;
import org.jruby.runtime.DynamicScope;
@@ -30,13 +28,6 @@ public OneOperandArgNoBlockCallInstr(IRScope scope, Operation op, CallType callT
super(scope, op, callType, result, name, receiver, args, NullBlock.INSTANCE, flags, isPotentiallyRefined);
}
- @Override
- public Instr clone(CloneInfo ii) {
- return new OneOperandArgNoBlockCallInstr(ii.getScope(), Operation.CALL_1O, getCallType(),
- ii.getRenamedVariable(result), getName(), getReceiver().cloneForInlining(ii), cloneCallArgs(ii),
- getFlags(), isPotentiallyRefined());
- }
-
@Override
public Object interpret(ThreadContext context, StaticScope currScope, DynamicScope dynamicScope, IRubyObject self, Object[] temp) {
IRubyObject object = (IRubyObject) getReceiver().retrieve(context, self, currScope, dynamicScope, temp);
diff --git a/core/src/main/java/org/jruby/ir/instructions/specialized/TwoOperandArgNoBlockCallInstr.java b/core/src/main/java/org/jruby/ir/instructions/specialized/TwoOperandArgNoBlockCallInstr.java
index c02a067f27a..2d72cf597d4 100644
--- a/core/src/main/java/org/jruby/ir/instructions/specialized/TwoOperandArgNoBlockCallInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/specialized/TwoOperandArgNoBlockCallInstr.java
@@ -4,12 +4,10 @@
import org.jruby.ir.IRScope;
import org.jruby.ir.Operation;
import org.jruby.ir.instructions.CallInstr;
-import org.jruby.ir.instructions.Instr;
import org.jruby.ir.operands.NullBlock;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.runtime.IRRuntimeHelpers;
-import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.CallType;
import org.jruby.runtime.DynamicScope;
@@ -23,13 +21,6 @@ public TwoOperandArgNoBlockCallInstr(IRScope scope, CallType callType, Variable
super(scope, Operation.CALL_2O, callType, result, name, receiver, args, NullBlock.INSTANCE, flags, isPotentiallyRefined);
}
- @Override
- public Instr clone(CloneInfo ii) {
- return new TwoOperandArgNoBlockCallInstr(ii.getScope(), getCallType(), ii.getRenamedVariable(result), getName(),
- getReceiver().cloneForInlining(ii), cloneCallArgs(ii), getFlags(), isPotentiallyRefined()
- );
- }
-
public Operand getArg2() {
return operands[2];
}
diff --git a/core/src/main/java/org/jruby/ir/instructions/specialized/ZeroOperandArgNoBlockCallInstr.java b/core/src/main/java/org/jruby/ir/instructions/specialized/ZeroOperandArgNoBlockCallInstr.java
index 7f370265bb6..e8cc45f7dd4 100644
--- a/core/src/main/java/org/jruby/ir/instructions/specialized/ZeroOperandArgNoBlockCallInstr.java
+++ b/core/src/main/java/org/jruby/ir/instructions/specialized/ZeroOperandArgNoBlockCallInstr.java
@@ -4,12 +4,10 @@
import org.jruby.ir.IRScope;
import org.jruby.ir.Operation;
import org.jruby.ir.instructions.CallInstr;
-import org.jruby.ir.instructions.Instr;
import org.jruby.ir.operands.NullBlock;
import org.jruby.ir.operands.Operand;
import org.jruby.ir.operands.Variable;
import org.jruby.ir.runtime.IRRuntimeHelpers;
-import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.parser.StaticScope;
import org.jruby.runtime.CallType;
import org.jruby.runtime.DynamicScope;
@@ -30,12 +28,6 @@ public ZeroOperandArgNoBlockCallInstr(IRScope scope, CallType callType, Variable
super(scope, Operation.CALL_0O, callType, result, name, receiver, args, NullBlock.INSTANCE, flags, isPotentiallyRefined);
}
- @Override
- public Instr clone(CloneInfo ii) {
- return new ZeroOperandArgNoBlockCallInstr(ii.getScope(), getOperation(), getCallType(), ii.getRenamedVariable(result), getName(),
- getReceiver().cloneForInlining(ii), cloneCallArgs(ii), getFlags(), isPotentiallyRefined());
- }
-
@Override
public Object interpret(ThreadContext context, StaticScope currScope, DynamicScope dynamicScope, IRubyObject self, Object[] temp) {
IRubyObject object = (IRubyObject) getReceiver().retrieve(context, self, currScope, dynamicScope, temp);
diff --git a/core/src/main/java/org/jruby/ir/operands/SymbolProc.java b/core/src/main/java/org/jruby/ir/operands/SymbolProc.java
index 99685148bae..e90e8ec8831 100644
--- a/core/src/main/java/org/jruby/ir/operands/SymbolProc.java
+++ b/core/src/main/java/org/jruby/ir/operands/SymbolProc.java
@@ -5,6 +5,7 @@
import org.jruby.ir.persistence.IRReaderDecoder;
import org.jruby.ir.persistence.IRWriterEncoder;
import org.jruby.ir.runtime.IRRuntimeHelpers;
+import org.jruby.ir.transformations.inlining.CloneInfo;
import org.jruby.runtime.ThreadContext;
/**
@@ -31,6 +32,15 @@ public Object createCacheObject(ThreadContext context) {
return IRRuntimeHelpers.newSymbolProc(context, getName());
}
+ @Override
+ public Operand cloneForInlining(CloneInfo ii) {
+ // A baked &:sym caches a *non-refined* symbol proc, so it would ignore the refinements activated on a
+ // Proc#refined clone.
+ if (ii.isRefinementsClone()) return new Symbol(name);
+
+ return this;
+ }
+
@Override
public int hashCode() {
return 31 * super.hashCode() + name.hashCode();
diff --git a/core/src/main/java/org/jruby/ir/operands/WrappedIRClosure.java b/core/src/main/java/org/jruby/ir/operands/WrappedIRClosure.java
index b3f9374047c..4c44ec3f2f1 100644
--- a/core/src/main/java/org/jruby/ir/operands/WrappedIRClosure.java
+++ b/core/src/main/java/org/jruby/ir/operands/WrappedIRClosure.java
@@ -61,6 +61,11 @@ public Operand getSimplifiedOperand(Map valueMap, boolean forc
@Override
public Operand cloneForInlining(CloneInfo info) {
+ // For Proc#refined we must deep-clone nested closures
+ if (info.isRefinementsClone()) {
+ return new WrappedIRClosure(info.getRenamedVariable(self), closure.cloneForInlining(info));
+ }
+
// Making interp instrs so that if JIT hits IRClosure we will not concurrently modify the same IRScope.
if (info instanceof SimpleCloneInfo && !((SimpleCloneInfo) info).isEnsureBlockCloneMode()) {
// FIXME: It really bothers me we do not clone closure here but cloning like main clone case loses interpContext + other things.
diff --git a/core/src/main/java/org/jruby/ir/transformations/inlining/CloneInfo.java b/core/src/main/java/org/jruby/ir/transformations/inlining/CloneInfo.java
index 62dda170f49..e4a528a8c74 100644
--- a/core/src/main/java/org/jruby/ir/transformations/inlining/CloneInfo.java
+++ b/core/src/main/java/org/jruby/ir/transformations/inlining/CloneInfo.java
@@ -26,13 +26,32 @@ protected CloneInfo(IRScope scope) {
public SimpleCloneInfo cloneForCloningClosure(IRClosure clonedClosure) {
// If cloning for ensure block cloning we want to propagate that to child closure clones
boolean ensureClone = this instanceof SimpleCloneInfo && ((SimpleCloneInfo) this).isEnsureBlockCloneMode();
- SimpleCloneInfo clone = new SimpleCloneInfo(clonedClosure, ensureClone);
+ // If cloning for Proc#refined we must propagate that so nested closures are also made refinement-aware
+ SimpleCloneInfo clone = new SimpleCloneInfo(clonedClosure, ensureClone, false, isRefinementsClone());
clone.variableRenameMap.putAll(variableRenameMap);
return clone;
}
+ /**
+ * Is this clone being performed to produce a refinement-aware copy (Proc#refined)? When true, call-like
+ * instructions are re-created as refined call sites bound to the clone's static scope.
+ *
+ * @return true if this is a refinements clone
+ */
+ public boolean isRefinementsClone() {
+ return false;
+ }
+
+ /**
+ * Is this the root of a refinements clone request -- the closure Proc#refined was called on, which
+ * becomes an IRRefinedClosure -- rather than a nested scope cloned along with it?
+ */
+ public boolean isRefinementsCloneRoot() {
+ return false;
+ }
+
/**
* @return The IRScope this cloning operation is happening in (or is coming from).
*/
diff --git a/core/src/main/java/org/jruby/ir/transformations/inlining/SimpleCloneInfo.java b/core/src/main/java/org/jruby/ir/transformations/inlining/SimpleCloneInfo.java
index e20ecc4a379..5f1982fb514 100644
--- a/core/src/main/java/org/jruby/ir/transformations/inlining/SimpleCloneInfo.java
+++ b/core/src/main/java/org/jruby/ir/transformations/inlining/SimpleCloneInfo.java
@@ -10,22 +10,46 @@
*/
public class SimpleCloneInfo extends CloneInfo {
private final boolean isEnsureBlock;
+ private final boolean refinementsClone;
+ private final boolean refinementsCloneRoot;
private boolean cloneIPC;
- public SimpleCloneInfo(IRScope scope, boolean isEnsureBlock, boolean cloneIPC) {
+ public SimpleCloneInfo(IRScope scope, boolean isEnsureBlock, boolean cloneIPC, boolean refinementsClone,
+ boolean refinementsCloneRoot) {
super(scope);
this.isEnsureBlock = isEnsureBlock;
+ this.cloneIPC = cloneIPC;
+ this.refinementsClone = refinementsClone;
+ this.refinementsCloneRoot = refinementsCloneRoot;
+ }
+
+ public SimpleCloneInfo(IRScope scope, boolean isEnsureBlock, boolean cloneIPC, boolean refinementsClone) {
+ this(scope, isEnsureBlock, cloneIPC, refinementsClone, false);
+ }
+
+ public SimpleCloneInfo(IRScope scope, boolean isEnsureBlock, boolean cloneIPC) {
+ this(scope, isEnsureBlock, cloneIPC, false);
}
public SimpleCloneInfo(IRScope scope, boolean isEnsureBlock) {
- this(scope, isEnsureBlock, false);
+ this(scope, isEnsureBlock, false, false);
}
public boolean isEnsureBlockCloneMode() {
return this.isEnsureBlock;
}
+ @Override
+ public boolean isRefinementsClone() {
+ return this.refinementsClone;
+ }
+
+ @Override
+ public boolean isRefinementsCloneRoot() {
+ return this.refinementsCloneRoot;
+ }
+
public boolean shouldCloneIPC() {
return cloneIPC;
}
diff --git a/core/src/main/java/org/jruby/parser/StaticScope.java b/core/src/main/java/org/jruby/parser/StaticScope.java
index bfd52b61161..70054784b0e 100644
--- a/core/src/main/java/org/jruby/parser/StaticScope.java
+++ b/core/src/main/java/org/jruby/parser/StaticScope.java
@@ -56,6 +56,7 @@
import org.jruby.ast.LocalVarNode;
import org.jruby.ast.Node;
import org.jruby.ast.VCallNode;
+import org.jruby.ir.IRClosure;
import org.jruby.ir.IRMethod;
import org.jruby.ir.IRScope;
import org.jruby.ir.IRScopeType;
@@ -875,6 +876,21 @@ public boolean isWithinMethod() {
return false;
}
+ /**
+ * Determine if we are lexically within the body of a Proc#refined proc (its refinements clone tree).
+ * `using` is rejected there: it would diverge the refinement set shared by the memoized clone. Walk the
+ * enclosing-scope chain, since nested bodies/blocks/evals reach the clone through it.
+ * @return true if so
+ */
+ public boolean isWithinRefinementsProc() {
+ for (StaticScope current = this; current != null; current = current.getEnclosingScope()) {
+ IRScope irScope = current.getIRScope();
+ if (irScope instanceof IRClosure && ((IRClosure) irScope).isInRefinementsCloneTree()) return true;
+ }
+
+ return false;
+ }
+
@Override
public String toString() {
// FIXME: Do we need to persist cref as well?
diff --git a/core/src/main/java/org/jruby/runtime/IRBlockBody.java b/core/src/main/java/org/jruby/runtime/IRBlockBody.java
index 7986996294e..3f823c19fcf 100644
--- a/core/src/main/java/org/jruby/runtime/IRBlockBody.java
+++ b/core/src/main/java/org/jruby/runtime/IRBlockBody.java
@@ -2,6 +2,7 @@
import org.jruby.RubyArray;
import org.jruby.ir.IRClosure;
+import org.jruby.ir.IRRefinedClosure;
import org.jruby.ir.IRScope;
import org.jruby.ir.interpreter.InterpreterContext;
import org.jruby.ir.runtime.IRRuntimeHelpers;
@@ -195,6 +196,22 @@ public IRClosure getScope() {
return (IRClosure) scope.getIRScope();
}
+ // Proc#refined clones with the same recipe are eql? and hash-equal even as distinct copies,
+ // as in CRuby; every other body compares by identity.
+ @Override
+ public boolean equals(Object other) {
+ if (this == other) return true;
+
+ return other instanceof IRBlockBody body &&
+ getScope() instanceof IRRefinedClosure scope && body.getScope() instanceof IRRefinedClosure otherScope &&
+ scope.sameRecipe(otherScope);
+ }
+
+ @Override
+ public int hashCode() {
+ return getScope() instanceof IRRefinedClosure refined ? refined.recipeHash() : super.hashCode();
+ }
+
@Override
public String getFile() {
return fileName;
diff --git a/core/src/main/java/org/jruby/runtime/MixedModeIRBlockBody.java b/core/src/main/java/org/jruby/runtime/MixedModeIRBlockBody.java
index 6b3c15f15e5..32b5c4f39e9 100644
--- a/core/src/main/java/org/jruby/runtime/MixedModeIRBlockBody.java
+++ b/core/src/main/java/org/jruby/runtime/MixedModeIRBlockBody.java
@@ -179,6 +179,13 @@ private void promoteToFullBuild(ThreadContext context, boolean force) {
ensureInstrsReady();
closure.getNearestTopLocalVariableScope().prepareForCompilation();
+ // A Proc#refined clone is grafted under an already-built enclosing scope, so the recursive
+ // prepareForCompilation above can short-circuit before reaching it. Build the closure's own full IR
+ // directly in that case so the refinement-aware clone can still be JIT-compiled.
+ if (closure.getFullInterpreterContext() == null) {
+ closure.prepareForCompilation();
+ }
+
FullInterpreterContext fic = closure.getFullInterpreterContext();
if (fic == null || !fic.hasExplicitCallProtocol()) {
diff --git a/core/src/main/java/org/jruby/runtime/ObjectSpace.java b/core/src/main/java/org/jruby/runtime/ObjectSpace.java
index 3e2db23e317..bc9f609ae34 100644
--- a/core/src/main/java/org/jruby/runtime/ObjectSpace.java
+++ b/core/src/main/java/org/jruby/runtime/ObjectSpace.java
@@ -47,7 +47,6 @@
import org.jruby.java.proxies.JavaProxy;
import org.jruby.javasupport.JavaPackage;
import org.jruby.runtime.builtin.IRubyObject;
-import org.jruby.util.WeakIdentityHashMap;
/**
* FIXME: This version is faster than the previous, but both suffer from a
@@ -64,7 +63,6 @@ public class ObjectSpace {
private final ReferenceQueue deadIdentityReferences = new ReferenceQueue();
private final Map identities = new HashMap<>(64);
- private final Map identitiesByObject = new WeakIdentityHashMap(64);
private static final AtomicLong maxId = new AtomicLong(1000);
private final ThreadLocal> currentObjectGroup = new ThreadLocal<>();
private Reference groupSweeperReference;
@@ -73,31 +71,14 @@ public void registerObjectId(long id, IRubyObject object) {
synchronized (identities) {
cleanIdentities();
identities.put(id, new IdReference(object, id, deadIdentityReferences));
- identitiesByObject.put(object, id);
}
}
- public static long calculateObjectId(Object object) {
+ public static long calculateObjectId() {
// Fixnums get all the 0b01 id's, flonums get the 0b10 id's, so we use next ID * 4
return maxId.getAndIncrement() * 4;
}
- public long createAndRegisterObjectId(IRubyObject rubyObject) {
- synchronized (identities) {
- Long longId = identitiesByObject.get(rubyObject);
- if (longId == null) {
- longId = createId(rubyObject);
- }
- return longId.longValue();
- }
- }
-
- private long createId(IRubyObject object) {
- long id = calculateObjectId(object);
- registerObjectId(id, object);
- return id;
- }
-
public IRubyObject id2ref(long id) {
synchronized (identities) {
cleanIdentities();
diff --git a/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java b/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java
index dd0e1a0a23a..36e11ffaa00 100644
--- a/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java
+++ b/core/src/main/java/org/jruby/runtime/ivars/VariableTableManager.java
@@ -32,13 +32,11 @@
import java.io.ObjectOutputStream;
import java.lang.invoke.MethodHandle;
import java.util.ArrayList;
-import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.function.Function;
-import org.jruby.Ruby;
import org.jruby.RubyBasicObject;
import org.jruby.RubyClass;
import org.jruby.java.codegen.Reified;
@@ -575,14 +573,7 @@ public VariableTableManager duplicateForData(RubyClass newRealClass) {
* @return The generated object ID
*/
private long initObjectId(RubyBasicObject self, VariableAccessor objectIdAccessor) {
- Ruby runtime = self.getRuntime();
- long id;
-
- if (runtime.isObjectSpaceEnabled()) {
- id = runtime.getObjectSpace().createAndRegisterObjectId(self);
- } else {
- id = ObjectSpace.calculateObjectId(self);
- }
+ long id = ObjectSpace.calculateObjectId();
// we use a direct path here to avoid frozen checks
setObjectId(realClass, self, objectIdAccessor.getIndex(), id);
diff --git a/core/src/main/java/org/jruby/util/ConvertBytes.java b/core/src/main/java/org/jruby/util/ConvertBytes.java
index ed36fd6df3d..a4f2bf25cf8 100644
--- a/core/src/main/java/org/jruby/util/ConvertBytes.java
+++ b/core/src/main/java/org/jruby/util/ConvertBytes.java
@@ -18,6 +18,7 @@ public class ConvertBytes {
private byte[] data;
private int base;
private final boolean badcheck;
+ private boolean nilIfNoDigits;
public ConvertBytes(Ruby runtime, ByteList str, int base, boolean badcheck) {
this.runtime = runtime;
@@ -365,6 +366,14 @@ public static RubyInteger byteListToInum(Ruby runtime, ByteList str, int off, in
return (RubyInteger) new ConvertBytes(runtime, str, off, end, base, badcheck).byteListToInum(true);
}
+ // MRI: rb_int_parse_cstr with endp, which ignores trailing garbage but returns nil
+ // if nothing could be parsed
+ public static IRubyObject byteListToInumOrNil(Ruby runtime, ByteList str, int off, int end, int base) {
+ ConvertBytes convert = new ConvertBytes(runtime, str, off, end, base, false);
+ convert.nilIfNoDigits = true;
+ return convert.byteListToInum(true);
+ }
+
private final static byte[] conv_digit = new byte[128];
private final static boolean[] digit = new boolean[128];
private final static boolean[] space = new boolean[128];
@@ -683,6 +692,8 @@ public IRubyObject byteListToInum(boolean exception) {
return RubyFixnum.zero(runtime);
}
+ final int start = beg;
+
ignoreLeadingWhitespace();
boolean sign = getSign();
@@ -713,6 +724,11 @@ public IRubyObject byteListToInum(boolean exception) {
if (!exception) return runtime.getNil();
invalidString("Integer");
}
+ // MRI only returns 0 if whitespace, sign, prefix or zeros were consumed
+ // without reaching the end
+ if (nilIfNoDigits && (beg == start || beg == end)) {
+ return runtime.getNil();
+ }
return RubyFixnum.zero(runtime);
}
diff --git a/core/src/main/ruby/jruby/kernel/enumerable.rb b/core/src/main/ruby/jruby/kernel/enumerable.rb
index 358786a4402..e60b13b8ced 100644
--- a/core/src/main/ruby/jruby/kernel/enumerable.rb
+++ b/core/src/main/ruby/jruby/kernel/enumerable.rb
@@ -115,14 +115,7 @@ def enumerator_size
end
private :enumerator_size
- # Passing arguments to this method is deprecated.
- def to_set(*args, &block)
- klass = if args.empty?
- Set
- else
- warn "passing arguments to Enumerable#to_set is deprecated", uplevel: 1
- args.shift
- end
- klass.new(self, *args, &block)
+ def to_set(&block)
+ Set.new(self, &block)
end
end
diff --git a/core/src/main/ruby/jruby/kernel/prelude.rb b/core/src/main/ruby/jruby/kernel/prelude.rb
index 06949076c84..7b5a7668982 100644
--- a/core/src/main/ruby/jruby/kernel/prelude.rb
+++ b/core/src/main/ruby/jruby/kernel/prelude.rb
@@ -30,14 +30,7 @@ def pp(*objs)
module Enumerable
# Makes a set from the enumerable object with given arguments.
- # Passing arguments to this method is deprecated.
- def to_set(*args, &block)
- klass = if args.empty?
- Set
- else
- warn "passing arguments to Enumerable#to_set is deprecated", uplevel: 1
- args.shift
- end
- klass.new(self, *args, &block)
+ def to_set(&block)
+ Set.new(self, &block)
end
end
diff --git a/core/src/test/java/org/jruby/test/TestObjectSpace.java b/core/src/test/java/org/jruby/test/TestObjectSpace.java
index 4cf1915560b..e27b20f3069 100644
--- a/core/src/test/java/org/jruby/test/TestObjectSpace.java
+++ b/core/src/test/java/org/jruby/test/TestObjectSpace.java
@@ -38,7 +38,6 @@
import junit.framework.TestCase;
import org.jruby.Ruby;
-import org.jruby.RubyString;
import org.jruby.api.Create;
import org.jruby.runtime.ObjectSpace;
import org.jruby.runtime.ThreadContext;
@@ -46,7 +45,6 @@
import static org.jruby.api.Access.arrayClass;
import static org.jruby.api.Access.stringClass;
-import static org.jruby.api.Create.newArray;
import static org.jruby.api.Create.newString;
/**
@@ -67,23 +65,6 @@ public void setUp() throws Exception {
target = new ObjectSpace();
}
- public void testIdentities() {
- RubyString o1 = newString(context, "hey");
- RubyString o2 = newString(context, "ho");
-
- long id1 = target.createAndRegisterObjectId(o1);
- long id2 = target.createAndRegisterObjectId(o2);
-
- assertEquals("id of normal objects must be even", 0, id1 % 2);
- assertEquals("id of normal objects must be even", 0, id2 % 2);
- assertTrue("normal ids must be bigger than reserved values", id1 > 4);
- assertTrue("normal ids must be bigger than reserved values", id2 > 4);
-
- assertSame(o1, target.id2ref(id1));
- assertSame(o2, target.id2ref(id2));
- assertNull(target.id2ref(4711));
- }
-
public void testObjectSpace() {
IRubyObject o1 = Create.allocArray(context, 10);
IRubyObject o2 = Create.allocArray(context, 20);
diff --git a/default.build.properties b/default.build.properties
index 110dc39d37b..c2a5b7583e4 100644
--- a/default.build.properties
+++ b/default.build.properties
@@ -28,7 +28,7 @@ rake.args=
install4j.executable=/Applications/install4j9/bin/install4jc
# Ruby versions
-version.ruby=4.0.0
-version.ruby.major=4.0
+version.ruby=4.1.0
+version.ruby.major=4.1
version.ruby.minor=0
diff --git a/lib/pom.xml b/lib/pom.xml
index b4efb21444e..590b52a4813 100644
--- a/lib/pom.xml
+++ b/lib/pom.xml
@@ -1380,89 +1380,89 @@ DO NOT MODIFY - GENERATED CODE
cache/rss-0.3.3*
cache/syslog-0.4.0*
cache/test-unit-3.7.8*
- extensions/universal-java/4.0.0/rubygems-update-4.0.21/*
- extensions/universal-java/4.0.0/bundler-4.0.21/*
- extensions/universal-java/4.0.0/date-3.5.1/*
- extensions/universal-java/4.0.0/delegate-0.6.1/*
- extensions/universal-java/4.0.0/did_you_mean-2.0.0/*
- extensions/universal-java/4.0.0/digest-3.2.1/*
- extensions/universal-java/4.0.0/english-0.8.1/*
- extensions/universal-java/4.0.0/erb-6.0.7/*
- extensions/universal-java/4.0.0/error_highlight-0.7.2/*
- extensions/universal-java/4.0.0/ffi-1.17.4/*
- extensions/universal-java/4.0.0/fiddle-1.1.8/*
- extensions/universal-java/4.0.0/fileutils-1.8.0/*
- extensions/universal-java/4.0.0/find-0.2.0/*
- extensions/universal-java/4.0.0/forwardable-1.4.0/*
- extensions/universal-java/4.0.0/io-console-0.9.4/*
- extensions/universal-java/4.0.0/io-wait-1.0.0/*
- extensions/universal-java/4.0.0/ipaddr-1.2.9/*
- extensions/universal-java/4.0.0/jar-dependencies-0.6.1/*
- extensions/universal-java/4.0.0/jruby-readline-1.3.7/*
- extensions/universal-java/4.0.0/jruby-openssl-0.19.3/*
- extensions/universal-java/4.0.0/json-2.21.2/*
- extensions/universal-java/4.0.0/net-http-0.9.1/*
- extensions/universal-java/4.0.0/net-protocol-0.4.0/*
- extensions/universal-java/4.0.0/open-uri-0.5.0/*
- extensions/universal-java/4.0.0/open3-0.2.1/*
- extensions/universal-java/4.0.0/optparse-0.8.1/*
- extensions/universal-java/4.0.0/pp-0.6.4/*
- extensions/universal-java/4.0.0/prettyprint-0.2.0/*
- extensions/universal-java/4.0.0/psych-5.5.0/*
- extensions/universal-java/4.0.0/resolv-0.8.0/*
- extensions/universal-java/4.0.0/ruby2_keywords-0.0.5/*
- extensions/universal-java/4.0.0/securerandom-0.4.1/*
- extensions/universal-java/4.0.0/shellwords-0.2.2/*
- extensions/universal-java/4.0.0/singleton-0.3.0/*
- extensions/universal-java/4.0.0/stringio-3.2.0/*
- extensions/universal-java/4.0.0/strscan-3.1.8/*
- extensions/universal-java/4.0.0/subspawn-0.1.1/*
- extensions/universal-java/4.0.0/subspawn-posix-0.1.1/*
- extensions/universal-java/4.0.0/ffi-binary-libfixposix-0.5.1.1/*
- extensions/universal-java/4.0.0/ffi-bindings-libfixposix-0.5.1.0/*
- extensions/universal-java/4.0.0/syntax_suggest-3.0.0/*
- extensions/universal-java/4.0.0/tempfile-0.3.1/*
- extensions/universal-java/4.0.0/time-0.4.2/*
- extensions/universal-java/4.0.0/timeout-0.6.1/*
- extensions/universal-java/4.0.0/tsort-0.2.0/*
- extensions/universal-java/4.0.0/un-0.3.0/*
- extensions/universal-java/4.0.0/uri-1.1.1/*
- extensions/universal-java/4.0.0/weakref-0.1.4/*
- extensions/universal-java/4.0.0/win32-registry-0.1.2/*
- extensions/universal-java/4.0.0/yaml-0.4.0/*
- extensions/universal-java/4.0.0/abbrev-0.1.2/*
- extensions/universal-java/4.0.0/base64-0.3.0/*
- extensions/universal-java/4.0.0/benchmark-0.5.0/*
- extensions/universal-java/4.0.0/bigdecimal-4.1.3/*
- extensions/universal-java/4.0.0/csv-3.3.6/*
- extensions/universal-java/4.0.0/debug-0.2.1/*
- extensions/universal-java/4.0.0/drb-2.2.3/*
- extensions/universal-java/4.0.0/getoptlong-0.2.1/*
- extensions/universal-java/4.0.0/irb-1.16.0/*
- extensions/universal-java/4.0.0/logger-1.7.0/*
- extensions/universal-java/4.0.0/matrix-0.4.3/*
- extensions/universal-java/4.0.0/minitest-5.27.0/*
- extensions/universal-java/4.0.0/mutex_m-0.3.0/*
- extensions/universal-java/4.0.0/net-ftp-0.3.9/*
- extensions/universal-java/4.0.0/net-imap-0.6.7/*
- extensions/universal-java/4.0.0/net-pop-0.1.2/*
- extensions/universal-java/4.0.0/net-smtp-0.5.1/*
- extensions/universal-java/4.0.0/nkf-0.3.0/*
- extensions/universal-java/4.0.0/observer-0.1.2/*
- extensions/universal-java/4.0.0/ostruct-0.6.3/*
- extensions/universal-java/4.0.0/power_assert-3.0.1/*
- extensions/universal-java/4.0.0/prime-0.1.4/*
- extensions/universal-java/4.0.0/pstore-0.2.1/*
- extensions/universal-java/4.0.0/racc-1.8.1/*
- extensions/universal-java/4.0.0/rake-${rake.version}/*
- extensions/universal-java/4.0.0/rdoc-7.2.0/*
- extensions/universal-java/4.0.0/reline-0.7.0/*
- extensions/universal-java/4.0.0/resolv-replace-0.2.0/*
- extensions/universal-java/4.0.0/rexml-3.4.4/*
- extensions/universal-java/4.0.0/rinda-0.2.0/*
- extensions/universal-java/4.0.0/rss-0.3.3/*
- extensions/universal-java/4.0.0/syslog-0.4.0/*
- extensions/universal-java/4.0.0/test-unit-3.7.8/*
+ extensions/universal-java/4.1.0/rubygems-update-4.0.21/*
+ extensions/universal-java/4.1.0/bundler-4.0.21/*
+ extensions/universal-java/4.1.0/date-3.5.1/*
+ extensions/universal-java/4.1.0/delegate-0.6.1/*
+ extensions/universal-java/4.1.0/did_you_mean-2.0.0/*
+ extensions/universal-java/4.1.0/digest-3.2.1/*
+ extensions/universal-java/4.1.0/english-0.8.1/*
+ extensions/universal-java/4.1.0/erb-6.0.7/*
+ extensions/universal-java/4.1.0/error_highlight-0.7.2/*
+ extensions/universal-java/4.1.0/ffi-1.17.4/*
+ extensions/universal-java/4.1.0/fiddle-1.1.8/*
+ extensions/universal-java/4.1.0/fileutils-1.8.0/*
+ extensions/universal-java/4.1.0/find-0.2.0/*
+ extensions/universal-java/4.1.0/forwardable-1.4.0/*
+ extensions/universal-java/4.1.0/io-console-0.9.4/*
+ extensions/universal-java/4.1.0/io-wait-1.0.0/*
+ extensions/universal-java/4.1.0/ipaddr-1.2.9/*
+ extensions/universal-java/4.1.0/jar-dependencies-0.6.1/*
+ extensions/universal-java/4.1.0/jruby-readline-1.3.7/*
+ extensions/universal-java/4.1.0/jruby-openssl-0.19.3/*
+ extensions/universal-java/4.1.0/json-2.21.2/*
+ extensions/universal-java/4.1.0/net-http-0.9.1/*
+ extensions/universal-java/4.1.0/net-protocol-0.4.0/*
+ extensions/universal-java/4.1.0/open-uri-0.5.0/*
+ extensions/universal-java/4.1.0/open3-0.2.1/*
+ extensions/universal-java/4.1.0/optparse-0.8.1/*
+ extensions/universal-java/4.1.0/pp-0.6.4/*
+ extensions/universal-java/4.1.0/prettyprint-0.2.0/*
+ extensions/universal-java/4.1.0/psych-5.5.0/*
+ extensions/universal-java/4.1.0/resolv-0.8.0/*
+ extensions/universal-java/4.1.0/ruby2_keywords-0.0.5/*
+ extensions/universal-java/4.1.0/securerandom-0.4.1/*
+ extensions/universal-java/4.1.0/shellwords-0.2.2/*
+ extensions/universal-java/4.1.0/singleton-0.3.0/*
+ extensions/universal-java/4.1.0/stringio-3.2.0/*
+ extensions/universal-java/4.1.0/strscan-3.1.8/*
+ extensions/universal-java/4.1.0/subspawn-0.1.1/*
+ extensions/universal-java/4.1.0/subspawn-posix-0.1.1/*
+ extensions/universal-java/4.1.0/ffi-binary-libfixposix-0.5.1.1/*
+ extensions/universal-java/4.1.0/ffi-bindings-libfixposix-0.5.1.0/*
+ extensions/universal-java/4.1.0/syntax_suggest-3.0.0/*
+ extensions/universal-java/4.1.0/tempfile-0.3.1/*
+ extensions/universal-java/4.1.0/time-0.4.2/*
+ extensions/universal-java/4.1.0/timeout-0.6.1/*
+ extensions/universal-java/4.1.0/tsort-0.2.0/*
+ extensions/universal-java/4.1.0/un-0.3.0/*
+ extensions/universal-java/4.1.0/uri-1.1.1/*
+ extensions/universal-java/4.1.0/weakref-0.1.4/*
+ extensions/universal-java/4.1.0/win32-registry-0.1.2/*
+ extensions/universal-java/4.1.0/yaml-0.4.0/*
+ extensions/universal-java/4.1.0/abbrev-0.1.2/*
+ extensions/universal-java/4.1.0/base64-0.3.0/*
+ extensions/universal-java/4.1.0/benchmark-0.5.0/*
+ extensions/universal-java/4.1.0/bigdecimal-4.1.3/*
+ extensions/universal-java/4.1.0/csv-3.3.6/*
+ extensions/universal-java/4.1.0/debug-0.2.1/*
+ extensions/universal-java/4.1.0/drb-2.2.3/*
+ extensions/universal-java/4.1.0/getoptlong-0.2.1/*
+ extensions/universal-java/4.1.0/irb-1.16.0/*
+ extensions/universal-java/4.1.0/logger-1.7.0/*
+ extensions/universal-java/4.1.0/matrix-0.4.3/*
+ extensions/universal-java/4.1.0/minitest-5.27.0/*
+ extensions/universal-java/4.1.0/mutex_m-0.3.0/*
+ extensions/universal-java/4.1.0/net-ftp-0.3.9/*
+ extensions/universal-java/4.1.0/net-imap-0.6.7/*
+ extensions/universal-java/4.1.0/net-pop-0.1.2/*
+ extensions/universal-java/4.1.0/net-smtp-0.5.1/*
+ extensions/universal-java/4.1.0/nkf-0.3.0/*
+ extensions/universal-java/4.1.0/observer-0.1.2/*
+ extensions/universal-java/4.1.0/ostruct-0.6.3/*
+ extensions/universal-java/4.1.0/power_assert-3.0.1/*
+ extensions/universal-java/4.1.0/prime-0.1.4/*
+ extensions/universal-java/4.1.0/pstore-0.2.1/*
+ extensions/universal-java/4.1.0/racc-1.8.1/*
+ extensions/universal-java/4.1.0/rake-${rake.version}/*
+ extensions/universal-java/4.1.0/rdoc-7.2.0/*
+ extensions/universal-java/4.1.0/reline-0.7.0/*
+ extensions/universal-java/4.1.0/resolv-replace-0.2.0/*
+ extensions/universal-java/4.1.0/rexml-3.4.4/*
+ extensions/universal-java/4.1.0/rinda-0.2.0/*
+ extensions/universal-java/4.1.0/rss-0.3.3/*
+ extensions/universal-java/4.1.0/syslog-0.4.0/*
+ extensions/universal-java/4.1.0/test-unit-3.7.8/*
diff --git a/spec/ruby/core/module/method_defined_spec.rb b/spec/ruby/core/module/method_defined_spec.rb
index e6b4c7b8176..698ff682194 100644
--- a/spec/ruby/core/module/method_defined_spec.rb
+++ b/spec/ruby/core/module/method_defined_spec.rb
@@ -95,4 +95,98 @@
ModuleSpecs::Child.method_defined?(:private_super_module, false).should == false
end
end
+
+ ruby_version_is "4.1" do
+ describe "when passed true as include_all" do
+ it "returns true for private methods as well" do
+ # Include super
+ ModuleSpecs::Child.method_defined?(:public_child, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_child, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:accessor_method, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_child, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:undefined, true, true).should == false
+
+ # Defined in Parent
+ ModuleSpecs::Child.method_defined?(:public_parent, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_parent, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_parent, true, true).should == true
+
+ # Defined in Module
+ ModuleSpecs::Child.method_defined?(:public_module, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_module, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_module, true, true).should == true
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.method_defined?(:public_super_module, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_super_module, true, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_super_module, true, true).should == true
+
+ # Ignore super
+ ModuleSpecs::Child.method_defined?(:public_child, false, true).should == true
+ ModuleSpecs::Child.method_defined?(:protected_child, false, true).should == true
+ ModuleSpecs::Child.method_defined?(:accessor_method, false, true).should == true
+ ModuleSpecs::Child.method_defined?(:private_child, false, true).should == true
+ ModuleSpecs::Child.method_defined?(:undefined, false, true).should == false
+
+ # Defined in Parent
+ ModuleSpecs::Child.method_defined?(:public_parent, false, true).should == false
+ ModuleSpecs::Child.method_defined?(:protected_parent, false, true).should == false
+ ModuleSpecs::Child.method_defined?(:private_parent, false, true).should == false
+
+ # Defined in Module
+ ModuleSpecs::Child.method_defined?(:public_module, false, true).should == false
+ ModuleSpecs::Child.method_defined?(:protected_module, false, true).should == false
+ ModuleSpecs::Child.method_defined?(:private_module, false, true).should == false
+
+ # Defined in SuperModule
+ ModuleSpecs::Child.method_defined?(:public_super_module, false, true).should == false
+ ModuleSpecs::Child.method_defined?(:protected_super_module, false, true).should == false
+ ModuleSpecs::Child.method_defined?(:private_super_module, false, true).should == false
+ end
+ end
+
+ describe "when passed false as include_all" do
+ it "does not return true for private methods" do
+ # Include super
+ ModuleSpecs::Child.method_defined?(:public_child, true, false).should == true
+ ModuleSpecs::Child.method_defined?(:protected_child, true, false).should == true
+ ModuleSpecs::Child.method_defined?(:private_child, true, false).should == false
+ ModuleSpecs::Child.method_defined?(:public_parent, true, false).should == true
+ ModuleSpecs::Child.method_defined?(:private_parent, true, false).should == false
+ ModuleSpecs::Child.method_defined?(:public_module, true, false).should == true
+ ModuleSpecs::Child.method_defined?(:private_module, true, false).should == false
+ ModuleSpecs::Child.method_defined?(:private_super_module, true, false).should == false
+ ModuleSpecs::Child.method_defined?(:undefined, true, false).should == false
+
+ # Ignore super
+ ModuleSpecs::Child.method_defined?(:public_child, false, false).should == true
+ ModuleSpecs::Child.method_defined?(:protected_child, false, false).should == true
+ ModuleSpecs::Child.method_defined?(:private_child, false, false).should == false
+ ModuleSpecs::Child.method_defined?(:public_parent, false, false).should == false
+ ModuleSpecs::Child.method_defined?(:private_parent, false, false).should == false
+ ModuleSpecs::Child.method_defined?(:private_module, false, false).should == false
+ ModuleSpecs::Child.method_defined?(:undefined, false, false).should == false
+ end
+
+ it "returns the same result as when the third argument is omitted" do
+ [:public_child, :protected_child, :private_child, :public_parent, :private_parent,
+ :public_module, :private_module, :private_super_module, :undefined].each do |name|
+ [true, false].each do |include_super|
+ ModuleSpecs::Child.method_defined?(name, include_super, false).should ==
+ ModuleSpecs::Child.method_defined?(name, include_super)
+ end
+ end
+ end
+ end
+
+ describe "when passed nil as include_all" do
+ it "does not return true for private methods" do
+ ModuleSpecs::Child.method_defined?(:private_child, true, nil).should == false
+ ModuleSpecs::Child.method_defined?(:private_child, false, nil).should == false
+ ModuleSpecs::Child.method_defined?(:private_parent, true, nil).should == false
+ ModuleSpecs::Child.method_defined?(:public_child, true, nil).should == true
+ ModuleSpecs::Child.method_defined?(:public_parent, false, nil).should == false
+ end
+ end
+ end
end
diff --git a/spec/ruby/core/set/classify_spec.rb b/spec/ruby/core/set/classify_spec.rb
index 1cfacf3576c..a9521d3b528 100644
--- a/spec/ruby/core/set/classify_spec.rb
+++ b/spec/ruby/core/set/classify_spec.rb
@@ -24,9 +24,19 @@
classified.should == { 3 => Set["one", "two"], 4 => Set["four"], 5 => Set["three"] }
end
- it "does not retain compare_by_identity flag" do
- set = Set["one", "two"].compare_by_identity
- classified = set.classify { |x| x.length }
- classified.values.each { |s| s.compare_by_identity?.should == false }
+ ruby_version_is "4.1" do
+ it "retains compare_by_identity flag" do
+ set = Set["one", "two"].compare_by_identity
+ classified = set.classify { |x| x.length }
+ classified.values.each { |s| s.compare_by_identity?.should == true }
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "does not retain compare_by_identity flag" do
+ set = Set["one", "two"].compare_by_identity
+ classified = set.classify { |x| x.length }
+ classified.values.each { |s| s.compare_by_identity?.should == false }
+ end
end
end
diff --git a/spec/ruby/core/set/divide_spec.rb b/spec/ruby/core/set/divide_spec.rb
index e00e4616e19..96880db9b4b 100644
--- a/spec/ruby/core/set/divide_spec.rb
+++ b/spec/ruby/core/set/divide_spec.rb
@@ -18,10 +18,20 @@
ret.each(&:even?).should == Set[Set[1, 3], Set[2, 4]]
end
- it "does not retain compare_by_identity flag" do
- set = Set["one", "two"].compare_by_identity
- res = set.divide { |x| x.length }
- res.each { |s| s.compare_by_identity?.should == false }
+ ruby_version_is "4.1" do
+ it "retains compare_by_identity flag" do
+ set = Set["one", "two"].compare_by_identity
+ res = set.divide { |x| x.length }
+ res.each { |s| s.compare_by_identity?.should == true }
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "does not retain compare_by_identity flag" do
+ set = Set["one", "two"].compare_by_identity
+ res = set.divide { |x| x.length }
+ res.each { |s| s.compare_by_identity?.should == false }
+ end
end
end
diff --git a/spec/ruby/core/set/flatten_spec.rb b/spec/ruby/core/set/flatten_spec.rb
index 65c63a9b7f3..9fc05ea3731 100644
--- a/spec/ruby/core/set/flatten_spec.rb
+++ b/spec/ruby/core/set/flatten_spec.rb
@@ -16,17 +16,24 @@
-> { set.flatten }.should.raise(ArgumentError)
end
+ ruby_version_is "4.1" do
+ it "retains compare_by_identity flag" do
+ set = Set[1, 2, Set[3, 4]].compare_by_identity
+ set.flatten.compare_by_identity?.should == true
+ end
+ end
+
ruby_version_is ""..."4.0" do
context "when Set contains a Set-like object" do
it "returns a copy of self with each included Set-like object flattened" do
Set[SetSpecs::SetLike.new([1])].flatten.should == Set[1]
end
end
- end
- it "does not retain compare_by_identity flag" do
- set = Set[1, 2, Set[3, 4]].compare_by_identity
- set.flatten.compare_by_identity?.should == false
+ it "does not retain compare_by_identity flag" do
+ set = Set[1, 2, Set[3, 4]].compare_by_identity
+ set.flatten.compare_by_identity?.should == false
+ end
end
end
@@ -52,10 +59,20 @@
-> { set.flatten! }.should.raise(ArgumentError)
end
- it "does not retain compare_by_identity flag when flattening elements" do
- set = Set[1, 2, Set[3, 4]].compare_by_identity
- set.flatten!
- set.compare_by_identity?.should == false
+ ruby_version_is "4.1" do
+ it "retains compare_by_identity flag when flattening elements" do
+ set = Set[1, 2, Set[3, 4]].compare_by_identity
+ set.flatten!
+ set.compare_by_identity?.should == true
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "does not retain compare_by_identity flag when flattening elements" do
+ set = Set[1, 2, Set[3, 4]].compare_by_identity
+ set.flatten!
+ set.compare_by_identity?.should == false
+ end
end
it "retains compare_by_identity flag if no elements are flattened" do
diff --git a/spec/ruby/core/set/intersection_spec.rb b/spec/ruby/core/set/intersection_spec.rb
index 3551fe70189..2e1388e0cc3 100644
--- a/spec/ruby/core/set/intersection_spec.rb
+++ b/spec/ruby/core/set/intersection_spec.rb
@@ -21,9 +21,19 @@
-> { @set & Object.new }.should.raise(ArgumentError)
end
- it "does not retain compare_by_identity flag" do
- @set.compare_by_identity
- (@set & Set[:b, :c, :d, :e]).compare_by_identity?.should == false
- (@set & [:b, :c, :d]).compare_by_identity?.should == false
+ ruby_version_is "4.1" do
+ it "retains compare_by_identity flag" do
+ @set.compare_by_identity
+ (@set & Set[:b, :c, :d, :e]).compare_by_identity?.should == true
+ (@set & [:b, :c, :d]).compare_by_identity?.should == true
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "does not retain compare_by_identity flag" do
+ @set.compare_by_identity
+ (@set & Set[:b, :c, :d, :e]).compare_by_identity?.should == false
+ (@set & [:b, :c, :d]).compare_by_identity?.should == false
+ end
end
end
diff --git a/spec/ruby/core/set/map_spec.rb b/spec/ruby/core/set/map_spec.rb
index 6f6959feebd..7121afc459b 100644
--- a/spec/ruby/core/set/map_spec.rb
+++ b/spec/ruby/core/set/map_spec.rb
@@ -20,9 +20,19 @@
@set.should == Set[2, 4, 6, 8, 10]
end
- it "does not retain compare_by_identity flag" do
- @set.compare_by_identity
- @set.map! { |x| x * 2 }
- @set.compare_by_identity?.should == false
+ ruby_version_is "4.1" do
+ it "retains compare_by_identity flag" do
+ @set.compare_by_identity
+ @set.map! { |x| x * 2 }
+ @set.compare_by_identity?.should == true
+ end
+ end
+
+ ruby_version_is ""..."4.0" do
+ it "does not retain compare_by_identity flag" do
+ @set.compare_by_identity
+ @set.map! { |x| x * 2 }
+ @set.compare_by_identity?.should == false
+ end
end
end
diff --git a/spec/ruby/core/string/tr_spec.rb b/spec/ruby/core/string/tr_spec.rb
index 631a1e4964e..2cc193a3ca1 100644
--- a/spec/ruby/core/string/tr_spec.rb
+++ b/spec/ruby/core/string/tr_spec.rb
@@ -169,6 +169,34 @@
).should == "hello".encode(Encoding::UTF_16LE)
end
+ it "replaces characters above U+00FF" do
+ "日本語".tr("本" => "ほん").should == "日ほん語"
+ end
+
+ it "does not modify the receiver" do
+ str = "hello"
+ str.tr("l" => "L").should == "heLLo"
+ str.should == "hello"
+ end
+
+ it "does not match an invalid key against characters of a valid string" do
+ "abc".tr("\xFF".dup.force_encoding(Encoding::UTF_8) => "x").should == "abc"
+ end
+
+ it "skips entries deleted from the Hash by a key's #to_str" do
+ pairs = {}
+ key = Object.new
+ key.define_singleton_method(:to_str) { pairs.delete("b"); "a" }
+ pairs[key] = "x"
+ pairs["b"] = "y"
+ "ab".tr(pairs).should == "xb"
+ end
+
+ it "raises TypeError if the only argument is not a Hash" do
+ -> { "hello".tr(1) }.should.raise(TypeError, "wrong argument type Integer (expected Hash)")
+ -> { "hello".tr(nil) }.should.raise(TypeError, "wrong argument type nil (expected Hash)")
+ end
+
it "raises ArgumentError if a key is more than one codepoint" do
-> { "hello".tr("hel" => "") }.should.raise(ArgumentError)
-> { "🤦🏼♂️".tr("🤦🏼♂️" => "") }.should.raise(ArgumentError)
@@ -243,6 +271,24 @@
"hello".tr!("€" => "", "Ø" => "").should == nil
end
+ it "replaces characters above U+00FF" do
+ "日本語".tr!("本" => "ほん").should == "日ほん語"
+ end
+
+ it "modifies the receiver in place" do
+ str = "hello"
+ str.tr!("l" => "L").should.equal?(str)
+ str.should == "heLLo"
+ end
+
+ it "raises FrozenError if the receiver is frozen" do
+ -> { "hello".freeze.tr!("l" => "L") }.should.raise(FrozenError)
+ end
+
+ it "raises TypeError if the only argument is not a Hash" do
+ -> { "hello".tr!(1) }.should.raise(TypeError, "wrong argument type Integer (expected Hash)")
+ end
+
it "raises ArgumentError if a key is more than one codepoint" do
-> { "hello".tr!("hel" => "") }.should.raise(ArgumentError)
-> { "🤦🏼♂️".tr!("🤦🏼♂️" => "") }.should.raise(ArgumentError)
diff --git a/spec/tags/ruby/core/set/flatten_tags.txt b/spec/tags/ruby/core/set/flatten_tags.txt
deleted file mode 100644
index a70a8488266..00000000000
--- a/spec/tags/ruby/core/set/flatten_tags.txt
+++ /dev/null
@@ -1 +0,0 @@
-fails:Set#flatten! does not retain compare_by_identity flag when flattening elements
diff --git a/spec/tags/ruby/core/set/map_tags.txt b/spec/tags/ruby/core/set/map_tags.txt
deleted file mode 100644
index fcfe86f918a..00000000000
--- a/spec/tags/ruby/core/set/map_tags.txt
+++ /dev/null
@@ -1 +0,0 @@
-fails:Set#map! does not retain compare_by_identity flag
diff --git a/test/jruby/test_objectspace.rb b/test/jruby/test_objectspace.rb
index 579c543b2cd..2d2b79f0c32 100644
--- a/test/jruby/test_objectspace.rb
+++ b/test/jruby/test_objectspace.rb
@@ -12,38 +12,6 @@ def teardown
JRuby.objectspace = @objectspace
end
- def test_id2ref
- # Normal objects
- o1 = "hey"
- o2 = "ho"
- id1 = o1.object_id
- id2 = o2.object_id
- assert_equal(o1, ObjectSpace._id2ref(id1))
- assert_equal(o2, ObjectSpace._id2ref(id2))
-
- # Fixnums
- o1 = 17
- o2 = 100001
- id1 = o1.object_id
- id2 = o2.object_id
- assert_equal(o1, ObjectSpace._id2ref(id1))
- assert_equal(o2, ObjectSpace._id2ref(id2))
-
- assert_equal(1, 0.object_id)
- assert_equal(3, 1.object_id)
- assert_equal(201, 100.object_id)
- assert_equal(-1, -1.object_id)
- assert_equal(-19, -10.object_id)
-
- assert_equal(0, false.object_id)
- assert_equal(20, true.object_id)
- assert_equal(8, nil.object_id)
-
- assert_equal(false, ObjectSpace._id2ref(0))
- assert_equal(true, ObjectSpace._id2ref(20))
- assert_equal(nil, ObjectSpace._id2ref(8))
- end
-
def test_jruby_objectspace
JRuby.objectspace = false
assert_equal(false, JRuby.objectspace)
diff --git a/test/jruby/test_proc_refined.rb b/test/jruby/test_proc_refined.rb
new file mode 100644
index 00000000000..238f6e4284a
--- /dev/null
+++ b/test/jruby/test_proc_refined.rb
@@ -0,0 +1,166 @@
+require 'test/unit'
+require 'test/jruby/test_helper'
+require 'tempfile'
+require 'jruby'
+
+class TestProcRefined < Test::Unit::TestCase
+ include TestHelper
+ module StringRefinement
+ refine String do
+ def shout
+ upcase + "!"
+ end
+ end
+ end
+
+ # Instructions of a refined clone are copied on the first call: nested closure shells only
+ # appear once the enclosing body materializes, so a never-called refined proc stays a cheap shell.
+ def test_clone_materializes_on_first_call
+ prc = ->(a) { a.map { |s| s.shout } }
+ refined = prc.refined(StringRefinement)
+ scope = JRuby.reference(refined).get_block.get_body.get_scope
+ assert_equal 0, scope.get_closures.size, "nested shells created before the first call"
+ assert_equal ["A!"], refined.call(["a"])
+ assert_equal 1, scope.get_closures.size, "nested shell not created at materialization"
+ end
+
+ # Concurrent first calls can force the lazy instruction copy from several threads at once;
+ # a duplicate build would register nested shells twice in the clone.
+ def test_concurrent_first_call_builds_once
+ 20.times do
+ refined = ->(s) { [s].map { |x| x.shout }.first }.refined(StringRefinement)
+ scope = JRuby.reference(refined).get_block.get_body.get_scope
+ latch = java.util.concurrent.CountDownLatch.new(1)
+ results = Queue.new
+ threads = Array.new(8) do
+ Thread.new do
+ latch.await
+ results << refined.call("a")
+ end
+ end
+ latch.count_down
+ threads.each(&:join)
+ 8.times { assert_equal "A!", results.pop }
+ assert_equal 1, scope.get_closures.size, "duplicate lazy build registered nested shells twice"
+ end
+ end
+
+ # p.refined(a).refined(b) shares one memoized clone with p.refined(a, b); the memo is written at
+ # materialization, so the never-called chain intermediate does not evict the live entry.
+ def test_chain_shares_memoized_clone
+ quiet = Module.new { refine(String) { def shout; downcase; end } }
+ prc = ->(s) { s.shout }
+ combined = prc.refined(StringRefinement, quiet)
+ assert_equal "hi", combined.call("Hi")
+ chained = prc.refined(StringRefinement).refined(quiet)
+ assert_equal "hi", chained.call("Hi")
+ combined_scope = JRuby.reference(combined).get_block.get_body.get_scope
+ chained_scope = JRuby.reference(chained).get_block.get_body.get_scope
+ # not assert_equal: test-unit's diagnostics call encoding, which IRScope stubs with an exception
+ assert_same combined_scope, chained_scope, "chained call did not reuse the memoized clone"
+ end
+
+ # A proc created inside a refined body lives in the clone tree; refined() on it must still produce
+ # a proper root clone: memoized once materialized, chainable, rejected by define_method.
+ def test_refined_on_proc_from_refined_body
+ quiet = Module.new { refine(String) { def shout; downcase; end } }
+ inner = -> { ->(s) { s.shout } }.refined(StringRefinement).call
+ r1 = inner.refined(quiet)
+ assert_equal "hi", r1.call("Hi")
+ r2 = inner.refined(quiet)
+ assert_same JRuby.reference(r1).get_block.get_body.get_scope,
+ JRuby.reference(r2).get_block.get_body.get_scope,
+ "repeat refined() on a clone-tree proc did not hit the memo"
+ # the chain re-clones from inner with the merged list, so the later module wins
+ assert_equal "HI!", r1.refined(StringRefinement).call("Hi")
+ assert_raise(ArgumentError) { Class.new { define_method(:m, r1) } }
+ end
+
+ # Refinement activation (refined -> usingModuleRecursive) can run on any thread while another
+ # thread mutates the same module's refinement map via Module#refine; the activation must
+ # snapshot the map instead of iterating it unsynchronized (ConcurrentModificationException).
+ def test_concurrent_refined_and_refine
+ mod = Module.new { refine(String) { def shout = upcase } }
+ fillers = Array.new(2) { Module.new { refine(String) { def noop = self } } }
+ src = ->(s) { s.shout }
+ writer = Thread.new do
+ 300.times { mod.module_eval { refine(Class.new) { def x = 1 } } }
+ end
+ i = 0
+ assert_nothing_raised do
+ while writer.alive?
+ # alternate the second module to defeat the single-slot memo and force re-activation
+ assert_equal("HI", src.refined(mod, fillers[i % 2]).call("hi"))
+ i += 1
+ end
+ assert_equal("HI", src.refined(mod, fillers[0]).call("hi"))
+ end
+ ensure
+ writer.join
+ end
+
+ # Each refined clone is grafted under the SOURCE proc's (shared, long-lived) lexical parent, but it must
+ # not be registered in that parent's nested-closure / lexical-child lists: doing so would retain every discarded
+ # clone forever (memory leak) and race with concurrent calls on those unsynchronized/AOT lists. Drive many
+ # cache-missing clones (distinct module each time) and assert the parent's lists do not grow.
+ def test_clones_not_retained_by_lexical_parent
+ prc = ->(s) { s.to_s }
+ closure = JRuby.reference(prc).get_block.get_body.get_scope
+ parent = closure.get_lexical_parent
+ before_closures = parent.get_closures.size
+ before_children = parent.get_lexical_scopes.size
+ 200.times do
+ mod = Module.new { refine(String) { def to_s; "x"; end } }
+ prc.refined(mod).call("hi")
+ end
+ assert_operator parent.get_closures.size - before_closures, :<=, 1,
+ "refined clones accumulated in the source's parent nestedClosures"
+ assert_operator parent.get_lexical_scopes.size - before_children, :<=, 1,
+ "refined clones accumulated in the source's parent lexicalChildren"
+ end
+
+ # An exception raised through a refined clone whose source block was created at top level (so the captured
+ # frame has no method name) must still build a backtrace -- the clone always runs interpreted, and a null frame name
+ # used to crash backtrace construction with a Java NullPointerException instead of surfacing the Ruby exception.
+ def test_exception_backtrace_through_top_level_clone
+ refined = -> { raise "boom" }.refined(StringRefinement)
+ err = assert_raise(RuntimeError) { refined.call }
+ assert_equal "boom", err.message
+ assert_match(/block in /, err.backtrace.first)
+ end
+
+ # Many independently created refined clones must all survive GC and keep working.
+ def test_gc
+ procs = 100.times.map { ->(s) { s.shout }.refined(StringRefinement) }
+ JRuby.gc rescue GC.start
+ procs.each { |pr| assert_equal("A!", pr.call("a")) }
+ end
+
+ # A refinement-aware clone is grafted under an already-built enclosing scope, so it needs its own full IR
+ # built before it can be JIT-compiled. Run in a subprocess with a low JIT threshold and synchronous (non
+ # background) compilation so both the original and the clone cross the threshold deterministically. We
+ # assert two things: the refinement still produces correct results after JIT (and the original is
+ # unaffected), and -- crucially for guarding the fix -- that the clone did not hit "JIT failed". Without
+ # the fix the clone cannot build its full IR and falls back to the interpreter, which is still correct, so
+ # the JIT log is what distinguishes a working fix from a silent regression.
+ def test_refinement_survives_jit
+ script = <<~'RUBY'
+ module R
+ refine(String) { def upcase; "REFINED"; end }
+ end
+ prc = ->(s) { s.upcase }
+ 1000.times { exit(1) unless prc.call("hi") == "HI" } # JIT the original (bare upcase) first
+ refined = prc.refined(R)
+ 1000.times { exit(2) unless refined.call("hi") == "REFINED" } # drive the clone past the threshold
+ exit(3) unless prc.call("hi") == "HI" # original must stay unaffected post-JIT
+ print "OK"
+ RUBY
+ Tempfile.create(['jit_refine', '.rb']) do |f|
+ f.write(script)
+ f.flush
+ out = jruby("-Xjit.threshold=10 -Xjit.background=false -Xjit.logging #{f.path} 2>&1")
+ assert_include out, "OK", "refinement-aware proc produced wrong results under JIT"
+ assert_not_include out, "JIT failed", "refinement-aware clone could not be JIT-compiled"
+ end
+ end
+end
diff --git a/test/mri/excludes/TC_Set.rb b/test/mri/excludes/TC_Set.rb
new file mode 100644
index 00000000000..0f74220d0aa
--- /dev/null
+++ b/test/mri/excludes/TC_Set.rb
@@ -0,0 +1,2 @@
+exclude :test_compare_by_identity_compact, "no GC.verify_compaction_references method"
+exclude :test_iteration_mutation_guards, "Set does not raise when mutated during iteration yet"
diff --git a/test/mri/excludes/TestClass.rb b/test/mri/excludes/TestClass.rb
index 934aef73bc6..5aade2f96c5 100644
--- a/test/mri/excludes/TestClass.rb
+++ b/test/mri/excludes/TestClass.rb
@@ -1,6 +1,5 @@
exclude :test_classext_memory_leak, "no working assert_no_memory_leak method"
exclude :test_clone_singleton_class_exists, "work in progress"
-exclude :test_constant_access_from_method_in_cloned_class, "work in progress"
exclude :test_method_redefinition, "missing a warning"
exclude :test_redefine_private_class, "needs investigation"
exclude :test_redefinition_mismatch, "missing original method location in warning"
diff --git a/test/mri/excludes/TestModule.rb b/test/mri/excludes/TestModule.rb
index 48294eace57..6fd8cfd0887 100644
--- a/test/mri/excludes/TestModule.rb
+++ b/test/mri/excludes/TestModule.rb
@@ -1,21 +1,20 @@
+exclude :test_alias_prepended_module_warning, "missing deprecation warning for aliasing a method defined in a prepended module"
exclude :test_ancestry_of_duped_classes, "handle along with optimizations as described in https://github.com/jruby/jruby/issues/8717"
exclude :test_bug17590, "work in progress"
-exclude :test_class_variable_in_dup_class, "work in progress"
exclude :test_complemented_method_entry_memory_leak, "no working assert_no_memory_leak method"
exclude :test_const_defined?, "needs investigation"
exclude :test_const_set_invalid_name, "UTF-16 invalid name is accepted as ok"
-exclude :test_constant_access_from_method_in_cloned_module, "work in progress"
exclude :test_define_module_under_private_constant, "we don't evaluate colon2-like class opening via colon2 logic, can't tell if private are ok"
exclude :test_deprecate_constant, "work in progress"
exclude :test_iclass_memory_leak, "no working assert_no_memory_leak method"
exclude :test_include_with_prepend, "work in progress"
exclude :test_initialize_copy, "work in progress"
-exclude :test_initialize_copy_empty, "needs investigation"
exclude :test_method_redefinition, "needs investigation"
exclude :test_module_clone_memory_leak, "no working assert_no_memory_leak method"
exclude :test_module_function_inside_method, "work in progress"
exclude :test_nested_defined, "needs investigation"
exclude :test_prepend_constant_lookup, "work in progress"
+exclude :test_prepend_super_in_alias, "missing deprecation warning for aliasing a method defined in a prepended module"
exclude :test_prepend_works_with_duped_classes, "work in progress"
exclude :test_private_constant_reopen, "needs investigation"
exclude :test_private_constant_with_no_args, "needs investigation"
diff --git a/test/mri/excludes/TestVariable.rb b/test/mri/excludes/TestVariable.rb
index cfe2671167f..fc8589981b6 100644
--- a/test/mri/excludes/TestVariable.rb
+++ b/test/mri/excludes/TestVariable.rb
@@ -1,4 +1,3 @@
-exclude :test_cloned_allows_setting_cvar, "needs investigation"
exclude :test_exivar_resize_with_compaction_stress, "depends on CRuby GC APIs"
exclude :test_global_variables, "requires dynamic listing of backref numbers for available groups"
exclude :test_setting_class_variable_on_module_through_inheritance, "work in progress"
diff --git a/test/mri/ruby/test_alias.rb b/test/mri/ruby/test_alias.rb
index 539cd494887..9b55c425ed8 100644
--- a/test/mri/ruby/test_alias.rb
+++ b/test/mri/ruby/test_alias.rb
@@ -208,8 +208,10 @@ def test_alias_in_module
begin;
bug = ARGV[0]
- m = Module.new do
- alias orig_to_s to_s
+ m = EnvUtil.suppress_warning do
+ Module.new do
+ alias orig_to_s to_s
+ end
end
o = Object.new.extend(m)
@@ -217,6 +219,30 @@ def test_alias_in_module
end;
end
+ def test_alias_fallback_to_object_is_deprecated
+ bug22276 = '[ruby-core:126537] [Bug #22276]'
+ message = /the fallback to Object for alias of 'to_s' in module '.*' is deprecated and will be removed in Ruby 4\.3/
+
+ m = nil
+ assert_deprecated_warning(message) do
+ m = Module.new { alias orig_to_s to_s }
+ end
+ o = Object.new.extend(m)
+ assert_equal(o.to_s, o.orig_to_s, bug22276)
+
+ assert_deprecated_warning(message) do
+ Module.new { alias_method :orig_to_s, :to_s }
+ end
+
+ assert_warning('', bug22276) do
+ EnvUtil.deprecation_warning do
+ Module.new { def foo; end; alias bar foo }
+ Module.new { include Comparable; alias in_range? between? }
+ Class.new { alias to_str to_s }
+ end
+ end
+ end
+
class C0; def foo; end; end
class C1 < C0; alias bar foo; end
diff --git a/test/mri/ruby/test_class.rb b/test/mri/ruby/test_class.rb
index 8f12e06685b..b328ec6db4e 100644
--- a/test/mri/ruby/test_class.rb
+++ b/test/mri/ruby/test_class.rb
@@ -435,6 +435,7 @@ class B; end
end
class CloneTest
+ TEST = :C0
def foo; TEST; end
end
@@ -448,8 +449,8 @@ class CloneTest2
end
def test_constant_access_from_method_in_cloned_class
- assert_equal :C1, CloneTest1.new.foo, '[Bug #15877]'
- assert_equal :C2, CloneTest2.new.foo, '[Bug #15877]'
+ assert_equal :C0, CloneTest1.new.foo, 'originally [Bug #15877], but behaviour changed'
+ assert_equal :C0, CloneTest2.new.foo, 'originally [Bug #15877], but behaviour changed'
end
def test_invalid_superclass
diff --git a/test/mri/ruby/test_module.rb b/test/mri/ruby/test_module.rb
index 30a7c5d9bc1..0b7f241325c 100644
--- a/test/mri/ruby/test_module.rb
+++ b/test/mri/ruby/test_module.rb
@@ -9,18 +9,18 @@ def _wrap_assertion
yield
end
- def assert_method_defined?(klass, mid, message="")
+ def assert_method_defined?(klass, (mid, *args), message="")
message = build_message(message, "#{klass}\##{mid} expected to be defined.")
_wrap_assertion do
- klass.method_defined?(mid) or
+ klass.method_defined?(mid, *args) or
raise Test::Unit::AssertionFailedError, message, caller(3)
end
end
- def assert_method_not_defined?(klass, mid, message="")
+ def assert_method_not_defined?(klass, (mid, *args), message="")
message = build_message(message, "#{klass}\##{mid} expected to not be defined.")
_wrap_assertion do
- klass.method_defined?(mid) and
+ klass.method_defined?(mid, *args) and
raise Test::Unit::AssertionFailedError, message, caller(3)
end
end
@@ -221,6 +221,83 @@ def test_ancestors
assert_equal([String, Comparable, Object, Kernel, BasicObject], String.ancestors - mixins)
end
+ def test_descendants
+ a = Module.new
+ b = Module.new { include a }
+ c = Class.new { include b }
+ d = Class.new(c)
+
+ order = [a, b, c, d]
+ assert_equal([b, c, d], a.descendants.sort_by {|m| order.index(m)})
+ assert_equal([c, d], b.descendants.sort_by {|m| order.index(m)})
+ assert_equal([d], c.descendants)
+ assert_equal([], d.descendants)
+
+ # prepend
+ pre = Module.new
+ e = Class.new { prepend pre }
+ assert_include(pre.descendants, e)
+
+ # diamond inclusion does not produce duplicates
+ f = Class.new { include a; include b }
+ descendants = a.descendants
+ assert_equal(descendants.uniq, descendants)
+ assert_include(descendants, f)
+
+ # include into a module propagates to existing includers
+ g = Module.new
+ h = Module.new
+ i = Class.new { include h }
+ h.include(g)
+ assert_include(g.descendants, h)
+ assert_include(g.descendants, i)
+
+ # singleton classes are excluded, even via Object#extend
+ obj = Object.new
+ obj.extend(a)
+ assert_not_include(a.descendants, obj.singleton_class)
+
+ # duality with Module#ancestors, except that the receiver is excluded
+ all = [a, b, c, d, pre, e, f, g, h, i]
+ all.each do |x|
+ dx = x.descendants
+ assert_not_include(dx, x)
+ all.each do |y|
+ next if x == y
+ assert_equal(y.ancestors.include?(x), dx.include?(y),
+ "#{x.inspect} vs #{y.inspect}")
+ end
+ end
+ end
+
+ def test_descendants_gc
+ a = Module.new
+ Class.new { include a }
+ Module.new { include a }
+ GC.start
+ descendants = a.descendants
+ assert_not_include(descendants, a)
+ # the anonymous descendants above may or may not have been collected
+ # already; what must hold is that no dead object is returned
+ descendants.each {|m| assert_kind_of(Module, m)}
+ end
+
+ def test_descendants_refinement
+ assert_separately([], <<-"end;")
+ class Target; def t; end; end
+ module M; end
+ refinement = nil
+ Module.new do
+ refinement = refine(Target) do
+ def t2; end
+ end
+ end
+ assert_equal([], Target.descendants)
+ assert_not_include(M.descendants, Target)
+ assert_not_include(Target.descendants, refinement)
+ end;
+ end
+
CLASS_EVAL = 2
@@class_eval = 'b'
@@ -583,7 +660,7 @@ def b; 2 end
end
def test_gc_prepend_chain
- assert_separately([], <<-EOS)
+ assert_ruby_status([], <<-EOS)
10000.times { |i|
m1 = Module.new do
def foo; end
@@ -813,40 +890,40 @@ def test_instance_methods
def test_method_defined?
[User, Class.new{include User}, Class.new{prepend User}].each do |klass|
[[], [true]].each do |args|
- assert !klass.method_defined?(:wombat, *args)
- assert klass.method_defined?(:mixin, *args)
- assert klass.method_defined?(:user, *args)
- assert klass.method_defined?(:user2, *args)
- assert !klass.method_defined?(:user3, *args)
+ assert_method_not_defined?(klass, [:wombat, *args])
+ assert_method_defined?(klass, [:mixin, *args])
+ assert_method_defined?(klass, [:user, *args])
+ assert_method_defined?(klass, [:user2, *args])
+ assert_method_not_defined?(klass, [:user3, *args])
- assert !klass.method_defined?("wombat", *args)
- assert klass.method_defined?("mixin", *args)
- assert klass.method_defined?("user", *args)
- assert klass.method_defined?("user2", *args)
- assert !klass.method_defined?("user3", *args)
+ assert_method_not_defined?(klass, ["wombat", *args])
+ assert_method_defined?(klass, ["mixin", *args])
+ assert_method_defined?(klass, ["user", *args])
+ assert_method_defined?(klass, ["user2", *args])
+ assert_method_not_defined?(klass, ["user3", *args])
end
end
end
def test_method_defined_without_include_super
- assert User.method_defined?(:user, false)
- assert !User.method_defined?(:mixin, false)
- assert Mixin.method_defined?(:mixin, false)
+ assert_method_defined?(User, [:user, false])
+ assert_method_not_defined?(User, [:mixin, false])
+ assert_method_defined?(Mixin, [:mixin, false])
User.const_set(:FOO, c = Class.new)
c.prepend(User)
- assert !c.method_defined?(:user, false)
+ assert_method_not_defined?(c, [:user, false])
c.define_method(:user){}
- assert c.method_defined?(:user, false)
+ assert_method_defined?(c, [:user, false])
- assert !c.method_defined?(:mixin, false)
+ assert_method_not_defined?(c, [:mixin, false])
c.define_method(:mixin){}
- assert c.method_defined?(:mixin, false)
+ assert_method_defined?(c, [:mixin, false])
- assert !c.method_defined?(:userx, false)
+ assert_method_not_defined?(c, [:userx, false])
c.define_method(:userx){}
- assert c.method_defined?(:userx, false)
+ assert_method_defined?(c, [:userx, false])
# cleanup
User.class_eval do
@@ -1510,6 +1587,30 @@ def bar; 2; end
INPUT
end
+ def test_alias_prepended_module_warning
+ assert_in_out_err([], <<-INPUT, [], /aliasing C#foo defined in a prepended module M is deprecated/)
+ Warning[:deprecated] = true
+ module M
+ def foo = :foo
+ end
+ class C
+ prepend M
+ alias bar foo
+ end
+ INPUT
+
+ assert_in_out_err([], <<-INPUT, [], /aliasing C#foo defined in a prepended module M is deprecated/)
+ Warning[:deprecated] = true
+ module M
+ def foo = :foo
+ end
+ class C
+ prepend M
+ alias_method :bar, :foo
+ end
+ INPUT
+ end
+
def test_mod_constants
m = Module.new
m.const_set(:Foo, :foo)
@@ -2704,7 +2805,9 @@ def m; "A"; end
def m; "B"+super; end
alias m2 m
prepend p
- alias m3 m
+ end
+ assert_deprecated_warning(/aliasing .*#m defined in a prepended module .* is deprecated/) do
+ b.class_eval { alias m3 m }
end
assert_equal("BA", b.new.m2, bug7842)
assert_equal("PBA", b.new.m3, bug7842)
@@ -2822,7 +2925,7 @@ def a
b = a.dup
b.new.a = 'B'
- assert_equal 'A', a.new.a, '[ruby-core:17019]'
+ assert_equal 'B', a.new.a, '[ruby-core:17019] behaviour changed: cvar resolves through original CREF'
end
Bug6891 = '[ruby-core:47241]'
@@ -3073,7 +3176,7 @@ def test_return_value_of_define_singleton_method
end
def test_prepend_gc
- assert_separately [], %{
+ assert_ruby_status [], %{
module Foo
end
class Object
@@ -3265,15 +3368,18 @@ def self.m; C; end
end
module CloneTestM0
+ TEST = :M0
def foo; TEST; end
end
CloneTestM1 = CloneTestM0.clone
CloneTestM2 = CloneTestM0.clone
module CloneTestM1
+ remove_const :TEST
TEST = :M1
end
module CloneTestM2
+ remove_const :TEST
TEST = :M2
end
class CloneTestC1
@@ -3288,8 +3394,8 @@ def test_constant_access_from_method_in_cloned_module
assert_equal 1, m::C, '[ruby-core:47834]'
assert_equal 1, m.m, '[ruby-core:47834]'
- assert_equal :M1, CloneTestC1.new.foo, '[Bug #15877]'
- assert_equal :M2, CloneTestC2.new.foo, '[Bug #15877]'
+ assert_equal :M0, CloneTestC1.new.foo, 'originally [Bug #15877], but behaviour changed'
+ assert_equal :M0, CloneTestC2.new.foo, 'originally [Bug #15877], but behaviour changed'
end
def test_clone_freeze
@@ -3298,6 +3404,23 @@ def test_clone_freeze
assert_not_predicate m.clone(freeze: false), :frozen?
end
+ def test_dup_of_frozen_module_is_not_frozen
+ m = Module.new
+ m.instance_variable_set(:@a, 1)
+ m.freeze
+
+ copy = m.dup
+ assert_not_predicate(copy, :frozen?)
+ copy.instance_variable_set(:@b, 2)
+ assert_equal([1, 2], [copy.instance_variable_get(:@a), copy.instance_variable_get(:@b)])
+ end
+
+ def test_singleton_class_of_frozen_module_is_frozen
+ m = Module.new.freeze
+ assert_predicate(m.singleton_class, :frozen?)
+ assert_raise(FrozenError) {m.singleton_class.instance_variable_set(:@a, 1)}
+ end
+
def test_module_name_in_singleton_method
s = Object.new.singleton_class
mod = s.const_set(:Foo, Module.new)
diff --git a/test/mri/ruby/test_objectspace.rb b/test/mri/ruby/test_objectspace.rb
index a479547599a..033ad0f0de5 100644
--- a/test/mri/ruby/test_objectspace.rb
+++ b/test/mri/ruby/test_objectspace.rb
@@ -2,77 +2,6 @@
require 'test/unit'
class TestObjectSpace < Test::Unit::TestCase
- def self.deftest_id2ref(obj)
- /:(\d+)/ =~ caller[0]
- file = $`
- line = $1.to_i
- code = <<"End"
- define_method("test_id2ref_#{line}") {\
- o = EnvUtil.suppress_warning { ObjectSpace._id2ref(obj.object_id) }
- assert_same(obj, o, "didn't round trip: \#{obj.inspect}");\
- }
-End
- eval code, binding, file, line
- end
-
- deftest_id2ref(-0x4000000000000001)
- deftest_id2ref(-0x4000000000000000)
- deftest_id2ref(-0x40000001)
- deftest_id2ref(-0x40000000)
- deftest_id2ref(-1)
- deftest_id2ref(0)
- deftest_id2ref(1)
- deftest_id2ref(0x3fffffff)
- deftest_id2ref(0x40000000)
- deftest_id2ref(0x3fffffffffffffff)
- deftest_id2ref(0x4000000000000000)
- deftest_id2ref(:a)
- deftest_id2ref(:abcdefghijilkjl)
- deftest_id2ref(:==)
- deftest_id2ref(Object.new)
- deftest_id2ref(self)
- deftest_id2ref(true)
- deftest_id2ref(false)
- deftest_id2ref(nil)
-
- def test_id2ref_liveness
- assert_normal_exit <<-EOS
- ids = []
- 10.times{
- 1_000.times{
- ids << 'hello'.object_id
- }
- objs = ids.map{|id|
- begin
- ObjectSpace._id2ref(id)
- rescue RangeError
- nil
- end
- }
- GC.start
- objs.each{|e| e.inspect}
- }
- EOS
- end
-
- def test_id2ref_invalid_argument
- msg = /no implicit conversion/
- assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(nil) } }
- assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(false) } }
- assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(true) } }
- assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(:a) } }
- assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref("0") } }
- assert_raise_with_message(TypeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(Object.new) } }
- end
-
- def test_id2ref_invalid_symbol_id
- # RB_STATIC_SYM_P checks for static symbols by checking that the bottom
- # 8 bits of the object is equal to RUBY_SYMBOL_FLAG, so we need to make
- # sure that the bottom 8 bits remain unchanged.
- msg = /is not a symbol id value/
- assert_raise_with_message(RangeError, msg) { EnvUtil.suppress_warning { ObjectSpace._id2ref(:a.object_id + 256) } }
- end
-
def test_count_objects
h = {}
ObjectSpace.count_objects(h)
@@ -284,21 +213,6 @@ def test_each_object_recursive_key
end;
end
- def test_id2ref_table_build
- assert_separately([], <<-End)
- 10.times do
- Object.new.object_id
- end
-
- GC.start(immediate_mark: false)
-
- obj = Object.new
- EnvUtil.suppress_warning do
- assert_equal obj, ObjectSpace._id2ref(obj.object_id)
- end
- End
- end
-
def test_each_object_singleton_class
assert_separately([], <<-End)
class C
diff --git a/test/mri/ruby/test_range.rb b/test/mri/ruby/test_range.rb
index 7f097903d14..1b29396a0f0 100644
--- a/test/mri/ruby/test_range.rb
+++ b/test/mri/ruby/test_range.rb
@@ -1553,4 +1553,70 @@ def test_overlap?
assert_not_operator((1...3), :overlap?, (3..4))
assert_not_operator((...3), :overlap?, (3..))
end
+
+ def test_clamp
+ # Clamp begin and end to min and max.
+ assert_equal(3..7, (1..10).clamp(3, 7))
+ assert_equal(3..7, (..10).clamp(3, 7))
+
+ # Clamp begin and end to a range.
+ assert_equal(3..7, (1..10).clamp(3..7))
+ assert_equal(3..5, (1..5).clamp(3...7))
+ assert_equal(3...7, (1..10).clamp(DuckRange.new(3, 7, true)))
+
+ # Treat min and max like an inclusive range.
+ assert_equal((1...10).clamp(3..7), (1...10).clamp(3, 7))
+ assert_equal((1...10).clamp(3..10), (1...10).clamp(3, 10))
+
+ # Exclude the end when the returned end is excluded by self or the
+ # argument range.
+ assert_equal(3...10, (1...10).clamp(3, 10))
+ assert_equal(3...10, (1...10).clamp(3..10))
+ assert_equal(3...10, (1..10).clamp(3...10))
+ assert_equal(3...10, (..10).clamp(3...10))
+
+ # Include the end when the returned end is not an excluded end.
+ assert_equal(3..7, (1...10).clamp(3, 7))
+ assert_equal(0..10, (0...).clamp(0, 10))
+ assert_equal(3..5, (1..5).clamp(3...7))
+ assert_equal(3..5, (..5).clamp(3...7))
+ assert_equal(3..10, (1..10).clamp(3...))
+
+ # Handle beginless and endless argument ranges.
+ assert_equal(1..7, (1..10).clamp(..7))
+ assert_equal(1...7, (1..10).clamp(...7))
+ assert_equal(1..5, (1..5).clamp(...7))
+
+ assert_equal(3..10, (1..10).clamp(3..))
+ assert_equal(3.., (1..).clamp(3..))
+ assert_equal(3..., (1...).clamp(3...))
+
+ # Return an inclusive point range when begin and end are clamped to
+ # different equal bounds.
+ assert_equal(3..3, (1..10).clamp(3, 3))
+ assert_equal(3..3, (1..10).clamp(3..3))
+
+ # Return an empty range when begin and end are clamped to the same side.
+ assert_equal(20...20, (1..10).clamp(20, 30))
+ assert_equal(0...0, (1..10).clamp(-10, 0))
+ assert_equal(20...20, (..10).clamp(20, 30))
+ assert_equal(20...20, (1..10).clamp(20..30))
+ assert_equal(0...0, (1..10).clamp(-10..0))
+ assert_equal(20...20, (..10).clamp(20..30))
+ assert_equal(3...3, (1..2).clamp(3, 3))
+ assert_equal(3...3, (4..10).clamp(3, 3))
+ assert_equal(0...0, (1..).clamp(nil, 0))
+ assert_equal(0...0, (1..).clamp(..0))
+
+ # Return a Range instance, not an instance of a subclass.
+ subclass = Class.new(Range)
+ assert_instance_of(Range, subclass.new(1, 10).clamp(3, 7))
+
+ # Raise for invalid bounds or argument types.
+ assert_raise(ArgumentError) {(1..3).clamp(1, "z")}
+ assert_raise(ArgumentError) {(1..3).clamp("a", "z")}
+ assert_raise(ArgumentError) {(1..3).clamp(2, 1)}
+ assert_raise(ArgumentError) {(1..3).clamp(2..1)}
+ assert_raise(TypeError) {(1..3).clamp(1)}
+ end
end
diff --git a/test/mri/ruby/test_set.rb b/test/mri/ruby/test_set.rb
index 70a61aa3b5d..c1135c86ce3 100644
--- a/test/mri/ruby/test_set.rb
+++ b/test/mri/ruby/test_set.rb
@@ -687,6 +687,18 @@ def test_and
ret = set & [2,4,6]
assert_not_same(set, ret)
assert_equal(Set[2,4], ret)
+
+ assert_separately([], "#{<<-"{#"}\n#{<<-'};'}")
+ {#
+ class << (obj = Object.new)
+ include Enumerable
+ attr_reader :block
+ def each(&block) = (@block = block)
+ end
+ assert_equal(Set[], Set[1, 2, 3] & obj)
+ GC.start
+ assert_nil(obj.block.call(1))
+ };
end
def test_xor
@@ -700,6 +712,32 @@ def test_xor
ret2 = set2 ^ [2,4,5,5]
assert_instance_of(klass, ret2)
assert_equal(klass[1,3,5], ret2)
+
+ # Set ^ Set should not mutate either operand and should be symmetric
+ a = klass[1,2,3,4]
+ b = klass[2,4,5]
+ a_copy = a.dup
+ b_copy = b.dup
+ ret3 = a ^ b
+ ret4 = b ^ a
+ assert_equal(klass[1,3,5], ret3)
+ assert_equal(ret3, ret4)
+ assert_equal(a_copy, a)
+ assert_equal(b_copy, b)
+ assert_instance_of(klass, ret3)
+ assert_instance_of(klass, ret4)
+
+ # compare_by_identity should keep distinct objects from enumerable RHS
+ iset = Set.new.compare_by_identity
+ x1 = +"x"
+ x2 = +"x"
+ ret5 = iset ^ [x1, x2]
+ assert_equal(2, ret5.size)
+ arr = ret5.to_a
+ assert_include(arr, x1)
+ assert_include(arr, x2)
+ assert_not_predicate(x1, :frozen?)
+ assert_not_predicate(x2, :frozen?)
}
end
@@ -902,6 +940,25 @@ def test_compare_by_identity
assert_equal(array.uniq.sort, set.sort)
end
+ def test_compare_by_identity_compact
+ omit "compaction is not supported on this platform" unless GC.respond_to?(:compact)
+
+ # [Bug #22064]
+ assert_separately([], "#{<<~"begin;"}\n#{<<~'end;'}")
+ begin;
+ set = Set.new.compare_by_identity
+
+ o = Object.new
+ set.add(o)
+
+ assert_include(set, o)
+
+ GC.verify_compaction_references(expand_heap: true, toward: :empty)
+
+ assert_include(set, o)
+ end;
+ end
+
def test_reset
[Set, Class.new(Set)].each { |klass|
a = [1, 2]
@@ -939,7 +996,7 @@ def test_larger_sets
set = set.dup
10_000.times do |i|
- assert_includes set, i
+ assert_include set, i
end
end
@@ -963,6 +1020,95 @@ def initialize(enum)
assert_equal([2, 1], c[2].to_a)
end
+ def test_iteration_mutation_guards
+ set = Set[1, 2]
+ assert_raise(RuntimeError) { set.each { set.add(3) } }
+ assert_raise(RuntimeError) { set.each { set.merge([3]) } }
+ assert_raise(RuntimeError) { set.each { set.compare_by_identity } }
+ assert_raise(RuntimeError) { set.each { set.reset } }
+ end
+
+ def test_string_freezing_semantics
+ s = "foo"
+ set = Set.new
+ set << s
+ stored = set.to_a.first
+ assert_not_same(s, stored)
+ assert_predicate(stored, :frozen?)
+
+ s2 = "bar"
+ iset = Set.new.compare_by_identity
+ iset << s2
+ stored2 = iset.to_a.first
+ assert_same(s2, stored2)
+ assert_not_predicate(stored2, :frozen?)
+
+ sf = "baz".freeze
+ set2 = Set.new
+ set2 << sf
+ assert_same(sf, set2.to_a.first)
+ end
+
+ def test_compare_by_identity_hash_and_eq
+ a1, a2 = "a", "a"
+ set_default1 = Set[a1]
+ set_default2 = Set[a2]
+ assert_equal(set_default1, set_default2)
+ assert_equal(set_default1.hash, set_default2.hash)
+
+ iset = Set.new.compare_by_identity
+ iset.merge([a1, a2])
+ assert_not_equal(set_default1, iset)
+ refute_equal(set_default1.hash, iset.hash)
+ end
+
+ def test_insertion_order_preserved
+ set = Set[3, 1, 2]
+ assert_equal([3, 1, 2], set.to_a)
+
+ iset = Set.new.compare_by_identity
+ iset.merge([3, 1, 2])
+ assert_equal([3, 1, 2], iset.to_a)
+ end
+
+ def test_compare_by_identity_preservation
+ iset = Set.new.compare_by_identity
+
+ # 1. Intersection (&)
+ assert_predicate(iset & [1, 2], :compare_by_identity?)
+
+ # 2. Collect! / Map!
+ iset_collect = Set["a", "b"].compare_by_identity
+ iset_collect.collect! { |x| x }
+ assert_predicate(iset_collect, :compare_by_identity?)
+
+ # 3. Flatten / Flatten!
+ iset_flat = Set[Set[1, 2]].compare_by_identity
+ assert_predicate(iset_flat.flatten, :compare_by_identity?)
+ iset_flat.flatten!
+ assert_predicate(iset_flat, :compare_by_identity?)
+
+ # 4. Classify
+ iset_classify = Set["a", "b"].compare_by_identity
+ classified = iset_classify.classify { |x| x }
+ classified.each_value do |subset|
+ assert_predicate(subset, :compare_by_identity?)
+ end
+
+ # 5. Divide (arity 1 and arity 2)
+ iset_divide1 = Set["a", "b"].compare_by_identity
+ divided1 = iset_divide1.divide { |x| x }
+ divided1.each do |subset|
+ assert_predicate(subset, :compare_by_identity?)
+ end
+
+ iset_divide2 = Set["a", "b"].compare_by_identity
+ divided2 = iset_divide2.divide { |x, y| x == y }
+ divided2.each do |subset|
+ assert_predicate(subset, :compare_by_identity?)
+ end
+ end
+
end
class TC_Enumerable < Test::Unit::TestCase
@@ -978,7 +1124,8 @@ def test_to_set
assert_equal([-10,-8,-6,-4,-2], set.sort)
assert_same set, set.to_set
- assert_not_same set, set.to_set { |o| o }
+ transformed = set.to_set { |o| o + 1 }
+ assert_equal([-9,-7,-5,-3,-1], transformed.sort)
end
class MyEnum
diff --git a/test/mri/ruby/test_settracefunc.rb b/test/mri/ruby/test_settracefunc.rb
index 776534a2b54..95fc7f45536 100644
--- a/test/mri/ruby/test_settracefunc.rb
+++ b/test/mri/ruby/test_settracefunc.rb
@@ -26,6 +26,19 @@ def target_thread?
Thread.current == @target_thread
end
+ # Reject trace events from other code interrupting this thread, such as
+ # finalizers of objects left by other tests (e.g. Tempfile's), whose
+ # frames are pushed on top of the interrupted frame of this file. An
+ # event is ours iff the innermost frame, ignoring core methods written
+ # in Ruby ( such as Kernel#tap), belongs to this file.
+ #
+ # +locations+ must be the caller_locations captured directly in the
+ # trace handler; capturing it here would add this method's own frame.
+ def event_from_this_file?(locations)
+ innermost = locations.drop_while{|loc| loc.path.start_with?("