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 extends IValue> 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;