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?("