From e12fa3cf708020f5ba78612bab83aa714d9be6a3 Mon Sep 17 00:00:00 2001 From: kares Date: Sun, 17 Sep 2017 08:29:27 +0200 Subject: [PATCH 01/16] a well working (first) draft of rewritten native Set impl (based on set.rb) --- core/src/main/java/org/jruby/Ruby.java | 1 + core/src/main/java/org/jruby/RubyModule.java | 3 +- .../java/org/jruby/ext/set/EnumerableExt.java | 63 + .../main/java/org/jruby/ext/set/RubySet.java | 1089 +++++++++++++++++ .../java/org/jruby/ext/set/RubySortedSet.java | 72 ++ .../java/org/jruby/ext/set/SetLibrary.java | 45 + 6 files changed, 1272 insertions(+), 1 deletion(-) create mode 100644 core/src/main/java/org/jruby/ext/set/EnumerableExt.java create mode 100644 core/src/main/java/org/jruby/ext/set/RubySet.java create mode 100644 core/src/main/java/org/jruby/ext/set/RubySortedSet.java create mode 100644 core/src/main/java/org/jruby/ext/set/SetLibrary.java diff --git a/core/src/main/java/org/jruby/Ruby.java b/core/src/main/java/org/jruby/Ruby.java index a983ecbbdb7..c503a7cd69f 100644 --- a/core/src/main/java/org/jruby/Ruby.java +++ b/core/src/main/java/org/jruby/Ruby.java @@ -1719,6 +1719,7 @@ private void initBuiltins() { addLazyBuiltin("tempfile.jar", "tempfile", "org.jruby.ext.tempfile.TempfileLibrary"); addLazyBuiltin("fcntl.rb", "fcntl", "org.jruby.ext.fcntl.FcntlLibrary"); addLazyBuiltin("pathname.jar", "pathname", "org.jruby.ext.pathname.PathnameLibrary"); + addLazyBuiltin("set.rb", "set", "org.jruby.ext.set.SetLibrary"); addLazyBuiltin("mathn/complex.jar", "mathn/complex", "org.jruby.ext.mathn.Complex"); addLazyBuiltin("mathn/rational.jar", "mathn/rational", "org.jruby.ext.mathn.Rational"); diff --git a/core/src/main/java/org/jruby/RubyModule.java b/core/src/main/java/org/jruby/RubyModule.java index 4581f87a489..feda5ba7bf8 100644 --- a/core/src/main/java/org/jruby/RubyModule.java +++ b/core/src/main/java/org/jruby/RubyModule.java @@ -3522,7 +3522,8 @@ public IRubyObject prepended(ThreadContext context, IRubyObject other) { return context.nil; } - final void setConstantVisibility(Ruby runtime, String name, boolean hidden) { + // NOTE: internal API + public final void setConstantVisibility(Ruby runtime, String name, boolean hidden) { ConstantEntry entry = getConstantMap().get(name); if (entry == null) { diff --git a/core/src/main/java/org/jruby/ext/set/EnumerableExt.java b/core/src/main/java/org/jruby/ext/set/EnumerableExt.java new file mode 100644 index 00000000000..299c8781092 --- /dev/null +++ b/core/src/main/java/org/jruby/ext/set/EnumerableExt.java @@ -0,0 +1,63 @@ +/***** BEGIN LICENSE BLOCK ***** + * Version: EPL 1.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 1.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v10.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Copyright (C) 2016 Karol Bucek + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ +package org.jruby.ext.set; + +import org.jruby.Ruby; +import org.jruby.RubyClass; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.Block; +import org.jruby.runtime.ThreadContext; +import org.jruby.runtime.builtin.IRubyObject; + +/** + * Enumerable#to_set (from require 'set') + * + * @author kares + */ +public abstract class EnumerableExt { + + //@JRubyMethod + public static IRubyObject to_set(final ThreadContext context, final IRubyObject self, final Block block) { + final Ruby runtime = context.runtime; + + RubySet set = new RubySet(runtime, runtime.getClass("Set")); + 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 new file mode 100644 index 00000000000..37f24683c34 --- /dev/null +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -0,0 +1,1089 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 1.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 1.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v10.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Copyright (C) 2016 Karol Bucek + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ +package org.jruby.ext.set; + +import org.jcodings.specific.USASCIIEncoding; +import org.jruby.*; +import org.jruby.anno.JRubyMethod; +import org.jruby.common.IRubyWarnings; +import org.jruby.runtime.*; +import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.util.ArraySupport; + +import java.util.Collection; +import java.util.IdentityHashMap; +import java.util.Iterator; +import java.util.Set; + +import static org.jruby.RubyEnumerator.enumeratorizeWithSize; + +/** + * Native implementation of Ruby's Set (set.rb replacement). + * + * @author kares + */ +@org.jruby.anno.JRubyClass(name="Set", include = { "Enumerable" }) +public class RubySet extends RubyObject { // implements Set { + + static RubyClass createSetClass(final Ruby runtime) { + RubyClass Set = runtime.defineClass("Set", runtime.getObject(), ALLOCATOR); + + Set.setReifiedClass(RubySet.class); + + Set.includeModule(runtime.getEnumerable()); + Set.defineAnnotatedMethods(RubySet.class); + + return Set; + } + + private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { + public RubySet allocate(Ruby runtime, RubyClass klass) { + return new RubySet(runtime, klass); + } + }; + + RubyHash hash; // @hash + + protected RubySet(Ruby runtime, RubyClass klass) { + super(runtime, klass); + } + + /* + private RubySet(Ruby runtime, RubyHash hash) { + super(runtime, runtime.getClass("Set")); + initHash(hash); + } */ + + final void initHash(final Ruby runtime) { + initHash(new RubyHash(runtime)); + } + + final void initHash(final Ruby runtime, final int size) { + initHash(new RubyHash(runtime, size)); + } + + final void initHash(final RubyHash hash) { + this.hash = hash; + setInstanceVariable("@hash", hash); // MRI compat with set.rb + } + + RubySet newSet(final Ruby runtime) { + RubySet set = new RubySet(runtime, getMetaClass()); + set.initHash(runtime); return set; + } + + /** + * Creates a new set containing the given objects. + */ + @JRubyMethod(name = "[]", rest = true, meta = true) // def self.[](*ary) + public static RubySet create(final ThreadContext context, IRubyObject self, IRubyObject... ary) { + final Ruby runtime = context.runtime; + + RubySet set = new RubySet(runtime, (RubyClass) self); + set.initHash(runtime, Math.max(4, ary.length)); + for ( int i=0; i=" }) + public IRubyObject superset_p(final ThreadContext context, IRubyObject set) { + if ( set instanceof RubySet ) { + if ( getMetaClass().isInstance(set) ) { + return this.hash.op_ge(context, ((RubySet) set).hash); + } + // size >= set.size && set.all? { |o| include?(o) } + return context.runtime.newBoolean( + size() >= ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + ); + } + throw context.runtime.newArgumentError("value must be a set"); + } + + // Returns true if the set is a proper superset of the given set. + @JRubyMethod(name = "proper_superset?", alias = { ">" }) + public IRubyObject proper_superset_p(final ThreadContext context, IRubyObject set) { + if ( set instanceof RubySet ) { + if ( getMetaClass().isInstance(set) ) { + return this.hash.op_gt(context, ((RubySet) set).hash); + } + // size >= set.size && set.all? { |o| include?(o) } + return context.runtime.newBoolean( + size() > ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + ); + } + throw context.runtime.newArgumentError("value must be a set"); + } + + @JRubyMethod(name = "subset?", alias = { "<=" }) + public IRubyObject subset_p(final ThreadContext context, IRubyObject set) { + if ( set instanceof RubySet ) { + if ( getMetaClass().isInstance(set) ) { + return this.hash.op_le(context, ((RubySet) set).hash); + } + // size >= set.size && set.all? { |o| include?(o) } + return context.runtime.newBoolean( + size() <= ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + ); + } + throw context.runtime.newArgumentError("value must be a set"); + } + + @JRubyMethod(name = "proper_subset?", alias = { "<" }) + public IRubyObject proper_subset_p(final ThreadContext context, IRubyObject set) { + if ( set instanceof RubySet ) { + if ( getMetaClass().isInstance(set) ) { + return this.hash.op_lt(context, ((RubySet) set).hash); + } + // size >= set.size && set.all? { |o| include?(o) } + return context.runtime.newBoolean( + size() < ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + ); + } + throw context.runtime.newArgumentError("value must be a set"); + } + + /** + * Returns true if the set and the given set have at least one element in common. + */ + @JRubyMethod(name = "intersect?") + public IRubyObject intersect_p(final ThreadContext context, IRubyObject set) { + if ( set instanceof RubySet ) { + return context.runtime.newBoolean( intersect(context, (RubySet) set) ); + } + throw context.runtime.newArgumentError("value must be a set"); + } + + public boolean intersect(final ThreadContext context, final RubySet set) { + if ( size() < set.size() ) { + // any? { |o| set.include?(o) } + for ( IRubyObject o : elements() ) { + if ( set.contains(context, o) ) return true; + } + } + else { + // set.any? { |o| include?(o) } + for ( IRubyObject o : set.elements() ) { + if ( contains(context, o) ) return true; + } + } + return false; + } + + /** + * Returns true if the set and the given set have no element in common. + * This method is the opposite of +intersect?+. + */ + @JRubyMethod(name = "disjoint?") + public IRubyObject disjoint_p(final ThreadContext context, IRubyObject set) { + if ( set instanceof RubySet ) { + return context.runtime.newBoolean( ! intersect(context, (RubySet) set) ); + } + throw context.runtime.newArgumentError("value must be a set"); + } + + @JRubyMethod + public IRubyObject each(final ThreadContext context, Block block) { + if ( ! block.isGiven() ) { + return enumeratorizeWithSize(context, this, "each", enumSize()); + } + + for (IRubyObject elem : elements()) block.yield(context, elem); + return this; + } + + private RubyEnumerator.SizeFn enumSize() { + return new RubyEnumerator.SizeFn() { + @Override + public IRubyObject size(IRubyObject[] args) { + return getRuntime().newFixnum( RubySet.this.size() ); + } + }; + } + + /** + * Adds the given object to the set and returns self. + */ + @JRubyMethod(name = "add", alias = "<<") + public RubySet add(final ThreadContext context, IRubyObject obj) { + modifyCheck(context.runtime); + addObject(context.runtime, obj); + return this; + } + + protected void addObject(final Ruby runtime, final IRubyObject obj) { + hash.fastASetCheckString(runtime, obj, runtime.getTrue()); // @hash[obj] = true + } + + /** + * Adds the given object to the set and returns self. If the object is already in the set, returns nil. + */ + @JRubyMethod(name = "add?") + public IRubyObject add_p(final ThreadContext context, IRubyObject obj) { + // add(o) unless include?(o) + if ( contains(context, obj) ) return context.nil; + return add(context, obj); + } + + @JRubyMethod + public IRubyObject delete(final ThreadContext context, IRubyObject obj) { + modifyCheck(context.runtime); + deleteObject(obj); + return this; + } + + private void deleteObject(final IRubyObject obj) { + hash.modify(); + hash.fastDelete(obj); + } + + /** + * Deletes the given object from the set and returns self. If the object is not in the set, returns nil. + */ + @JRubyMethod(name = "delete?") + public IRubyObject delete_p(final ThreadContext context, IRubyObject obj) { + // delete(o) if include?(o) + if ( ! contains(context, obj) ) return context.nil; + return delete(context, obj); + } + + @JRubyMethod + public IRubyObject delete_if(final ThreadContext context, Block block) { + if ( ! block.isGiven() ) { + return enumeratorizeWithSize(context, this, "delete_if", enumSize()); + } + + Iterator it = elements().iterator(); + while ( it.hasNext() ) { + IRubyObject elem = it.next(); + if ( block.yield(context, elem).isTrue() ) it.remove(); + } + return this; + } + + @JRubyMethod + public IRubyObject keep_if(final ThreadContext context, Block block) { + if ( ! block.isGiven() ) { + return enumeratorizeWithSize(context, this, "keep_if", enumSize()); + } + + Iterator it = elements().iterator(); + while ( it.hasNext() ) { + IRubyObject elem = it.next(); + if ( ! block.yield(context, elem).isTrue() ) it.remove(); + } + return this; + } + + @JRubyMethod(name = "collect!", alias = "map!") + public IRubyObject collect_bang(final ThreadContext context, Block block) { + if ( ! block.isGiven() ) { + return enumeratorizeWithSize(context, this, "collect!", enumSize()); + } + + final RubyArray elems = to_a(context); clearImpl(); + for ( int i=0; i it = elements().iterator(); + while ( it.hasNext() ) { + IRubyObject elem = it.next(); + if ( block.yield(context, elem).isTrue() ) it.remove(); + } + return size == size() ? context.nil : this; + } + + // Equivalent to Set#keep_if, but returns nil if no changes were made. + @JRubyMethod(name = "select!") + public IRubyObject select_bang(final ThreadContext context, Block block) { + if ( ! block.isGiven() ) { + return enumeratorizeWithSize(context, this, "select!", enumSize()); + } + + final int size = size(); + Iterator it = elements().iterator(); + while ( it.hasNext() ) { + IRubyObject elem = it.next(); + if ( ! block.yield(context, elem).isTrue() ) it.remove(); + } + return size == size() ? context.nil : this; + } + + /** + * Merges the elements of the given enumerable object to the set and returns self. + */ + @JRubyMethod(name = "merge") + public RubySet rb_merge(final ThreadContext context, IRubyObject enume) { + final Ruby runtime = context.runtime; + + if ( enume instanceof RubySet ) { + modifyCheck(runtime); + // NOTE: MRI cheats - does not call Set#add thus we do not care ... + hash.merge_bang(context, ((RubySet) enume).hash, Block.NULL_BLOCK); + } + else if ( enume instanceof RubyArray ) { + modifyCheck(runtime); + RubyArray ary = (RubyArray) enume; + for ( int i = 0; i < ary.size(); i++ ) { + addObject(runtime, ary.eltInternal(i)); + } + } + else { // do_with_enum(enum) { |o| add(o) } + doWithEnum(context, enume, new EachBody(runtime) { + IRubyObject yieldImpl(ThreadContext context, IRubyObject val) { + addObject(context.runtime, val); return context.nil; + } + }); + } + + return this; + } + + /** + * Deletes every element that appears in the given enumerable object and returns self. + */ + @JRubyMethod(name = "subtract") + public IRubyObject subtract(final ThreadContext context, IRubyObject enume) { + final Ruby runtime = context.runtime; + + if ( enume instanceof RubySet ) { + modifyCheck(runtime); + for ( IRubyObject elem : ((RubySet) enume).elements() ) { + deleteObject(elem); + } + } + else if ( enume instanceof RubyArray ) { + modifyCheck(runtime); + RubyArray ary = (RubyArray) enume; + for ( int i = 0; i < ary.size(); i++ ) { + deleteObject(ary.eltInternal(i)); + } + } + else { // do_with_enum(enum) { |o| delete(o) } + doWithEnum(context, enume, new EachBody(runtime) { + IRubyObject yieldImpl(ThreadContext context, IRubyObject val) { + deleteObject(val); return context.nil; + } + }); + } + + return this; + } + + /** + * Returns a new set built by merging the set and the elements of the given enumerable object. + */ + @JRubyMethod(name = "|", alias = { "+", "union" }) + public IRubyObject op_or(final ThreadContext context, IRubyObject enume) { + return ((RubySet) dup()).rb_merge(context, enume); // dup.merge(enum) + } + + /** + * Returns a new set built by duplicating the set, removing every element that appears in the given enumerable object. + */ + @JRubyMethod(name = "-", alias = { "difference" }) + public IRubyObject op_diff(final ThreadContext context, IRubyObject enume) { + return ((RubySet) dup()).subtract(context, enume); + } + + /** + * Returns a new set built by merging the set and the elements of the given enumerable object. + */ + @JRubyMethod(name = "&", alias = { "intersection" }) + public IRubyObject op_and(final ThreadContext context, IRubyObject enume) { + final Ruby runtime = context.runtime; + + final RubySet newSet = new RubySet(runtime, getMetaClass()); + if ( enume instanceof RubySet ) { + newSet.initHash(runtime, ((RubySet) enume).size()); + for ( IRubyObject obj : ((RubySet) enume).elements() ) { + if ( contains(context, obj) ) newSet.addObject(runtime, obj); + } + } + else if ( enume instanceof RubyArray ) { + RubyArray ary = (RubyArray) enume; + newSet.initHash(runtime, ary.size()); + for ( int i = 0; i < ary.size(); i++ ) { + final IRubyObject obj = ary.eltInternal(i); + if ( contains(context, obj) ) newSet.addObject(runtime, obj); + } + } + else { + newSet.initHash(runtime); + // do_with_enum(enum) { |o| newSet.add(o) if include?(o) } + doWithEnum(context, enume, new EachBody(runtime) { + IRubyObject yieldImpl(ThreadContext context, IRubyObject obj) { + if ( contains(context, obj) ) newSet.addObject(runtime, obj); + return context.nil; + } + }); + } + + return newSet; + } + + /** + * Returns a new set containing elements exclusive between the set and the given enumerable object. + * `(set ^ enum)` is equivalent to `((set | enum) - (set & enum))`. + */ + @JRubyMethod(name = "^") + public IRubyObject op_xor(final ThreadContext context, IRubyObject enume) { + final Ruby runtime = context.runtime; + + RubySet newSet = new RubySet(runtime, runtime.getClass("Set")); + newSet.initialize(context, enume, Block.NULL_BLOCK); // Set.new(enum) + for ( IRubyObject o : elements() ) { + if ( newSet.contains(context, o) ) newSet.deleteObject(o); // exclusive or + else newSet.addObject(runtime, o); + } + + return newSet; + } + + @Override + @JRubyMethod(name = "==") + public IRubyObject op_equal(ThreadContext context, IRubyObject other) { + if ( this == other ) return context.runtime.getTrue(); + if ( getMetaClass().isInstance(other) ) { + return this.hash.op_equal(context, ((RubySet) other).hash); // @hash == ... + } + if ( other instanceof RubySet ) { + RubySet that = (RubySet) other; + if ( this.size() == that.size() ) { // && includes all of our elements : + for ( IRubyObject obj : elements() ) { + if ( ! that.contains(context, obj) ) return context.runtime.getFalse(); + } + return context.runtime.getTrue(); + } + } + return context.runtime.getFalse(); + } + + // TODO Java (Collection) equals ! + + @JRubyMethod(name = "eql?") + public IRubyObject op_eql(ThreadContext context, IRubyObject other) { + if ( other instanceof RubySet ) { + return this.hash.op_eql(context, ((RubySet) other).hash); + } + return context.runtime.getFalse(); + } + + @Override + public boolean eql(IRubyObject other) { + if ( other instanceof RubySet ) { + final Ruby runtime = getRuntime(); + return this.hash.op_eql(runtime.getCurrentContext(), ((RubySet) other).hash) == runtime.getTrue(); + } + return false; + } + + @Override + @JRubyMethod + public RubyFixnum hash() { // @hash.hash + return getRuntime().newFixnum(hashCode()); + } + + @Override + public int hashCode() { + return hash.hashCode(); + } + + + @JRubyMethod(name = "classify") + public IRubyObject classify(ThreadContext context, final Block block) { + if ( ! block.isGiven() ) { + return enumeratorizeWithSize(context, this, "classify", enumSize()); + } + + final Ruby runtime = context.runtime; + + final RubyHash h = new RubyHash(runtime, size()); + + for ( IRubyObject i : elements() ) { + final IRubyObject key = block.yield(context, i); + IRubyObject set; + if ( ( set = h.fastARef(key) ) == null ) { + h.fastASet(key, set = newSet(runtime)); + } + ((RubySet) set).invokeAdd(context, i); + } + + return h; + } + + /** + * Divides the set into a set of subsets according to the commonality + * defined by the given block. + * + * If the arity of the block is 2, elements o1 and o2 are in common + * if block.call(o1, o2) is true. Otherwise, elements o1 and o2 are + * in common if block.call(o1) == block.call(o2). + * + * e.g.: + * + * require 'set' + * numbers = Set[1, 3, 4, 6, 9, 10, 11] + * set = numbers.divide { |i,j| (i - j).abs == 1 } + * p set # => #, + * # #, + * # #, + * # #}> + */ + @JRubyMethod(name = "divide") + public IRubyObject divide(ThreadContext context, final Block block) { + if ( ! block.isGiven() ) { + return enumeratorizeWithSize(context, this, "divide", enumSize()); + } + + if ( block.getSignature().arityValue() == 2 ) { + return divideTSort(context, block); + } + + final Ruby runtime = context.runtime; // Set.new(classify(&func).values) : + + RubyHash vals = (RubyHash) classify(context, block); + final RubySet set = new RubySet(runtime, runtime.getClass("Set")); + set.initHash(runtime, vals.size()); + for ( IRubyObject val : (Collection) vals.directValues() ) { + set.invokeAdd(context, val); + } + return set; + } + + private IRubyObject divideTSort(ThreadContext context, final Block block) { + final Ruby runtime = context.runtime; + + final RubyHash dig = DivideTSortHash.newInstance(context); + + for ( IRubyObject u : elements() ) { // each + RubyArray a; + dig.fastASet(u, a = runtime.newArray()); // dig[u] = a = [] + for ( IRubyObject v : elements() ) { // each + // func.call(u, v) and a << v + IRubyObject ret = block.call(context, u, v); + if ( ret.isTrue() ) a.append(v); + } + } + + /* + each { |u| + dig[u] = a = [] + each{ |v| func.call(u, v) and a << v } + } + + set = Set.new() + dig.each_strongly_connected_component { |css| + set.add(self.class.new(css)) + } + set + */ + + final RubySet set = new RubySet(runtime, runtime.getClass("Set")); + set.initHash(runtime, dig.size()); + dig.callMethod(context, "each_strongly_connected_component", IRubyObject.NULL_ARRAY, new Block( + new JavaInternalBlockBody(runtime, Signature.ONE_REQUIRED) { + @Override + public IRubyObject yield(ThreadContext context, IRubyObject[] args) { + // final IRubyObject css = args[0]; + // set.add( self.class.new(css) ) : + set.addObject(runtime, RubySet.create(context, RubySet.this.getMetaClass(), args)); + return context.nil; + } + }) + ); + + return set; + } + + public static final class DivideTSortHash extends RubyHash { + + private static final String NAME = "DivideTSortHash"; // private constant under Set:: + + static DivideTSortHash newInstance(final ThreadContext context) { + final Ruby runtime = context.runtime; + + RubyClass Set = runtime.getClass("Set"); + RubyClass klass = (RubyClass) Set.getConstantAt(NAME, true); + if (klass == null) { + synchronized (DivideTSortHash.class) { + klass = (RubyClass) Set.getConstantAt(NAME, true); + if (klass == null) { + klass = Set.defineClassUnder(NAME, runtime.getHash(), runtime.getHash().getAllocator()); + Set.setConstantVisibility(runtime, NAME, true); // private + klass.includeModule(getTSort(runtime)); + klass.defineAnnotatedMethods(DivideTSortHash.class); + } + } + } + return new DivideTSortHash(runtime, klass); + } + + DivideTSortHash(final Ruby runtime, final RubyClass metaClass) { + super(runtime, metaClass); + } + + /* + class << dig = {} # :nodoc: + include TSort + + alias tsort_each_node each_key + def tsort_each_child(node, &block) + fetch(node).each(&block) + end + end + */ + + @JRubyMethod + public IRubyObject tsort_each_node(ThreadContext context, Block block) { + return each_key(context, block); + } + + @JRubyMethod + public IRubyObject tsort_each_child(ThreadContext context, IRubyObject node, Block block) { + IRubyObject set = fetch(context, node, Block.NULL_BLOCK); + if ( set instanceof RubySet ) { + return ((RubySet) set).each(context, block); + } + // some Enumerable (we do not expect this to happen) + return set.callMethod(context, "each", IRubyObject.NULL_ARRAY, block); + } + + } + + static RubyModule getTSort(final Ruby runtime) { + if ( ! runtime.getObject().hasConstant("TSort") ) { + runtime.getLoadService().require("tsort"); + } + return runtime.getModule("TSort"); + } + + @Override + public final IRubyObject inspect() { + return inspect(getRuntime().getCurrentContext()); + } + + private static final byte[] RECURSIVE_BYTES = new byte[] { '.','.','.' }; + + // Returns a string containing a human-readable representation of the set. + // e.g. "#" + @JRubyMethod(name = "inspect") + public RubyString inspect(ThreadContext context) { + final Ruby runtime = context.runtime; + + final RubyString str; + + if (size() == 0) { + str = RubyString.newStringLight(runtime, 16, USASCIIEncoding.INSTANCE); + inspectPrefix(str, getMetaClass()); str.cat('{').cat('}').cat('>'); // "#" + return str; + } + + if (runtime.isInspecting(this)) { + str = RubyString.newStringLight(runtime, 20, USASCIIEncoding.INSTANCE); + inspectPrefix(str, getMetaClass()); + str.cat('{').cat(RECURSIVE_BYTES).cat('}').cat('>'); // "#" + return str; + } + + str = RubyString.newStringLight(runtime, 32, USASCIIEncoding.INSTANCE); + inspectPrefix(str, getMetaClass()); + + try { + runtime.registerInspecting(this); + inspectSet(context, str); + return str.cat('>'); + } + finally { + runtime.unregisterInspecting(this); + } + } + + private static RubyString inspectPrefix(final RubyString str, final RubyClass metaClass) { + str.cat('#').cat('<').cat(metaClass.getRealClass().getName().getBytes(RubyEncoding.UTF8)); + str.cat(':').cat(' '); return str; + } + + private void inspectSet(final ThreadContext context, final RubyString str) { + + str.cat((byte) '{'); + + boolean tainted = isTaint(); boolean notFirst = false; + + for ( IRubyObject elem : elements() ) { + final RubyString s = inspect(context, elem); + if ( s.isTaint() ) tainted = true; + if ( notFirst ) str.cat((byte) ',').cat((byte) ' '); + else str.setEncoding( s.getEncoding() ); notFirst = true; + str.cat19( s ); + } + + str.cat((byte) '}'); + + if ( tainted ) str.setTaint(true); + } + + // + + protected final Set elements() { + return hash.directKeySet(); // Hash view -> no copying + } + + protected final void modifyCheck(final Ruby runtime) { + if ((flags & FROZEN_F) != 0) throw runtime.newFrozenError("Set"); + } + +} \ No newline at end of file diff --git a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java new file mode 100644 index 00000000000..72ef74d7c75 --- /dev/null +++ b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java @@ -0,0 +1,72 @@ +/* + **** BEGIN LICENSE BLOCK ***** + * Version: EPL 1.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 1.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v10.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Copyright (C) 2016 Karol Bucek + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ +package org.jruby.ext.set; + +import org.jruby.*; +import org.jruby.anno.JRubyMethod; +import org.jruby.runtime.*; +import org.jruby.runtime.builtin.IRubyObject; + +/** + * Native implementation of Ruby's SortedSet (set.rb replacement). + * + * @author kares + */ +@org.jruby.anno.JRubyClass(name="SortedSet", parent = "Set") +public class RubySortedSet extends RubySet { + + static RubyClass createSortedSetClass(final Ruby runtime) { + RubyClass SortedSet = runtime.defineClass("SortedSet", runtime.getClass("Set"), ALLOCATOR); + + SortedSet.setReifiedClass(RubySortedSet.class); + + SortedSet.defineAnnotatedMethods(RubySortedSet.class); + + return SortedSet; + } + + private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { + public RubySortedSet allocate(Ruby runtime, RubyClass klass) { + return new RubySortedSet(runtime, klass); + } + }; + + public RubySortedSet(Ruby runtime, RubyClass klass) { + super(runtime, klass); + } + + @Override + protected void addObject(final Ruby runtime, final IRubyObject obj) { + if ( ! obj.respondsTo("<=>") ) { // TODO site-cache + throw runtime.newArgumentError("value must respond to <=>"); + } + super.addObject(runtime, obj); + } + +} diff --git a/core/src/main/java/org/jruby/ext/set/SetLibrary.java b/core/src/main/java/org/jruby/ext/set/SetLibrary.java new file mode 100644 index 00000000000..a20a2d1eb78 --- /dev/null +++ b/core/src/main/java/org/jruby/ext/set/SetLibrary.java @@ -0,0 +1,45 @@ +/***** BEGIN LICENSE BLOCK ***** + * Version: EPL 1.0/GPL 2.0/LGPL 2.1 + * + * The contents of this file are subject to the Eclipse Public + * License Version 1.0 (the "License"); you may not use this file + * except in compliance with the License. You may obtain a copy of + * the License at http://www.eclipse.org/legal/epl-v10.html + * + * Software distributed under the License is distributed on an "AS + * IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or + * implied. See the License for the specific language governing + * rights and limitations under the License. + * + * Copyright (C) 2016 Karol Bucek + * + * Alternatively, the contents of this file may be used under the terms of + * either of the GNU General Public License Version 2 or later (the "GPL"), + * or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"), + * in which case the provisions of the GPL or the LGPL are applicable instead + * of those above. If you wish to allow use of your version of this file only + * under the terms of either the GPL or the LGPL, and not to allow others to + * use your version of this file under the terms of the EPL, indicate your + * decision by deleting the provisions above and replace them with the notice + * and other provisions required by the GPL or the LGPL. If you do not delete + * the provisions above, a recipient may use your version of this file under + * the terms of any one of the EPL, the GPL or the LGPL. + ***** END LICENSE BLOCK *****/ +package org.jruby.ext.set; + +import org.jruby.Ruby; +import org.jruby.runtime.load.Library; + +public final class SetLibrary implements Library { + + public void load(Ruby runtime, boolean wrap) { + SetLibrary.load(runtime); + } + + public static void load(Ruby runtime) { + RubySet.createSetClass(runtime); + RubySortedSet.createSortedSetClass(runtime); + runtime.getModule("Enumerable").defineAnnotatedMethods(EnumerableExt.class); + } + +} From 3bb3ae77c1d9bc9cb309e127769b34ed53d8e8c2 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 8 Dec 2016 08:58:19 +0100 Subject: [PATCH 02/16] extract out Array's Comparator impls into classes (for reusability) --- core/src/main/java/org/jruby/RubyArray.java | 154 ++++++++++++++++---- 1 file changed, 122 insertions(+), 32 deletions(-) diff --git a/core/src/main/java/org/jruby/RubyArray.java b/core/src/main/java/org/jruby/RubyArray.java index fef2c0dd58b..d9e4cd0dfb2 100644 --- a/core/src/main/java/org/jruby/RubyArray.java +++ b/core/src/main/java/org/jruby/RubyArray.java @@ -3483,42 +3483,134 @@ public IRubyObject sort_bang19(ThreadContext context, Block block) { return sort_bang(context, block); } - protected IRubyObject sortInternal(final ThreadContext context, boolean honorOverride) { - Ruby runtime = context.runtime; - - // One check per specialized fast-path to make the check invariant. - final boolean fixnumBypass = !honorOverride || runtime.getFixnum().isMethodBuiltin("<=>"); - final boolean stringBypass = !honorOverride || runtime.getString().isMethodBuiltin("<=>"); - + protected IRubyObject sortInternal(final ThreadContext context, final boolean honorOverride) { try { - Arrays.sort(values, begin, begin + realLength, new Comparator() { - public int compare(Object o1, Object o2) { - if (fixnumBypass && o1 instanceof RubyFixnum && o2 instanceof RubyFixnum) { - return compareFixnums((RubyFixnum) o1, (RubyFixnum) o2); - } - if (stringBypass && o1 instanceof RubyString && o2 instanceof RubyString) { - return ((RubyString) o1).op_cmp((RubyString) o2); - } - return compareOthers(context, (IRubyObject)o1, (IRubyObject)o2); + Arrays.sort(values, begin, begin + realLength, new DefaultComparator(context, honorOverride) { + protected int compareGeneric(IRubyObject o1, IRubyObject o2) { + //TODO: ary_sort_check should be done here + return super.compareGeneric(o1, o2); } }); - } catch (ArrayIndexOutOfBoundsException ex) { + } + catch (ArrayIndexOutOfBoundsException ex) { throw concurrentModification(context.runtime, ex); } return this; } + // @Deprecated protected static int compareFixnums(RubyFixnum o1, RubyFixnum o2) { - long a = o1.getLongValue(); - long b = o2.getLongValue(); - return a > b ? 1 : a == b ? 0 : -1; + return DefaultComparator.compareInteger(o1, o2); } + // @Deprecated protected static int compareOthers(ThreadContext context, IRubyObject o1, IRubyObject o2) { - IRubyObject ret = sites(context).op_cmp_sort.call(context, o1, o1, o2); - int n = RubyComparable.cmpint(context, ret, o1, o2); - //TODO: ary_sort_check should be done here - return n; + return DefaultComparator.compareGeneric(context, o1, o2); + } + + public static class DefaultComparator implements Comparator { + + final ThreadContext context; + + private final boolean fixnumBypass; + private final boolean stringBypass; + + public DefaultComparator(ThreadContext context) { + this(context, true); + } + + DefaultComparator(ThreadContext context, final boolean honorOverride) { + this.context = context; + if ( honorOverride && context != null ) { + this.fixnumBypass = !honorOverride || context.runtime.getFixnum().isMethodBuiltin("<=>"); + this.stringBypass = !honorOverride || context.runtime.getString().isMethodBuiltin("<=>"); + } + else { // no-opt + this.fixnumBypass = false; + this.stringBypass = false; + } + } + + /* + DefaultComparator(ThreadContext context, final boolean fixnumBypass, final boolean stringBypass) { + this.context = context; + this.fixnumBypass = fixnumBypass; + this.stringBypass = stringBypass; + } */ + + public int compare(IRubyObject obj1, IRubyObject obj2) { + if (fixnumBypass && obj1 instanceof RubyFixnum && obj2 instanceof RubyFixnum) { + return compareInteger((RubyFixnum) obj1, (RubyFixnum) obj2); + } + if (stringBypass && obj1 instanceof RubyString && obj2 instanceof RubyString) { + return compareString((RubyString) obj1, (RubyString) obj2); + } + return compareGeneric(obj1, obj2); + } + + protected int compareGeneric(IRubyObject o1, IRubyObject o2) { + final ThreadContext context = context(); + return compareGeneric(context, sites(context).op_cmp_sort, o1, o2); + } + + protected ThreadContext context() { + return context; + } + + public static int compareInteger(RubyFixnum o1, RubyFixnum o2) { + long a = o1.getLongValue(); + long b = o2.getLongValue(); + return a > b ? 1 : a == b ? 0 : -1; + } + + public static int compareString(RubyString o1, RubyString o2) { + return o1.op_cmp(o2); + } + + public static int compareGeneric(ThreadContext context, IRubyObject o1, IRubyObject o2) { + return compareGeneric(context, sites(context).op_cmp_sort, o1, o2); + } + + public static int compareGeneric(ThreadContext context, CallSite op_cmp_sort, IRubyObject o1, IRubyObject o2) { + IRubyObject ret = op_cmp_sort.call(context, o1, o1, o2); + return RubyComparable.cmpint(context, ret, o1, o2); + } + + } + + static class BlockComparator implements Comparator { + + final ThreadContext context; + + protected final Block block; + protected final IRubyObject self; + + private final CallSite gt; + private final CallSite lt; + + BlockComparator(ThreadContext context, Block block, CallSite gt, CallSite lt) { + this(context, block, null, gt, lt); + } + + BlockComparator(ThreadContext context, Block block, IRubyObject self, CallSite gt, CallSite lt) { + this.context = context == null ? self.getRuntime().getCurrentContext() : context; + this.block = block; this.self = self; + this.gt = gt; this.lt = lt; + } + + public int compare(IRubyObject obj1, IRubyObject obj2) { + return RubyComparable.cmpint(context, gt, lt, yieldBlock(obj1, obj2), obj1, obj2); + } + + protected final IRubyObject yieldBlock(IRubyObject obj1, IRubyObject obj2) { + final ThreadContext context = context(); + return block.yieldArray(context, context.runtime.newArray(obj1, obj2), self); + } + + protected final ThreadContext context() { + return context; + } + } protected IRubyObject sortInternal(final ThreadContext context, final Block block) { @@ -3528,15 +3620,13 @@ protected IRubyObject sortInternal(final ThreadContext context, final Block bloc int length = realLength; copyInto(newValues, 0); - Arrays.sort(newValues, 0, length, new Comparator() { - CallSite gt = sites(context).op_gt_sort; - CallSite lt = sites(context).op_lt_sort; - public int compare(Object o1, Object o2) { - IRubyObject obj1 = (IRubyObject) o1; - IRubyObject obj2 = (IRubyObject) o2; - IRubyObject ret = block.yieldArray(context, getRuntime().newArray(obj1, obj2), null); + CallSite gt = sites(context).op_gt_sort; + CallSite lt = sites(context).op_lt_sort; + Arrays.sort(newValues, 0, length, new BlockComparator(context, block, gt, lt) { + @Override + public int compare(IRubyObject obj1, IRubyObject obj2) { //TODO: ary_sort_check should be done here - return RubyComparable.cmpint(context, gt, lt, ret, obj1, obj2); + return super.compare(obj1, obj2); } }); From 7c602d9b1484e1da73d195fc3fbb5277cbaf4eb9 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 8 Dec 2016 09:01:02 +0100 Subject: [PATCH 03/16] get native SortedSet working (using Java's TreeSet as an order backend) --- .../main/java/org/jruby/ext/set/RubySet.java | 94 +++++++++++-------- .../java/org/jruby/ext/set/RubySortedSet.java | 70 +++++++++++++- 2 files changed, 120 insertions(+), 44 deletions(-) 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 37f24683c34..9471480fd5e 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -159,7 +159,7 @@ private IRubyObject initWithEnum(final ThreadContext context, final IRubyObject if ( enume instanceof RubySet ) { RubySet set = (RubySet) enume; initHash(context.runtime, set.size()); - for ( IRubyObject elem : set.elements() ) { + for ( IRubyObject elem : set.elementsOrdered() ) { invokeAdd(context, block.yield(context, elem)); } return set; // done @@ -280,7 +280,7 @@ public IRubyObject rb_clear(ThreadContext context) { return this; } - private void clearImpl() { + protected void clearImpl() { hash.rb_clear(); } @@ -291,7 +291,8 @@ private void clearImpl() { public RubySet replace(final ThreadContext context, IRubyObject enume) { if ( enume instanceof RubySet ) { modifyCheck(context.runtime); - this.hash.replace(context, ((RubySet) enume).hash); + clearImpl(); + addImplSet(context, (RubySet) enume); } else { final Ruby runtime = context.runtime; @@ -323,7 +324,7 @@ public RubyArray to_a(final ThreadContext context) { public RubySet to_set(final ThreadContext context, final Block block) { if ( block.isGiven() ) { RubySet set = new RubySet(context.runtime, getMetaClass()); - set.initialize(context, block); + set.initialize(context, this, block); return set; } return this; @@ -363,7 +364,7 @@ public RubySet flatten_merge(final ThreadContext context, IRubyObject set) { private void flattenMerge(final ThreadContext context, final IRubyObject set, final IdentityHashMap seen) { if ( set instanceof RubySet ) { - for ( IRubyObject e : ((RubySet) set).elements() ) { + for ( IRubyObject e : ((RubySet) set).elementsOrdered() ) { addFlattened(context, seen, e); } } @@ -400,7 +401,7 @@ public RubySet flatten(final ThreadContext context) { @JRubyMethod(name = "flatten!") public IRubyObject flatten_bang(final ThreadContext context) { - for ( IRubyObject e : elements() ) { + for ( IRubyObject e : elementsOrdered() ) { if ( e instanceof RubySet ) { // needs flatten return replace(context, flatten(context)); } @@ -499,13 +500,13 @@ public IRubyObject intersect_p(final ThreadContext context, IRubyObject set) { public boolean intersect(final ThreadContext context, final RubySet set) { if ( size() < set.size() ) { // any? { |o| set.include?(o) } - for ( IRubyObject o : elements() ) { + for ( IRubyObject o : elementsOrdered() ) { if ( set.contains(context, o) ) return true; } } else { // set.any? { |o| include?(o) } - for ( IRubyObject o : set.elements() ) { + for ( IRubyObject o : set.elementsOrdered() ) { if ( contains(context, o) ) return true; } } @@ -530,7 +531,7 @@ public IRubyObject each(final ThreadContext context, Block block) { return enumeratorizeWithSize(context, this, "each", enumSize()); } - for (IRubyObject elem : elements()) block.yield(context, elem); + for (IRubyObject elem : elementsOrdered()) block.yield(context, elem); return this; } @@ -549,14 +550,19 @@ public IRubyObject size(IRubyObject[] args) { @JRubyMethod(name = "add", alias = "<<") public RubySet add(final ThreadContext context, IRubyObject obj) { modifyCheck(context.runtime); - addObject(context.runtime, obj); + addImpl(context.runtime, obj); return this; } - protected void addObject(final Ruby runtime, final IRubyObject obj) { + protected void addImpl(final Ruby runtime, final IRubyObject obj) { hash.fastASetCheckString(runtime, obj, runtime.getTrue()); // @hash[obj] = true } + protected void addImplSet(final ThreadContext context, final RubySet set) { + // NOTE: MRI cheats - does not call Set#add thus we do not care ... + hash.merge_bang(context, set.hash, Block.NULL_BLOCK); + } + /** * Adds the given object to the set and returns self. If the object is already in the set, returns nil. */ @@ -570,13 +576,17 @@ public IRubyObject add_p(final ThreadContext context, IRubyObject obj) { @JRubyMethod public IRubyObject delete(final ThreadContext context, IRubyObject obj) { modifyCheck(context.runtime); - deleteObject(obj); + deleteImpl(obj); return this; } - private void deleteObject(final IRubyObject obj) { + protected boolean deleteImpl(final IRubyObject obj) { hash.modify(); - hash.fastDelete(obj); + return hash.fastDelete(obj); + } + + protected void deleteImplIterator(final IRubyObject obj, final Iterator it) { + it.remove(); } /** @@ -598,7 +608,7 @@ public IRubyObject delete_if(final ThreadContext context, Block block) { Iterator it = elements().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); - if ( block.yield(context, elem).isTrue() ) it.remove(); + if ( block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove } return this; } @@ -612,7 +622,7 @@ public IRubyObject keep_if(final ThreadContext context, Block block) { Iterator it = elements().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); - if ( ! block.yield(context, elem).isTrue() ) it.remove(); + if ( ! block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove } return this; } @@ -625,7 +635,7 @@ public IRubyObject collect_bang(final ThreadContext context, Block block) { final RubyArray elems = to_a(context); clearImpl(); for ( int i=0; i it = elements().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); - if ( block.yield(context, elem).isTrue() ) it.remove(); + if ( block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove } return size == size() ? context.nil : this; } @@ -657,7 +667,7 @@ public IRubyObject select_bang(final ThreadContext context, Block block) { Iterator it = elements().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); - if ( ! block.yield(context, elem).isTrue() ) it.remove(); + if ( ! block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove } return size == size() ? context.nil : this; } @@ -671,20 +681,19 @@ public RubySet rb_merge(final ThreadContext context, IRubyObject enume) { if ( enume instanceof RubySet ) { modifyCheck(runtime); - // NOTE: MRI cheats - does not call Set#add thus we do not care ... - hash.merge_bang(context, ((RubySet) enume).hash, Block.NULL_BLOCK); + addImplSet(context, (RubySet) enume); } else if ( enume instanceof RubyArray ) { modifyCheck(runtime); RubyArray ary = (RubyArray) enume; for ( int i = 0; i < ary.size(); i++ ) { - addObject(runtime, ary.eltInternal(i)); + addImpl(runtime, ary.eltInternal(i)); } } else { // do_with_enum(enum) { |o| add(o) } doWithEnum(context, enume, new EachBody(runtime) { IRubyObject yieldImpl(ThreadContext context, IRubyObject val) { - addObject(context.runtime, val); return context.nil; + addImpl(context.runtime, val); return context.nil; } }); } @@ -701,21 +710,21 @@ public IRubyObject subtract(final ThreadContext context, IRubyObject enume) { if ( enume instanceof RubySet ) { modifyCheck(runtime); - for ( IRubyObject elem : ((RubySet) enume).elements() ) { - deleteObject(elem); + for ( IRubyObject elem : ((RubySet) enume).elementsOrdered() ) { + deleteImpl(elem); } } else if ( enume instanceof RubyArray ) { modifyCheck(runtime); RubyArray ary = (RubyArray) enume; for ( int i = 0; i < ary.size(); i++ ) { - deleteObject(ary.eltInternal(i)); + deleteImpl(ary.eltInternal(i)); } } else { // do_with_enum(enum) { |o| delete(o) } doWithEnum(context, enume, new EachBody(runtime) { IRubyObject yieldImpl(ThreadContext context, IRubyObject val) { - deleteObject(val); return context.nil; + deleteImpl(val); return context.nil; } }); } @@ -749,8 +758,8 @@ public IRubyObject op_and(final ThreadContext context, IRubyObject enume) { final RubySet newSet = new RubySet(runtime, getMetaClass()); if ( enume instanceof RubySet ) { newSet.initHash(runtime, ((RubySet) enume).size()); - for ( IRubyObject obj : ((RubySet) enume).elements() ) { - if ( contains(context, obj) ) newSet.addObject(runtime, obj); + for ( IRubyObject obj : ((RubySet) enume).elementsOrdered() ) { + if ( contains(context, obj) ) newSet.addImpl(runtime, obj); } } else if ( enume instanceof RubyArray ) { @@ -758,7 +767,7 @@ else if ( enume instanceof RubyArray ) { newSet.initHash(runtime, ary.size()); for ( int i = 0; i < ary.size(); i++ ) { final IRubyObject obj = ary.eltInternal(i); - if ( contains(context, obj) ) newSet.addObject(runtime, obj); + if ( contains(context, obj) ) newSet.addImpl(runtime, obj); } } else { @@ -766,7 +775,7 @@ else if ( enume instanceof RubyArray ) { // do_with_enum(enum) { |o| newSet.add(o) if include?(o) } doWithEnum(context, enume, new EachBody(runtime) { IRubyObject yieldImpl(ThreadContext context, IRubyObject obj) { - if ( contains(context, obj) ) newSet.addObject(runtime, obj); + if ( contains(context, obj) ) newSet.addImpl(runtime, obj); return context.nil; } }); @@ -785,9 +794,9 @@ public IRubyObject op_xor(final ThreadContext context, IRubyObject enume) { RubySet newSet = new RubySet(runtime, runtime.getClass("Set")); newSet.initialize(context, enume, Block.NULL_BLOCK); // Set.new(enum) - for ( IRubyObject o : elements() ) { - if ( newSet.contains(context, o) ) newSet.deleteObject(o); // exclusive or - else newSet.addObject(runtime, o); + for ( IRubyObject o : elementsOrdered() ) { + if ( newSet.contains(context, o) ) newSet.deleteImpl(o); // exclusive or + else newSet.addImpl(runtime, o); } return newSet; @@ -803,7 +812,7 @@ public IRubyObject op_equal(ThreadContext context, IRubyObject other) { if ( other instanceof RubySet ) { RubySet that = (RubySet) other; if ( this.size() == that.size() ) { // && includes all of our elements : - for ( IRubyObject obj : elements() ) { + for ( IRubyObject obj : elementsOrdered() ) { if ( ! that.contains(context, obj) ) return context.runtime.getFalse(); } return context.runtime.getTrue(); @@ -853,7 +862,7 @@ public IRubyObject classify(ThreadContext context, final Block block) { final RubyHash h = new RubyHash(runtime, size()); - for ( IRubyObject i : elements() ) { + for ( IRubyObject i : elementsOrdered() ) { final IRubyObject key = block.yield(context, i); IRubyObject set; if ( ( set = h.fastARef(key) ) == null ) { @@ -909,10 +918,10 @@ private IRubyObject divideTSort(ThreadContext context, final Block block) { final RubyHash dig = DivideTSortHash.newInstance(context); - for ( IRubyObject u : elements() ) { // each + for ( IRubyObject u : elementsOrdered() ) { // each RubyArray a; dig.fastASet(u, a = runtime.newArray()); // dig[u] = a = [] - for ( IRubyObject v : elements() ) { // each + for ( IRubyObject v : elementsOrdered() ) { // each // func.call(u, v) and a << v IRubyObject ret = block.call(context, u, v); if ( ret.isTrue() ) a.append(v); @@ -940,7 +949,7 @@ private IRubyObject divideTSort(ThreadContext context, final Block block) { public IRubyObject yield(ThreadContext context, IRubyObject[] args) { // final IRubyObject css = args[0]; // set.add( self.class.new(css) ) : - set.addObject(runtime, RubySet.create(context, RubySet.this.getMetaClass(), args)); + set.addImpl(runtime, RubySet.create(context, RubySet.this.getMetaClass(), args)); return context.nil; } }) @@ -1063,7 +1072,7 @@ private void inspectSet(final ThreadContext context, final RubyString str) { boolean tainted = isTaint(); boolean notFirst = false; - for ( IRubyObject elem : elements() ) { + for ( IRubyObject elem : elementsOrdered() ) { final RubyString s = inspect(context, elem); if ( s.isTaint() ) tainted = true; if ( notFirst ) str.cat((byte) ',').cat((byte) ' '); @@ -1082,6 +1091,11 @@ protected final Set elements() { return hash.directKeySet(); // Hash view -> no copying } + // NOTE: implementation does not expect to be used for altering contents using iterator + protected Set elementsOrdered() { + return elements(); // potentially -> to be re-defined by SortedSet + } + protected final void modifyCheck(final Ruby runtime) { if ((flags & FROZEN_F) != 0) throw runtime.newFrozenError("Set"); } diff --git a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java index 72ef74d7c75..a1970abf599 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java @@ -33,6 +33,12 @@ import org.jruby.runtime.*; import org.jruby.runtime.builtin.IRubyObject; +import java.util.Iterator; +import java.util.Set; +import java.util.TreeSet; + +import static org.jruby.RubyArray.DefaultComparator; + /** * Native implementation of Ruby's SortedSet (set.rb replacement). * @@ -45,7 +51,6 @@ static RubyClass createSortedSetClass(final Ruby runtime) { RubyClass SortedSet = runtime.defineClass("SortedSet", runtime.getClass("Set"), ALLOCATOR); SortedSet.setReifiedClass(RubySortedSet.class); - SortedSet.defineAnnotatedMethods(RubySortedSet.class); return SortedSet; @@ -57,16 +62,73 @@ public RubySortedSet allocate(Ruby runtime, RubyClass klass) { } }; - public RubySortedSet(Ruby runtime, RubyClass klass) { + private static class OrderComparator extends DefaultComparator { + + private final Ruby runtime; + + OrderComparator(final Ruby runtime) { + super(null); this.runtime = runtime; + } + + @Override + protected ThreadContext context() { + return runtime.getCurrentContext(); + } + } + + private final TreeSet order; + + protected RubySortedSet(Ruby runtime, RubyClass klass) { super(runtime, klass); + order = new TreeSet(new OrderComparator(runtime)); } @Override - protected void addObject(final Ruby runtime, final IRubyObject obj) { + protected void addImpl(final Ruby runtime, final IRubyObject obj) { + if ( ! obj.respondsTo("<=>") ) { // TODO site-cache throw runtime.newArgumentError("value must respond to <=>"); } - super.addObject(runtime, obj); + + super.addImpl(runtime, obj); // @hash[obj] = true + order.add(obj); + } + + @Override + protected void addImplSet(final ThreadContext context, final RubySet set) { + super.addImplSet(context, set); + order.addAll(set.elements()); + } + + @Override + protected boolean deleteImpl(final IRubyObject obj) { + if ( super.deleteImpl(obj) ) { + order.remove(obj); return true; + } + return false; + } + + @Override + protected void deleteImplIterator(final IRubyObject obj, final Iterator it) { + it.remove(); // super + order.remove(obj); + } + + @Override + protected void clearImpl() { + hash.rb_clear(); + order.clear(); + } + + @Override + @JRubyMethod(name = "to_a", alias = "sort") // re-def Enumerable#sort + public RubyArray to_a(final ThreadContext context) { + return RubyArray.newArray(context.runtime, order); // instead of this.hash.keys(); } + // NOTE: weirdly Set/SortedSet in Ruby do not have sort! + + @Override + protected Set elementsOrdered() { return order; } + } From e0d6b6fb64d7e889744b64744d755d8fed07003d Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 8 Dec 2016 11:07:26 +0100 Subject: [PATCH 04/16] fix Set#divide un-wrapping Array when yielded from TSort's --- .../main/java/org/jruby/ext/set/RubySet.java | 46 +++++++++++++------ 1 file changed, 31 insertions(+), 15 deletions(-) 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 9471480fd5e..4221941de16 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -95,7 +95,18 @@ final void initHash(final RubyHash hash) { RubySet newSet(final Ruby runtime) { RubySet set = new RubySet(runtime, getMetaClass()); - set.initHash(runtime); return set; + set.initHash(runtime); + return set; + } + + private RubySet newSet(final ThreadContext context, final RubyClass metaClass, final RubyArray elements) { + RubySet set = new RubySet(context.runtime, metaClass); + final int len = elements.size(); + set.initHash(context.runtime, len); + for ( int i = 0; i < len; i++ ) { + set.invokeAdd(context, elements.eltInternal(i)); + } + return set; } /** @@ -918,38 +929,42 @@ private IRubyObject divideTSort(ThreadContext context, final Block block) { final RubyHash dig = DivideTSortHash.newInstance(context); - for ( IRubyObject u : elementsOrdered() ) { // each + /* + each { |u| + dig[u] = a = [] + each{ |v| func.call(u, v) and a << v } + } + */ + for ( IRubyObject u : elementsOrdered() ) { RubyArray a; - dig.fastASet(u, a = runtime.newArray()); // dig[u] = a = [] - for ( IRubyObject v : elementsOrdered() ) { // each - // func.call(u, v) and a << v + dig.fastASet(u, a = runtime.newArray()); + for ( IRubyObject v : elementsOrdered() ) { IRubyObject ret = block.call(context, u, v); if ( ret.isTrue() ) a.append(v); } } /* - each { |u| - dig[u] = a = [] - each{ |v| func.call(u, v) and a << v } - } - set = Set.new() dig.each_strongly_connected_component { |css| set.add(self.class.new(css)) } set */ - - final RubySet set = new RubySet(runtime, runtime.getClass("Set")); + final RubyClass Set = runtime.getClass("Set"); + final RubySet set = new RubySet(runtime, Set); set.initHash(runtime, dig.size()); dig.callMethod(context, "each_strongly_connected_component", IRubyObject.NULL_ARRAY, new Block( new JavaInternalBlockBody(runtime, Signature.ONE_REQUIRED) { @Override public IRubyObject yield(ThreadContext context, IRubyObject[] args) { - // final IRubyObject css = args[0]; + return doYield(context, null, args[0]); + } + + @Override + protected IRubyObject doYield(ThreadContext context, Block block, IRubyObject css) { // set.add( self.class.new(css) ) : - set.addImpl(runtime, RubySet.create(context, RubySet.this.getMetaClass(), args)); + set.addImpl(runtime, RubySet.this.newSet(context, Set, (RubyArray) css)); return context.nil; } }) @@ -958,6 +973,7 @@ public IRubyObject yield(ThreadContext context, IRubyObject[] args) { return set; } + // NOTE: a replacement for set.rb's eval in Set#divide : `class << dig = {} ...` public static final class DivideTSortHash extends RubyHash { private static final String NAME = "DivideTSortHash"; // private constant under Set:: @@ -967,7 +983,7 @@ static DivideTSortHash newInstance(final ThreadContext context) { RubyClass Set = runtime.getClass("Set"); RubyClass klass = (RubyClass) Set.getConstantAt(NAME, true); - if (klass == null) { + if (klass == null) { // initialize on-demand when Set#divide is first called synchronized (DivideTSortHash.class) { klass = (RubyClass) Set.getConstantAt(NAME, true); if (klass == null) { From c40e4c46235ec54bfe7de05e9be0019a5ee3fbb7 Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 8 Dec 2016 16:08:32 +0100 Subject: [PATCH 05/16] jruby specific .rb bits from stdlib's set.rb (pretty print only) --- .../main/java/org/jruby/ext/set/RubySet.java | 2 ++ core/src/main/ruby/jruby/set.rb | 21 +++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 core/src/main/ruby/jruby/set.rb 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 4221941de16..e81462e6c9f 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -59,6 +59,8 @@ static RubyClass createSetClass(final Ruby runtime) { Set.includeModule(runtime.getEnumerable()); Set.defineAnnotatedMethods(RubySet.class); + runtime.getLoadService().require("jruby/set.rb"); + return Set; } diff --git a/core/src/main/ruby/jruby/set.rb b/core/src/main/ruby/jruby/set.rb new file mode 100644 index 00000000000..4204fa05787 --- /dev/null +++ b/core/src/main/ruby/jruby/set.rb @@ -0,0 +1,21 @@ +# frozen_string_literal: true +# +# .rb part for JRuby's native Set impl (taken from set.rb) + +class Set + + def pretty_print(pp) # :nodoc: + pp.text sprintf('#<%s: {', self.class.name) + pp.nest(1) { + pp.seplist(self) { |o| + pp.pp o + } + } + pp.text '}>' + end + + def pretty_print_cycle(pp) # :nodoc: + pp.text sprintf('#<%s: {%s}>', self.class.name, empty? ? '' : '...') + end + +end \ No newline at end of file From 40db09d753d81fff2fb57b572e2534c44a8924bc Mon Sep 17 00:00:00 2001 From: kares Date: Thu, 8 Dec 2016 16:24:11 +0100 Subject: [PATCH 06/16] Ruby's Set/SortedSet implements java.util.Set/java.util.SortedSet ... with auto (magic) toJava conversions (like RubyArray does) --- .../main/java/org/jruby/ext/set/RubySet.java | 178 ++++++++++++++---- .../java/org/jruby/ext/set/RubySortedSet.java | 86 ++++++++- 2 files changed, 222 insertions(+), 42 deletions(-) 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 e81462e6c9f..addf69696b3 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -32,10 +32,12 @@ import org.jruby.*; import org.jruby.anno.JRubyMethod; import org.jruby.common.IRubyWarnings; +import org.jruby.javasupport.JavaUtil; import org.jruby.runtime.*; import org.jruby.runtime.builtin.IRubyObject; import org.jruby.util.ArraySupport; +import java.lang.reflect.Array; import java.util.Collection; import java.util.IdentityHashMap; import java.util.Iterator; @@ -49,7 +51,7 @@ * @author kares */ @org.jruby.anno.JRubyClass(name="Set", include = { "Enumerable" }) -public class RubySet extends RubyObject { // implements Set { +public class RubySet extends RubyObject implements Set { static RubyClass createSetClass(final Ruby runtime) { RubyClass Set = runtime.defineClass("Set", runtime.getObject(), ALLOCATOR); @@ -269,22 +271,16 @@ public IRubyObject untaint(ThreadContext context) { return super.untaint(context); } - public int size() { return hash.size(); } - @JRubyMethod(name = "size", alias = "length") public IRubyObject length(ThreadContext context) { return context.runtime.newFixnum( size() ); } - public boolean isEmpty() { return hash.isEmpty(); } - @JRubyMethod(name = "empty?") public IRubyObject empty_p(ThreadContext context) { return context.runtime.newBoolean( isEmpty() ); } - public void clear() { clearImpl(); } - @JRubyMethod(name = "clear") public IRubyObject rb_clear(ThreadContext context) { modifyCheck(context.runtime); @@ -427,16 +423,16 @@ public IRubyObject flatten_bang(final ThreadContext context) { */ @JRubyMethod(name = "include?", alias = { "member?" }) public RubyBoolean include_p(final ThreadContext context, IRubyObject obj) { - return hash.has_key_p(context, obj); + return context.runtime.newBoolean( containsImpl(obj) ); } - final boolean contains(final ThreadContext context, IRubyObject obj) { - return include_p(context, obj) == context.runtime.getTrue(); + final boolean containsImpl(IRubyObject obj) { + return hash.fastARef(obj) != null; } - private boolean allElementsIncluded(ThreadContext context, final RubySet set) { + private boolean allElementsIncluded(final RubySet set) { for ( IRubyObject o : set.elements() ) { // set.all? { |o| include?(o) } - if ( ! contains(context, o) ) return false; + if ( ! containsImpl(o) ) return false; } return true; } @@ -450,7 +446,7 @@ public IRubyObject superset_p(final ThreadContext context, IRubyObject set) { } // size >= set.size && set.all? { |o| include?(o) } return context.runtime.newBoolean( - size() >= ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + size() >= ((RubySet) set).size() && allElementsIncluded((RubySet) set) ); } throw context.runtime.newArgumentError("value must be a set"); @@ -465,7 +461,7 @@ public IRubyObject proper_superset_p(final ThreadContext context, IRubyObject se } // size >= set.size && set.all? { |o| include?(o) } return context.runtime.newBoolean( - size() > ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + size() > ((RubySet) set).size() && allElementsIncluded((RubySet) set) ); } throw context.runtime.newArgumentError("value must be a set"); @@ -479,7 +475,7 @@ public IRubyObject subset_p(final ThreadContext context, IRubyObject set) { } // size >= set.size && set.all? { |o| include?(o) } return context.runtime.newBoolean( - size() <= ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + size() <= ((RubySet) set).size() && allElementsIncluded((RubySet) set) ); } throw context.runtime.newArgumentError("value must be a set"); @@ -493,7 +489,7 @@ public IRubyObject proper_subset_p(final ThreadContext context, IRubyObject set) } // size >= set.size && set.all? { |o| include?(o) } return context.runtime.newBoolean( - size() < ((RubySet) set).size() && allElementsIncluded(context, (RubySet) set) + size() < ((RubySet) set).size() && allElementsIncluded((RubySet) set) ); } throw context.runtime.newArgumentError("value must be a set"); @@ -505,22 +501,22 @@ public IRubyObject proper_subset_p(final ThreadContext context, IRubyObject set) @JRubyMethod(name = "intersect?") public IRubyObject intersect_p(final ThreadContext context, IRubyObject set) { if ( set instanceof RubySet ) { - return context.runtime.newBoolean( intersect(context, (RubySet) set) ); + return context.runtime.newBoolean( intersect((RubySet) set) ); } throw context.runtime.newArgumentError("value must be a set"); } - public boolean intersect(final ThreadContext context, final RubySet set) { + public boolean intersect(final RubySet set) { if ( size() < set.size() ) { // any? { |o| set.include?(o) } for ( IRubyObject o : elementsOrdered() ) { - if ( set.contains(context, o) ) return true; + if ( set.containsImpl(o) ) return true; } } else { // set.any? { |o| include?(o) } for ( IRubyObject o : set.elementsOrdered() ) { - if ( contains(context, o) ) return true; + if ( containsImpl(o) ) return true; } } return false; @@ -533,7 +529,7 @@ public boolean intersect(final ThreadContext context, final RubySet set) { @JRubyMethod(name = "disjoint?") public IRubyObject disjoint_p(final ThreadContext context, IRubyObject set) { if ( set instanceof RubySet ) { - return context.runtime.newBoolean( ! intersect(context, (RubySet) set) ); + return context.runtime.newBoolean( ! intersect((RubySet) set) ); } throw context.runtime.newArgumentError("value must be a set"); } @@ -582,7 +578,7 @@ protected void addImplSet(final ThreadContext context, final RubySet set) { @JRubyMethod(name = "add?") public IRubyObject add_p(final ThreadContext context, IRubyObject obj) { // add(o) unless include?(o) - if ( contains(context, obj) ) return context.nil; + if ( containsImpl(obj) ) return context.nil; return add(context, obj); } @@ -608,7 +604,7 @@ protected void deleteImplIterator(final IRubyObject obj, final Iterator it) { @JRubyMethod(name = "delete?") public IRubyObject delete_p(final ThreadContext context, IRubyObject obj) { // delete(o) if include?(o) - if ( ! contains(context, obj) ) return context.nil; + if ( ! containsImpl(obj) ) return context.nil; return delete(context, obj); } @@ -772,7 +768,7 @@ public IRubyObject op_and(final ThreadContext context, IRubyObject enume) { if ( enume instanceof RubySet ) { newSet.initHash(runtime, ((RubySet) enume).size()); for ( IRubyObject obj : ((RubySet) enume).elementsOrdered() ) { - if ( contains(context, obj) ) newSet.addImpl(runtime, obj); + if ( containsImpl(obj) ) newSet.addImpl(runtime, obj); } } else if ( enume instanceof RubyArray ) { @@ -780,7 +776,7 @@ else if ( enume instanceof RubyArray ) { newSet.initHash(runtime, ary.size()); for ( int i = 0; i < ary.size(); i++ ) { final IRubyObject obj = ary.eltInternal(i); - if ( contains(context, obj) ) newSet.addImpl(runtime, obj); + if ( containsImpl(obj) ) newSet.addImpl(runtime, obj); } } else { @@ -788,7 +784,7 @@ else if ( enume instanceof RubyArray ) { // do_with_enum(enum) { |o| newSet.add(o) if include?(o) } doWithEnum(context, enume, new EachBody(runtime) { IRubyObject yieldImpl(ThreadContext context, IRubyObject obj) { - if ( contains(context, obj) ) newSet.addImpl(runtime, obj); + if ( containsImpl(obj) ) newSet.addImpl(runtime, obj); return context.nil; } }); @@ -808,7 +804,7 @@ public IRubyObject op_xor(final ThreadContext context, IRubyObject enume) { RubySet newSet = new RubySet(runtime, runtime.getClass("Set")); newSet.initialize(context, enume, Block.NULL_BLOCK); // Set.new(enum) for ( IRubyObject o : elementsOrdered() ) { - if ( newSet.contains(context, o) ) newSet.deleteImpl(o); // exclusive or + if ( newSet.containsImpl(o) ) newSet.deleteImpl(o); // exclusive or else newSet.addImpl(runtime, o); } @@ -826,7 +822,7 @@ public IRubyObject op_equal(ThreadContext context, IRubyObject other) { RubySet that = (RubySet) other; if ( this.size() == that.size() ) { // && includes all of our elements : for ( IRubyObject obj : elementsOrdered() ) { - if ( ! that.contains(context, obj) ) return context.runtime.getFalse(); + if ( ! that.containsImpl(obj) ) return context.runtime.getFalse(); } return context.runtime.getTrue(); } @@ -1054,16 +1050,11 @@ public RubyString inspect(ThreadContext context) { final RubyString str; if (size() == 0) { - str = RubyString.newStringLight(runtime, 16, USASCIIEncoding.INSTANCE); - inspectPrefix(str, getMetaClass()); str.cat('{').cat('}').cat('>'); // "#" - return str; + return inspectEmpty(runtime); } if (runtime.isInspecting(this)) { - str = RubyString.newStringLight(runtime, 20, USASCIIEncoding.INSTANCE); - inspectPrefix(str, getMetaClass()); - str.cat('{').cat(RECURSIVE_BYTES).cat('}').cat('>'); // "#" - return str; + return inspectRecurse(runtime); } str = RubyString.newStringLight(runtime, 32, USASCIIEncoding.INSTANCE); @@ -1079,6 +1070,19 @@ public RubyString inspect(ThreadContext context) { } } + private RubyString inspectEmpty(final Ruby runtime) { + RubyString str = RubyString.newStringLight(runtime, 16, USASCIIEncoding.INSTANCE); + inspectPrefix(str, getMetaClass()); str.cat('{').cat('}').cat('>'); // "#" + return str; + } + + private RubyString inspectRecurse(final Ruby runtime) { + RubyString str = RubyString.newStringLight(runtime, 20, USASCIIEncoding.INSTANCE); + inspectPrefix(str, getMetaClass()); + str.cat('{').cat(RECURSIVE_BYTES).cat('}').cat('>'); // "#" + return str; + } + private static RubyString inspectPrefix(final RubyString str, final RubyClass metaClass) { str.cat('#').cat('<').cat(metaClass.getRealClass().getName().getBytes(RubyEncoding.UTF8)); str.cat(':').cat(' '); return str; @@ -1103,7 +1107,13 @@ private void inspectSet(final ThreadContext context, final RubyString str) { if ( tainted ) str.setTaint(true); } - // + // pp (in __jruby/set.rb__) + + //@JRubyMethod + //public IRubyObject pretty_print_cycle(ThreadContext context, final IRubyObject pp) { + // RubyString str = isEmpty() ? inspectEmpty(context.runtime) : inspectRecurse(context.runtime); + // return pp.callMethod(context, "text", str); // pp.text ... + //} protected final Set elements() { return hash.directKeySet(); // Hash view -> no copying @@ -1118,4 +1128,100 @@ protected final void modifyCheck(final Ruby runtime) { if ((flags & FROZEN_F) != 0) throw runtime.newFrozenError("Set"); } + // java.util.Set + + public int size() { return hash.size(); } + + public boolean isEmpty() { return hash.isEmpty(); } + + public void clear() { clearImpl(); } + + public boolean contains(Object o) { + return containsImpl(toRuby(o)); + } + + public Iterator rawIterator() { + return elementsOrdered().iterator(); + } + + public Iterator iterator() { + return hash.keySet().iterator(); + } + + public Object[] toArray() { + Object[] array = new Object[size()]; int i = 0; + for ( IRubyObject elem : elementsOrdered() ) { + array[i++] = elem.toJava(Object.class); + } + return array; + } + + public Object[] toArray(final Object[] ary) { + final Class type = ary.getClass().getComponentType(); + Object[] array = ary; + if (array.length < size()) { + array = (Object[]) Array.newInstance(type, size()); + } + + int i = 0; + for ( IRubyObject elem : elementsOrdered() ) { + array[i++] = elem.toJava(type); + } + return array; + } + + public boolean add(Object element) { + final Ruby runtime = getRuntime(); + final int size = size(); + addImpl(runtime, toRuby(runtime, element)); + return size() > size; // if added + } + + public boolean remove(Object element) { + return deleteImpl(toRuby(element)); + } + + public boolean containsAll(Collection coll) { + for ( Object elem : coll ) { + if ( ! contains(elem) ) return false; + } + return true; + } + + public boolean addAll(Collection coll) { + final Ruby runtime = getRuntime(); + final int size = size(); + for ( Object elem : coll ) { + addImpl(runtime, toRuby(runtime, elem)); + } + return size() > size; // if added + } + + public boolean retainAll(Collection coll) { + final int size = size(); + for (Iterator iter = rawIterator(); iter.hasNext();) { + IRubyObject elem = iter.next(); + if ( ! coll.contains(elem.toJava(Object.class)) ) { + deleteImplIterator(elem, iter); + } + } + return size() < size; + } + + public boolean removeAll(Collection coll) { + boolean removed = false; + for ( Object elem : coll ) { + removed = remove(elem) | removed; + } + return removed; + } + + static IRubyObject toRuby(Ruby runtime, Object obj) { + return JavaUtil.convertJavaToUsableRubyObject(runtime, obj); + } + + final IRubyObject toRuby(Object obj) { + return JavaUtil.convertJavaToUsableRubyObject(getRuntime(), obj); + } + } \ No newline at end of file diff --git a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java index a1970abf599..550e8d78bf2 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java @@ -33,9 +33,7 @@ import org.jruby.runtime.*; import org.jruby.runtime.builtin.IRubyObject; -import java.util.Iterator; -import java.util.Set; -import java.util.TreeSet; +import java.util.*; import static org.jruby.RubyArray.DefaultComparator; @@ -45,7 +43,7 @@ * @author kares */ @org.jruby.anno.JRubyClass(name="SortedSet", parent = "Set") -public class RubySortedSet extends RubySet { +public class RubySortedSet extends RubySet implements SortedSet { static RubyClass createSortedSetClass(final Ruby runtime) { RubyClass SortedSet = runtime.defineClass("SortedSet", runtime.getClass("Set"), ALLOCATOR); @@ -76,11 +74,11 @@ protected ThreadContext context() { } } - private final TreeSet order; + private final TreeSet order; protected RubySortedSet(Ruby runtime, RubyClass klass) { super(runtime, klass); - order = new TreeSet(new OrderComparator(runtime)); + order = new TreeSet<>(new OrderComparator(runtime)); } @Override @@ -131,4 +129,80 @@ public RubyArray to_a(final ThreadContext context) { @Override protected Set elementsOrdered() { return order; } + @Override + public Iterator iterator() { + return new JavaIterator(); + } + + // java.util.SortedSet : + + public Comparator comparator() { + return order.comparator(); + } + + public Object first() { + return firstValue().toJava(Object.class); + } + + public IRubyObject firstValue() { + return order.first(); + } + + public Object last() { + return lastValue().toJava(Object.class); + } + + public IRubyObject lastValue() { + return order.last(); + } + + public SortedSet headSet(Object toElement) { + throw new UnsupportedOperationException("NOT IMPLEMENTED"); + } + + public SortedSet subSet(Object fromElement, Object toElement) { + throw new UnsupportedOperationException("NOT IMPLEMENTED"); + } + + public SortedSet tailSet(Object fromElement) { + throw new UnsupportedOperationException("NOT IMPLEMENTED"); + } + + public SortedSet rawHeadSet(IRubyObject toElement) { + return order.headSet(toElement); + } + + public SortedSet rawSubSet(IRubyObject fromElement, IRubyObject toElement) { + return order.subSet(fromElement, toElement); + } + + public SortedSet rawTailSet(IRubyObject fromElement) { + return order.tailSet(fromElement); + } + + private class JavaIterator implements Iterator { + + private final Iterator rawIterator; + + JavaIterator() { + rawIterator = RubySortedSet.this.order.iterator(); + } + + @Override + public boolean hasNext() { + return rawIterator.hasNext(); + } + + @Override + public Object next() { + return rawIterator.next().toJava(Object.class); + } + + @Override + public void remove() { + rawIterator.remove(); + } + + } + } From 9eddbb71d745dae7d00ed8d296f9d8dc15008743 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 9 Dec 2016 08:04:50 +0100 Subject: [PATCH 07/16] re-def SortedSet#sort directly - work around to_a re-def issues --- core/src/main/java/org/jruby/ext/set/RubySortedSet.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java index 550e8d78bf2..12e7fa27c73 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java @@ -118,10 +118,14 @@ protected void clearImpl() { order.clear(); } + @JRubyMethod(name = "sort") // re-def Enumerable#sort + public RubyArray sort(final ThreadContext context) { + return RubyArray.newArray(context.runtime, order); // instead of this.hash.keys(); + } + @Override - @JRubyMethod(name = "to_a", alias = "sort") // re-def Enumerable#sort public RubyArray to_a(final ThreadContext context) { - return RubyArray.newArray(context.runtime, order); // instead of this.hash.keys(); + return sort(context); // instead of this.hash.keys(); } // NOTE: weirdly Set/SortedSet in Ruby do not have sort! From bdcfbf0c4b4878e2b0d75e917d97628229c575de Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 9 Dec 2016 08:26:47 +0100 Subject: [PATCH 08/16] re-def SortedSet[] over Set's to produce RubySortedSet instances --- .../main/java/org/jruby/ext/set/RubySet.java | 21 ++++++++++--------- .../java/org/jruby/ext/set/RubySortedSet.java | 8 +++++++ 2 files changed, 19 insertions(+), 10 deletions(-) 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 addf69696b3..074f8aa42af 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -104,13 +104,16 @@ RubySet newSet(final Ruby runtime) { } private RubySet newSet(final ThreadContext context, final RubyClass metaClass, final RubyArray elements) { - RubySet set = new RubySet(context.runtime, metaClass); - final int len = elements.size(); - set.initHash(context.runtime, len); - for ( int i = 0; i < len; i++ ) { - set.invokeAdd(context, elements.eltInternal(i)); + final RubySet set = new RubySet(context.runtime, metaClass); + return set.initSet(context, elements.toJavaArrayMaybeUnsafe(), 0, elements.size()); + } + + final RubySet initSet(final ThreadContext context, final IRubyObject[] elements, final int off, final int len) { + initHash(context.runtime, Math.max(4, len)); + for ( int i = off; i < len; i++ ) { + invokeAdd(context, elements[i]); } - return set; + return this; } /** @@ -121,9 +124,7 @@ public static RubySet create(final ThreadContext context, IRubyObject self, IRub final Ruby runtime = context.runtime; RubySet set = new RubySet(runtime, (RubyClass) self); - set.initHash(runtime, Math.max(4, ary.length)); - for ( int i=0; i(new OrderComparator(runtime)); } + @JRubyMethod(name = "[]", rest = true, meta = true) // re-def Set[] for SortedSet + public static RubySortedSet create(final ThreadContext context, IRubyObject self, IRubyObject... ary) { + final Ruby runtime = context.runtime; + + RubySortedSet set = new RubySortedSet(runtime, (RubyClass) self); + return (RubySortedSet) set.initSet(context, ary, 0, ary.length); + } + @Override protected void addImpl(final Ruby runtime, final IRubyObject obj) { From 8c5a0310a739f8e3c1ee55483aea3bd355b18410 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 9 Dec 2016 08:28:47 +0100 Subject: [PATCH 09/16] [spec] improve SortedSet#to_a spec -> we expect sorted contents --- spec/ruby/library/set/sortedset/to_a_spec.rb | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/spec/ruby/library/set/sortedset/to_a_spec.rb b/spec/ruby/library/set/sortedset/to_a_spec.rb index f288cfb9d2c..77deb17731c 100644 --- a/spec/ruby/library/set/sortedset/to_a_spec.rb +++ b/spec/ruby/library/set/sortedset/to_a_spec.rb @@ -2,7 +2,16 @@ require 'set' describe "SortedSet#to_a" do - it "returns an array containing elements of self" do - SortedSet[1, 2, 3].to_a.sort.should == [1, 2, 3] + it "returns an array containing elements" do + set = SortedSet.new [1, 2, 3] + set.to_a.should == [1, 2, 3] + end + + it "returns a sorted array containing elements" do + set = SortedSet[2, 3, 1] + set.to_a.should == [1, 2, 3] + + set = SortedSet.new [5, 6, 4, 4] + set.to_a.should == [4, 5, 6] end end From 007e8f359abbc6d4dfaa839f53b6acf4281527d0 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 9 Dec 2016 09:59:49 +0100 Subject: [PATCH 10/16] SortedSet is (always) expected to process its elements ordered ... wouldn't care really but this is spec-ed by RubySpecs --- core/src/main/java/org/jruby/ext/set/RubySet.java | 8 ++++---- core/src/main/java/org/jruby/ext/set/RubySortedSet.java | 5 +++-- 2 files changed, 7 insertions(+), 6 deletions(-) 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 074f8aa42af..096aa41f619 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -615,7 +615,7 @@ public IRubyObject delete_if(final ThreadContext context, Block block) { return enumeratorizeWithSize(context, this, "delete_if", enumSize()); } - Iterator it = elements().iterator(); + Iterator it = elementsOrdered().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); if ( block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove @@ -629,7 +629,7 @@ public IRubyObject keep_if(final ThreadContext context, Block block) { return enumeratorizeWithSize(context, this, "keep_if", enumSize()); } - Iterator it = elements().iterator(); + Iterator it = elementsOrdered().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); if ( ! block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove @@ -658,7 +658,7 @@ public IRubyObject reject_bang(final ThreadContext context, Block block) { } final int size = size(); - Iterator it = elements().iterator(); + Iterator it = elementsOrdered().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); if ( block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove @@ -674,7 +674,7 @@ public IRubyObject select_bang(final ThreadContext context, Block block) { } final int size = size(); - Iterator it = elements().iterator(); + Iterator it = elementsOrdered().iterator(); while ( it.hasNext() ) { IRubyObject elem = it.next(); if ( ! block.yield(context, elem).isTrue() ) deleteImplIterator(elem, it); // it.remove diff --git a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java index 27c12cf2730..ce1010a5700 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java @@ -116,8 +116,9 @@ protected boolean deleteImpl(final IRubyObject obj) { @Override protected void deleteImplIterator(final IRubyObject obj, final Iterator it) { - it.remove(); // super - order.remove(obj); + super.deleteImpl(obj); + // iterator over elementsOrdered() + it.remove(); // order.remove(obj) } @Override From a6bba180792c011c71884efd11bd4eddceb7b568 Mon Sep 17 00:00:00 2001 From: kares Date: Sun, 11 Dec 2016 08:00:40 +0100 Subject: [PATCH 11/16] comparator already preforms a cmp compatibility check (and raises an ArgumentError with a more useful message) --- core/src/main/java/org/jruby/ext/set/RubySortedSet.java | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java index ce1010a5700..415f55188ee 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java @@ -91,11 +91,10 @@ public static RubySortedSet create(final ThreadContext context, IRubyObject self @Override protected void addImpl(final Ruby runtime, final IRubyObject obj) { - - if ( ! obj.respondsTo("<=>") ) { // TODO site-cache - throw runtime.newArgumentError("value must respond to <=>"); - } - + // NOTE: we're able to function without the check - comparator will raise ArgumentError + //if ( ! obj.respondsTo("<=>") ) { + // throw runtime.newArgumentError("value must respond to <=>"); + //} super.addImpl(runtime, obj); // @hash[obj] = true order.add(obj); } From 7d65e94b37e12be049e7346ddba406d1572145d0 Mon Sep 17 00:00:00 2001 From: kares Date: Sun, 11 Dec 2016 08:03:09 +0100 Subject: [PATCH 12/16] [spec] SortedSet argument error on adding 'incompatible' elements ... relaxed the respond_to?(:<=>) expectation as it seems less important --- spec/ruby/library/set/sortedset/add_spec.rb | 6 +++++- spec/ruby/library/set/sortedset/initialize_spec.rb | 4 ++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/spec/ruby/library/set/sortedset/add_spec.rb b/spec/ruby/library/set/sortedset/add_spec.rb index bdc5c077d89..13b04a2245b 100644 --- a/spec/ruby/library/set/sortedset/add_spec.rb +++ b/spec/ruby/library/set/sortedset/add_spec.rb @@ -7,9 +7,13 @@ it "takes only values which responds <=>" do obj = mock('no_comparison_operator') - obj.should_receive(:respond_to?).with(:<=>).and_return(false) + obj.stub!(:respond_to?).with(:<=>).and_return(false) lambda { SortedSet["hello"].add(obj) }.should raise_error(ArgumentError) end + + it "raises on incompatible <=> comparison" do + lambda { SortedSet['1', '2'].add(3) }.should raise_error(ArgumentError) + end end describe "SortedSet#add?" do diff --git a/spec/ruby/library/set/sortedset/initialize_spec.rb b/spec/ruby/library/set/sortedset/initialize_spec.rb index 04ad908667c..356607b2434 100644 --- a/spec/ruby/library/set/sortedset/initialize_spec.rb +++ b/spec/ruby/library/set/sortedset/initialize_spec.rb @@ -21,4 +21,8 @@ s.should include(4) s.should include(9) end + + it "raises on incompatible <=> comparison" do + lambda { SortedSet.new(['00', nil]) }.should raise_error(ArgumentError) + end end From 716b78147b7ef9a98f8c74fd6e5dcc7870b463a2 Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Jan 2017 09:49:22 +0100 Subject: [PATCH 13/16] internal refactoring in RubySet - for a clearer intent --- .../main/java/org/jruby/ext/set/RubySet.java | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) 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 096aa41f619..7b2bbc69e07 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -12,7 +12,7 @@ * implied. See the License for the specific language governing * rights and limitations under the License. * - * Copyright (C) 2016 Karol Bucek + * Copyright (C) 2016-2017 Karol Bucek * * Alternatively, the contents of this file may be used under the terms of * either of the GNU General Public License Version 2 or later (the "GPL"), @@ -81,25 +81,25 @@ protected RubySet(Ruby runtime, RubyClass klass) { /* private RubySet(Ruby runtime, RubyHash hash) { super(runtime, runtime.getClass("Set")); - initHash(hash); + allocHash(hash); } */ - final void initHash(final Ruby runtime) { - initHash(new RubyHash(runtime)); + final void allocHash(final Ruby runtime) { + setHash(new RubyHash(runtime)); } - final void initHash(final Ruby runtime, final int size) { - initHash(new RubyHash(runtime, size)); + final void allocHash(final Ruby runtime, final int size) { + setHash(new RubyHash(runtime, size)); } - final void initHash(final RubyHash hash) { + final void setHash(final RubyHash hash) { this.hash = hash; setInstanceVariable("@hash", hash); // MRI compat with set.rb } RubySet newSet(final Ruby runtime) { RubySet set = new RubySet(runtime, getMetaClass()); - set.initHash(runtime); + set.allocHash(runtime); return set; } @@ -109,7 +109,7 @@ private RubySet newSet(final ThreadContext context, final RubyClass metaClass, f } final RubySet initSet(final ThreadContext context, final IRubyObject[] elements, final int off, final int len) { - initHash(context.runtime, Math.max(4, len)); + allocHash(context.runtime, Math.max(4, len)); for ( int i = off; i < len; i++ ) { invokeAdd(context, elements[i]); } @@ -135,7 +135,7 @@ public IRubyObject initialize(ThreadContext context, Block block) { if ( block.isGiven() && context.runtime.isVerbose() ) { context.runtime.getWarnings().warning(IRubyWarnings.ID.BLOCK_UNUSED, "given block not used"); } - initHash(context.runtime); + allocHash(context.runtime); return this; } @@ -150,7 +150,7 @@ public IRubyObject initialize(ThreadContext context, IRubyObject enume, Block bl return initWithEnum(context, enume, block); } - initHash(context.runtime); + allocHash(context.runtime); return callMethod(context, "merge", enume); // TODO site-cache } @@ -165,7 +165,7 @@ protected IRubyObject initialize(ThreadContext context, IRubyObject[] args, Bloc private IRubyObject initWithEnum(final ThreadContext context, final IRubyObject enume, final Block block) { if ( enume instanceof RubyArray ) { RubyArray ary = (RubyArray) enume; - initHash(context.runtime, ary.size()); + allocHash(context.runtime, ary.size()); for ( int i = 0; i < ary.size(); i++ ) { invokeAdd(context, block.yield(context, ary.eltInternal(i))); } @@ -174,7 +174,7 @@ private IRubyObject initWithEnum(final ThreadContext context, final IRubyObject if ( enume instanceof RubySet ) { RubySet set = (RubySet) enume; - initHash(context.runtime, set.size()); + allocHash(context.runtime, set.size()); for ( IRubyObject elem : set.elementsOrdered() ) { invokeAdd(context, block.yield(context, elem)); } @@ -183,7 +183,7 @@ private IRubyObject initWithEnum(final ThreadContext context, final IRubyObject final Ruby runtime = context.runtime; - initHash(runtime); + allocHash(runtime); // set.rb do_with_enum : return doWithEnum(context, enume, new EachBody(runtime) { @@ -237,14 +237,14 @@ protected final IRubyObject doYield(ThreadContext context, Block block, IRubyObj @JRubyMethod public IRubyObject initialize_dup(ThreadContext context, IRubyObject orig) { super.initialize_copy(orig); - initHash((RubyHash) (((RubySet) orig).hash).dup(context)); + setHash((RubyHash) (((RubySet) orig).hash).dup(context)); return this; } @JRubyMethod public IRubyObject initialize_clone(ThreadContext context, IRubyObject orig) { super.initialize_copy(orig); - initHash((RubyHash) (((RubySet) orig).hash).rbClone(context)); + setHash((RubyHash) (((RubySet) orig).hash).rbClone(context)); return this; } @@ -767,21 +767,21 @@ public IRubyObject op_and(final ThreadContext context, IRubyObject enume) { final RubySet newSet = new RubySet(runtime, getMetaClass()); if ( enume instanceof RubySet ) { - newSet.initHash(runtime, ((RubySet) enume).size()); + newSet.allocHash(runtime, ((RubySet) enume).size()); for ( IRubyObject obj : ((RubySet) enume).elementsOrdered() ) { if ( containsImpl(obj) ) newSet.addImpl(runtime, obj); } } else if ( enume instanceof RubyArray ) { RubyArray ary = (RubyArray) enume; - newSet.initHash(runtime, ary.size()); + newSet.allocHash(runtime, ary.size()); for ( int i = 0; i < ary.size(); i++ ) { final IRubyObject obj = ary.eltInternal(i); if ( containsImpl(obj) ) newSet.addImpl(runtime, obj); } } else { - newSet.initHash(runtime); + newSet.allocHash(runtime); // do_with_enum(enum) { |o| newSet.add(o) if include?(o) } doWithEnum(context, enume, new EachBody(runtime) { IRubyObject yieldImpl(ThreadContext context, IRubyObject obj) { @@ -916,7 +916,7 @@ public IRubyObject divide(ThreadContext context, final Block block) { RubyHash vals = (RubyHash) classify(context, block); final RubySet set = new RubySet(runtime, runtime.getClass("Set")); - set.initHash(runtime, vals.size()); + set.allocHash(runtime, vals.size()); for ( IRubyObject val : (Collection) vals.directValues() ) { set.invokeAdd(context, val); } @@ -952,7 +952,7 @@ private IRubyObject divideTSort(ThreadContext context, final Block block) { */ final RubyClass Set = runtime.getClass("Set"); final RubySet set = new RubySet(runtime, Set); - set.initHash(runtime, dig.size()); + set.allocHash(runtime, dig.size()); dig.callMethod(context, "each_strongly_connected_component", IRubyObject.NULL_ARRAY, new Block( new JavaInternalBlockBody(runtime, Signature.ONE_REQUIRED) { @Override From 325a60dfa38cbe3db2e39183ec085c53feddddcc Mon Sep 17 00:00:00 2001 From: kares Date: Fri, 6 Jan 2017 11:49:01 +0100 Subject: [PATCH 14/16] final frontier for MRI compat - make sure marshal-ing works ... necessary since we use some Java internal structures its been implemented in Java, could have been done with _load/_dump although would require exposing some of the Set/SortedSet internals --- .../main/java/org/jruby/ext/set/RubySet.java | 30 +++++ .../java/org/jruby/ext/set/RubySortedSet.java | 7 + test/jruby/test_set.rb | 121 ++++++++++++++++++ 3 files changed, 158 insertions(+) create mode 100644 test/jruby/test_set.rb 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 7b2bbc69e07..c152b1fc582 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -35,8 +35,11 @@ import org.jruby.javasupport.JavaUtil; import org.jruby.runtime.*; import org.jruby.runtime.builtin.IRubyObject; +import org.jruby.runtime.marshal.MarshalStream; +import org.jruby.runtime.marshal.UnmarshalStream; import org.jruby.util.ArraySupport; +import java.io.IOException; import java.lang.reflect.Array; import java.util.Collection; import java.util.IdentityHashMap; @@ -61,11 +64,38 @@ static RubyClass createSetClass(final Ruby runtime) { Set.includeModule(runtime.getEnumerable()); Set.defineAnnotatedMethods(RubySet.class); + Set.setMarshal(new SetMarshal(Set.getMarshal())); + runtime.getLoadService().require("jruby/set.rb"); return Set; } + // custom Set marshaling without _marshal_dump and _marshal_load for maximum compatibility + private static final class SetMarshal implements ObjectMarshal { + + protected final ObjectMarshal defaultMarshal; + + SetMarshal(ObjectMarshal defaultMarshal) { + this.defaultMarshal = defaultMarshal; + } + + public void marshalTo(Ruby runtime, Object obj, RubyClass type, MarshalStream marshalStream) throws IOException { + defaultMarshal.marshalTo(runtime, obj, type, marshalStream); + } + + public Object unmarshalFrom(Ruby runtime, RubyClass type, UnmarshalStream unmarshalStream) throws IOException { + Object result = defaultMarshal.unmarshalFrom(runtime, type, unmarshalStream); + ((RubySet) result).unmarshal(); + return result; + } + + } + + void unmarshal() { + this.hash = (RubyHash) getInstanceVariable("@hash"); + } + private static final ObjectAllocator ALLOCATOR = new ObjectAllocator() { public RubySet allocate(Ruby runtime, RubyClass klass) { return new RubySet(runtime, klass); diff --git a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java index 415f55188ee..819c1cf9ce6 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySortedSet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySortedSet.java @@ -81,6 +81,13 @@ protected RubySortedSet(Ruby runtime, RubyClass klass) { order = new TreeSet<>(new OrderComparator(runtime)); } + @Override + void unmarshal() { + super.unmarshal(); + IRubyObject[] elems = hash.keys().toJavaArrayMaybeUnsafe(); + for ( int i=0; i', set.inspect + + assert_false Set.new.equal?(SubSet.new) + assert_true Set.new.eql?(SubSet.new) + assert_true ( SubSet.new == Set.new ) + + assert_false SortedSet.new.equal?(SubSortedSet.new) + assert_true SortedSet.new.eql?(SubSortedSet.new) + assert_true ( SubSortedSet.new == Set.new ) + assert_true ( SubSortedSet.new == SortedSet.new ) + assert_true ( SortedSet.new == SubSortedSet.new ) + end + + def test_allocate + set = Set.allocate + assert_same Set, set.class + assert_nil set.instance_variable_get(:@hash) # same on MRI + + # set not really usable : + begin + set << 1 ; fail 'set << 1 did not fail!' + rescue # NoMethodError # JRuby: NPE + # NoMethodError: undefined method `[]=' for nil:NilClass + # from /opt/local/rvm/rubies/ruby-2.3.3/lib/ruby/2.3.0/set.rb:313:in `add' + end + end + + def test_marshal_dump + assert_equal "\x04\b{\x00".force_encoding('ASCII-8BIT'), Marshal.dump(Hash.new) + + # MRI internally uses a @hash with a default: `Hash.new(false)' + #empty_set = "\x04\bo:\bSet\x06:\n@hash}\x00F".force_encoding('ASCII-8BIT') + #assert_equal empty_set, Marshal.dump(Set.new) + + dump = Marshal.dump(Set.new) + assert_equal Set.new, Marshal.load(dump) + + set = Marshal.load Marshal.dump(Set.new([1, 2])) + assert_same Set, set.class + assert_equal Set.new([1, 2]), set + set << 3 + assert_equal 3, set.size + + set = Marshal.load Marshal.dump(Set.new([1, 2]).dup) + assert_same Set, set.class + assert_equal Set.new([1, 2]), set + end + + def test_sorted_marshal_dump + dump = Marshal.dump(SortedSet.new) + assert_equal SortedSet.new, Marshal.load(dump) + + set = Marshal.load Marshal.dump(SortedSet.new([2, 1])) + assert_same SortedSet, set.class + assert_equal SortedSet.new([1, 2]), set + assert_equal [1, 2], set.sort + set << 3 + assert_equal 3, set.size + assert_equal [1, 2, 3], set.sort + + set = Marshal.load Marshal.dump(SortedSet.new([2, 1]).dup) + assert_same SortedSet, set.class + assert_equal SortedSet.new([1, 2]), set + assert_equal [1, 2], set.to_a + + set = Marshal.load Marshal.dump(SortedSet.new([2, 3, 1])) + each = []; set.each { |e| each << e } + assert_equal [1, 2, 3], each + end + + def test_dup + set = Set.new [1, 2] + assert_same Set, set.dup.class + assert_equal set, set.dup + dup = set.dup + set << 3 + assert_equal 3, set.size + assert_equal 2, dup.size + + set = SortedSet.new [1, 2] + assert_same SortedSet, set.dup.class + assert_equal set, set.dup + dup = set.dup + set << 0 + assert_equal 3, set.size + assert_equal 2, dup.size + end + + def test_to_java + assert set = Set.new.to_java + assert set.toString.start_with?('# Date: Mon, 26 Jun 2017 22:19:39 +0200 Subject: [PATCH 15/16] assure Set marshal-ing is compatible at the binary level with previous ... and thus MRI's (pure Ruby) set.rb implementation as well important with Rails using Sprockets at its marshalling Set instances --- core/src/main/java/org/jruby/ext/set/RubySet.java | 7 +++++-- test/jruby/test_set.rb | 6 +++--- 2 files changed, 8 insertions(+), 5 deletions(-) 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 c152b1fc582..ac7a426d53d 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -114,12 +114,15 @@ private RubySet(Ruby runtime, RubyHash hash) { allocHash(hash); } */ + // since MRI uses Hash.new(false) we'll (initially) strive for maximum compatibility + // ... this is important with Rails using Sprockets at its marshalling Set instances + final void allocHash(final Ruby runtime) { - setHash(new RubyHash(runtime)); + setHash(new RubyHash(runtime, runtime.getFalse())); } final void allocHash(final Ruby runtime, final int size) { - setHash(new RubyHash(runtime, size)); + setHash(new RubyHash(runtime, runtime.getFalse(), size)); } final void setHash(final RubyHash hash) { diff --git a/test/jruby/test_set.rb b/test/jruby/test_set.rb index d1c9ed0e1e9..acd11fa8e92 100644 --- a/test/jruby/test_set.rb +++ b/test/jruby/test_set.rb @@ -46,8 +46,8 @@ def test_marshal_dump assert_equal "\x04\b{\x00".force_encoding('ASCII-8BIT'), Marshal.dump(Hash.new) # MRI internally uses a @hash with a default: `Hash.new(false)' - #empty_set = "\x04\bo:\bSet\x06:\n@hash}\x00F".force_encoding('ASCII-8BIT') - #assert_equal empty_set, Marshal.dump(Set.new) + empty_set = "\x04\bo:\bSet\x06:\n@hash}\x00F".force_encoding('ASCII-8BIT') + assert_equal empty_set, Marshal.dump(Set.new) dump = Marshal.dump(Set.new) assert_equal Set.new, Marshal.load(dump) @@ -116,6 +116,6 @@ def test_to_java assert set.is_a?(java.util.Set) assert set.is_a?(java.util.SortedSet) assert_equal java.util.TreeSet.new([1, 2]), set - end + end if defined? JRUBY_VERSION end From f9d0d0f02ea0cc4facefdadf4ef188e8e22e6767 Mon Sep 17 00:00:00 2001 From: kares Date: Tue, 27 Jun 2017 08:23:17 +0200 Subject: [PATCH 16/16] delegate Set#hash directly, for equals/hashCode rely on super dispatch --- core/src/main/java/org/jruby/ext/set/RubySet.java | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) 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 ac7a426d53d..3b1a86ec160 100644 --- a/core/src/main/java/org/jruby/ext/set/RubySet.java +++ b/core/src/main/java/org/jruby/ext/set/RubySet.java @@ -864,8 +864,6 @@ public IRubyObject op_equal(ThreadContext context, IRubyObject other) { return context.runtime.getFalse(); } - // TODO Java (Collection) equals ! - @JRubyMethod(name = "eql?") public IRubyObject op_eql(ThreadContext context, IRubyObject other) { if ( other instanceof RubySet ) { @@ -886,15 +884,9 @@ public boolean eql(IRubyObject other) { @Override @JRubyMethod public RubyFixnum hash() { // @hash.hash - return getRuntime().newFixnum(hashCode()); + return hash.hash(); } - @Override - public int hashCode() { - return hash.hashCode(); - } - - @JRubyMethod(name = "classify") public IRubyObject classify(ThreadContext context, final Block block) { if ( ! block.isGiven() ) {