diff --git a/pom.xml b/pom.xml index badc8da2..bb1d6f1c 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ io.usethesource vallang - 0.15.2-SNAPSHOT + 1.0.0-SNAPSHOT jar diff --git a/src/main/java/io/usethesource/vallang/IBool.java b/src/main/java/io/usethesource/vallang/IBool.java index 21e6e47a..1b7ac29a 100644 --- a/src/main/java/io/usethesource/vallang/IBool.java +++ b/src/main/java/io/usethesource/vallang/IBool.java @@ -13,6 +13,16 @@ import io.usethesource.vallang.visitors.IValueVisitor; public interface IBool extends IValue { + @Override + default int getMatchFingerprint() { + if (getValue()) { + return 3569038; /* "true".hashCode() */ + } + else { + return 97196323; /* "false".hashCode() */ + } + } + boolean getValue(); String getStringRepresentation(); IBool and(IBool other); diff --git a/src/main/java/io/usethesource/vallang/IConstructor.java b/src/main/java/io/usethesource/vallang/IConstructor.java index 305eca65..13566ca6 100644 --- a/src/main/java/io/usethesource/vallang/IConstructor.java +++ b/src/main/java/io/usethesource/vallang/IConstructor.java @@ -25,6 +25,11 @@ */ public interface IConstructor extends INode { + @Override + default int getMatchFingerprint() { + return getName().hashCode() + 131 * arity(); + } + /** * @return the specific ConstructorType of this constructor */ diff --git a/src/main/java/io/usethesource/vallang/IExternalValue.java b/src/main/java/io/usethesource/vallang/IExternalValue.java index f3678054..19364139 100644 --- a/src/main/java/io/usethesource/vallang/IExternalValue.java +++ b/src/main/java/io/usethesource/vallang/IExternalValue.java @@ -37,6 +37,12 @@ * Note that NORMAL USE OF THE PDB DOES NOT REQUIRE IMPLEMENTING THIS INTERFACE */ public interface IExternalValue extends IValue { + /** + * External values must re-think their pattern match fingerprint, + * instead of returning `IValue.hashCode()` automatically. + */ + @Override + int getMatchFingerprint(); /** * @return an ExternalType diff --git a/src/main/java/io/usethesource/vallang/IInteger.java b/src/main/java/io/usethesource/vallang/IInteger.java index 2f4756ee..5fd7e6d6 100644 --- a/src/main/java/io/usethesource/vallang/IInteger.java +++ b/src/main/java/io/usethesource/vallang/IInteger.java @@ -15,6 +15,16 @@ import io.usethesource.vallang.visitors.IValueVisitor; public interface IInteger extends INumber { + @Override + default int getMatchFingerprint() { + if (signum() == 0) { + return 104431; /* "int".hashCode() */ + } + else { + return hashCode(); + } + } + /** * @return this + other; */ diff --git a/src/main/java/io/usethesource/vallang/IList.java b/src/main/java/io/usethesource/vallang/IList.java index 496e232d..7335d755 100644 --- a/src/main/java/io/usethesource/vallang/IList.java +++ b/src/main/java/io/usethesource/vallang/IList.java @@ -22,6 +22,12 @@ import io.usethesource.vallang.visitors.IValueVisitor; public interface IList extends ICollection { + + @Override + default int getMatchFingerprint() { + return 3322014; // "list".hashCode() + } + /** * @return the number of elements in the list */ diff --git a/src/main/java/io/usethesource/vallang/IMap.java b/src/main/java/io/usethesource/vallang/IMap.java index 46b31371..5af85c1c 100644 --- a/src/main/java/io/usethesource/vallang/IMap.java +++ b/src/main/java/io/usethesource/vallang/IMap.java @@ -26,6 +26,11 @@ public interface IMap extends ICollection { + @Override + default int getMatchFingerprint() { + return 107868; // "map".hashCode() + } + /** * Adds a new entry to the map, mapping the key to value. If the * key existed before, the old value will be lost. diff --git a/src/main/java/io/usethesource/vallang/INode.java b/src/main/java/io/usethesource/vallang/INode.java index bfe51cdb..623e8732 100644 --- a/src/main/java/io/usethesource/vallang/INode.java +++ b/src/main/java/io/usethesource/vallang/INode.java @@ -30,6 +30,14 @@ * it recursively. */ public interface INode extends IValue, Iterable { + + @Override + default int getMatchFingerprint() { + int hash = getName().hashCode(); + + return hash == 0 ? 13547528 /* "node".hashCode() << 2*/ + arity() : hash + 131 * arity(); + } + /** * Get a child * @param i the zero based index of the child diff --git a/src/main/java/io/usethesource/vallang/IReal.java b/src/main/java/io/usethesource/vallang/IReal.java index 6e471108..346dff40 100644 --- a/src/main/java/io/usethesource/vallang/IReal.java +++ b/src/main/java/io/usethesource/vallang/IReal.java @@ -16,6 +16,12 @@ import io.usethesource.vallang.visitors.IValueVisitor; public interface IReal extends INumber { + @Override + default int getMatchFingerprint() { + int hash = hashCode(); + return hash == 0 ? 3496350 /* real.hashCode() */ : hash; + } + /** * @return this + other; */ diff --git a/src/main/java/io/usethesource/vallang/ISet.java b/src/main/java/io/usethesource/vallang/ISet.java index 3fd05cf8..85158536 100644 --- a/src/main/java/io/usethesource/vallang/ISet.java +++ b/src/main/java/io/usethesource/vallang/ISet.java @@ -21,6 +21,11 @@ public interface ISet extends ICollection { + @Override + default int getMatchFingerprint() { + return 113762; // "set".hashCode() + } + /** * Add an element to the set. * @param element diff --git a/src/main/java/io/usethesource/vallang/IString.java b/src/main/java/io/usethesource/vallang/IString.java index 8198be0e..924790be 100644 --- a/src/main/java/io/usethesource/vallang/IString.java +++ b/src/main/java/io/usethesource/vallang/IString.java @@ -19,6 +19,17 @@ import io.usethesource.vallang.visitors.IValueVisitor; public interface IString extends IValue, Iterable { + + @Override + default int getMatchFingerprint() { + if (length() == 0) { + return 114225; /* "str".hashCode() */ + } + else { + return hashCode(); + } + } + /** * @return the Java string that this string represents */ diff --git a/src/main/java/io/usethesource/vallang/ITuple.java b/src/main/java/io/usethesource/vallang/ITuple.java index 35d01645..97ee8f01 100644 --- a/src/main/java/io/usethesource/vallang/ITuple.java +++ b/src/main/java/io/usethesource/vallang/ITuple.java @@ -16,6 +16,11 @@ import io.usethesource.vallang.visitors.IValueVisitor; public interface ITuple extends Iterable, IValue { + @Override + default int getMatchFingerprint() { + return 442900256 /* "tuple".hashCode() << 2 */ + arity(); + } + /** * Retrieve the given field at the given index. * diff --git a/src/main/java/io/usethesource/vallang/IValue.java b/src/main/java/io/usethesource/vallang/IValue.java index 1af86064..ba4674b5 100644 --- a/src/main/java/io/usethesource/vallang/IValue.java +++ b/src/main/java/io/usethesource/vallang/IValue.java @@ -28,7 +28,35 @@ public interface IValue { * @return the {@link Type} of a value */ public Type getType(); + + /** + * This method is used exclusively by code generated by the Rascal compiler, + * or by the Rascal interpreter. The returned integer codes are opaque, although stable. + * If you need to know what kind of value you have, use the IValueVisitor or the ITypeVisitor + * interfaces and the `accept` methods on IValue and Type. + * + * @return an integer code that: + * * accurate reflects the identity of the top-level structure of this value + * * such that if pattern.match(this) ===> pattern.getPatternMatchFingerprint() == this.getPatternMatchFingerprint() + * * distinguishes maximally between different kinds of values + * * never makes the same or similar value have a different fingerprint + */ + default int getMatchFingerprint() { + return hashCode(); + } + /** + * This method is used exclusively by code generated by the Rascal compiler, + * + * @return an integer code that: + * * is guaranteed to be different from `getMatchFingerPrint` + * * is guaranteed to be constant + * * is guaranteed to be the same for every IValue + */ + static int getDefaultMatchFingerprint() { + return 0; + } + /** * Execute the {@link IValueVisitor} on the current node * diff --git a/src/main/java/io/usethesource/vallang/impl/persistent/PersistentHashIndexedBinaryRelation.java b/src/main/java/io/usethesource/vallang/impl/persistent/PersistentHashIndexedBinaryRelation.java index 136a3956..ad71af11 100644 --- a/src/main/java/io/usethesource/vallang/impl/persistent/PersistentHashIndexedBinaryRelation.java +++ b/src/main/java/io/usethesource/vallang/impl/persistent/PersistentHashIndexedBinaryRelation.java @@ -38,7 +38,6 @@ import io.usethesource.vallang.ISetWriter; import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; -import io.usethesource.vallang.exceptions.IllegalOperationException; import io.usethesource.vallang.type.Type; import io.usethesource.vallang.util.AbstractTypeBag; diff --git a/src/main/java/io/usethesource/vallang/impl/primitive/IntegerValue.java b/src/main/java/io/usethesource/vallang/impl/primitive/IntegerValue.java index 2178c16b..d2bce4b2 100644 --- a/src/main/java/io/usethesource/vallang/impl/primitive/IntegerValue.java +++ b/src/main/java/io/usethesource/vallang/impl/primitive/IntegerValue.java @@ -519,6 +519,7 @@ else if (isRationalType(other)) { } } + @Override public int hashCode(){ int h = value ^ 0x85ebca6b; // based on the final Avalanching phase of MurmurHash2 diff --git a/src/test/java/io/usethesource/vallang/ValueProvider.java b/src/test/java/io/usethesource/vallang/ValueProvider.java index 6331dfcb..56f9a756 100644 --- a/src/test/java/io/usethesource/vallang/ValueProvider.java +++ b/src/test/java/io/usethesource/vallang/ValueProvider.java @@ -337,8 +337,26 @@ private RandomTypesConfig configureRandomTypes(TypeConfig typeConfig, int depth) * @return an instance assignable to `cl` */ private IValue generateValue(IValueFactory vf, TypeStore ts, Class cl, ExpectedType expected, int depth, int width) { - Type expectedType = expected != null ? readType(ts, expected) : types.getOrDefault(cl, (x, n) -> tf.valueType()).apply(ts, expected); + Type expectedType = tf.voidType(); + + // this should terminate through random selection. + // only tuple types with nested void arguments can reduce to void. + int i = 0; + while (expectedType.isBottom() && i++ < 1000) { + if (expected != null) { + expectedType = readType(ts, expected); + break; + } + else { + expectedType = types + .getOrDefault(cl, (x, n) -> tf.valueType()) + .apply(ts, expected); + } + } + + assert !expectedType.isBottom() : cl + " generated void type?"; + if (previous != null && rnd.nextInt(4) == 0 && previous.getType().isSubtypeOf(expectedType)) { return rnd.nextBoolean() ? previous : reinstantiate(vf, ts, previous); } diff --git a/src/test/java/io/usethesource/vallang/specification/IValueTests.java b/src/test/java/io/usethesource/vallang/specification/IValueTests.java index 32b49d94..c3d23d86 100644 --- a/src/test/java/io/usethesource/vallang/specification/IValueTests.java +++ b/src/test/java/io/usethesource/vallang/specification/IValueTests.java @@ -2,15 +2,30 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import java.io.IOException; import java.io.StringReader; +import java.util.PrimitiveIterator.OfInt; +import java.util.function.IntConsumer; +import java.util.stream.Stream; +import java.util.stream.StreamSupport; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource; import io.usethesource.vallang.IConstructor; +import io.usethesource.vallang.IInteger; +import io.usethesource.vallang.IList; +import io.usethesource.vallang.IMap; +import io.usethesource.vallang.INode; +import io.usethesource.vallang.IRational; +import io.usethesource.vallang.IReal; +import io.usethesource.vallang.ISet; +import io.usethesource.vallang.ISourceLocation; +import io.usethesource.vallang.IString; +import io.usethesource.vallang.ITuple; import io.usethesource.vallang.IValue; import io.usethesource.vallang.IValueFactory; import io.usethesource.vallang.ValueProvider; @@ -44,6 +59,154 @@ public void testHashCodeContract(IValue val1, IValue val2) { assertTrue(!val1.equals(val2) || val1.hashCode() == val2.hashCode()); } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintContract(IValue val1, IValue val2) { + if (val1.equals(val2)) { + assertEquals(val1.getMatchFingerprint(), val2.getMatchFingerprint(), "" + val1.toString() + " and " + val2.toString() + " are equal but do not have the same fingerprint?"); + } + assertTrue(!val1.equals(val2) || val1.getMatchFingerprint() == val2.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testDefaultFingerprintContracts(IValue val1) { + assertEquals(IValue.getDefaultMatchFingerprint(), 0); + assertNotEquals(IValue.getDefaultMatchFingerprint(), val1.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityIntegersDoNotChangeTheTest(IValueFactory vf, IInteger integer) { + assertEquals(integer.equals(vf.integer(0)) ? "int".hashCode() : integer.hashCode(), integer.getMatchFingerprint()); + + // this should stay or we have to make sure that the fingerprint works like that again + // if it changes + if (!integer.equals(vf.integer(0)) && integer.less(vf.integer(Integer.MAX_VALUE)).getValue() && integer.greater(vf.integer(Integer.MIN_VALUE)).getValue()) { + // copied the implementation of IntegerValue.hashCode here + // because this is now officially a contract. + int hash = integer.intValue() ^ 0x85ebca6b; + hash ^= hash >>> 13; + hash *= 0x5bd1e995; + hash ^= hash >>> 15; + + assertEquals(hash, integer.getMatchFingerprint()); + } + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityStringDoNotChangeTheTest(IString string) { + assertEquals(string.length() == 0 ? "str".hashCode() : string.hashCode(), string.getMatchFingerprint()); + + // we copied the generic hashCode implementation here, to check the contract. + int h = 0; + OfInt it = string.iterator(); + + while (it.hasNext()) { + int c = it.nextInt(); + + if (!Character.isBmpCodePoint(c)) { + h = 31 * h + Character.highSurrogate(c); + h = 31 * h + Character.lowSurrogate(c); + } else { + h = 31 * h + ((char) c); + } + } + + if (string.length() != 0) { + assertEquals(h, string.getMatchFingerprint()); + } + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityRealDoNotChangeTheTest(IReal real) { + assertEquals(real.hashCode() == 0 ? "real".hashCode() : real.hashCode(), real.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityRationalDoNotChangeTheTest(IRational rational) { + assertEquals(rational.hashCode(), rational.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityListDoNotChangeTheTest(IList list) { + assertEquals("list".hashCode(), list.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintAllListsTheSameDoNotChangeTheTest(IList list1, IList list2) { + assertEquals(list1.getMatchFingerprint(), list2.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilitySetDoNotChangeTheTest(ISet set) { + assertEquals("set".hashCode(), set.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintAllSetsTheSameDoNotChangeTheTest(ISet set1, ISet set2) { + assertEquals(set1.getMatchFingerprint(), set2.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityMapDoNotChangeTheTest(IMap map) { + assertEquals("map".hashCode(), map.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintAllMapsTheSameDoNotChangeTheTest(IMap map1, IMap map2) { + assertEquals(map1.getMatchFingerprint(), map2.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityTupleDoNotChangeTheTest(ITuple tuple) { + assertEquals(("tuple".hashCode() << 2) + tuple.arity(), tuple.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintEqualArityTuplesTheSameDoNotChangeTheTest(ITuple tuple1, ITuple tuple2) { + if (tuple1.arity() == tuple2.arity()) { + assertEquals(tuple1.getMatchFingerprint(), tuple2.getMatchFingerprint()); + } + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityNodeDoNotChangeTheTest(ISourceLocation node) { + assertEquals(node.hashCode(), node.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityNodeDoNotChangeTheTest(INode node) { + assertEquals(node.getName().hashCode() == 0 + ? ("node".hashCode() << 2) + node.arity() + : node.getName().hashCode() + 131 * node.arity(), node.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintEqualArityNodesTheSameDoNotChangeTheTest(INode node1, INode node2) { + if (node1.arity() == node2.arity() && node1.getName().equals(node2.getName())) { + assertEquals(node1.getMatchFingerprint(), node2.getMatchFingerprint()); + } + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityNodesMatchConstructorsDoNotChangeTheTest(IValueFactory vf, IConstructor constructor) { + assertEquals( + constructor.getMatchFingerprint(), + vf.node(constructor.getName(), StreamSupport.stream(constructor.getChildren().spliterator(), false).toArray(IValue[]::new)).getMatchFingerprint() + ); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintStabilityConstructorDoNotChangeTheTest(IConstructor constructor) { + assertEquals(constructor.getName().hashCode() + 131 * constructor.arity(), constructor.getMatchFingerprint()); + } + + @ParameterizedTest @ArgumentsSource(ValueProvider.class) + public void testFingerprintEqualArityConstructorsTheSameDoNotChangeTheTest(IConstructor node1, IConstructor node2) { + if (node1.arity() == node2.arity()) { + assertEquals(node1.getMatchFingerprint(), node2.getMatchFingerprint()); + } + } + @ParameterizedTest @ArgumentsSource(ValueProvider.class) public void testWysiwyg(IValueFactory vf, TypeStore store, IValue val) throws FactTypeUseException, IOException { StandardTextReader reader = new StandardTextReader(); diff --git a/src/test/java/io/usethesource/vallang/specification/SetTests.java b/src/test/java/io/usethesource/vallang/specification/SetTests.java index f316be2e..5a872862 100644 --- a/src/test/java/io/usethesource/vallang/specification/SetTests.java +++ b/src/test/java/io/usethesource/vallang/specification/SetTests.java @@ -4,8 +4,6 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import java.util.Random; -import java.util.stream.StreamSupport; - import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ArgumentsSource;