From 960af83c7c1cb871da03b9ac4ec6f44c94e78a1d Mon Sep 17 00:00:00 2001 From: Kristóf Marussy Date: Sun, 30 Oct 2022 19:27:34 -0400 Subject: refactor: DNF atoms Restore count != capability. Still needs semantics and tests for count atoms over partial models. --- .../refinery/store/map/internal/ImmutableNode.java | 8 +-- .../refinery/store/map/internal/MapDiffCursor.java | 4 +- .../refinery/store/map/internal/MutableNode.java | 18 +++--- .../tools/refinery/store/map/internal/Node.java | 20 +++---- .../store/query/atom/AbstractCallAtom.java | 30 ---------- .../refinery/store/query/atom/BasicCallKind.java | 26 +++++++++ .../tools/refinery/store/query/atom/CallAtom.java | 66 ++++++++++++++++++++++ .../tools/refinery/store/query/atom/CallKind.java | 23 ++------ .../store/query/atom/ComparisonOperator.java | 22 ++++++++ .../refinery/store/query/atom/ConstantAtom.java | 12 ++++ .../refinery/store/query/atom/CountCallKind.java | 13 +++++ .../store/query/atom/CountNotEqualsAtom.java | 31 ++++++++++ .../refinery/store/query/atom/DNFCallAtom.java | 40 ------------- .../refinery/store/query/atom/ModalRelation.java | 17 ++++++ .../store/query/atom/ModalRelationAtom.java | 38 ------------- .../tools/refinery/store/query/atom/Modality.java | 7 +++ .../refinery/store/query/atom/RelationAtom.java | 32 ----------- .../store/map/tests/utils/MapTestEnvironment.java | 14 ++--- 18 files changed, 231 insertions(+), 190 deletions(-) delete mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/AbstractCallAtom.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/BasicCallKind.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/CallAtom.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/ComparisonOperator.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/ConstantAtom.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/CountCallKind.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/CountNotEqualsAtom.java delete mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/DNFCallAtom.java create mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelation.java delete mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelationAtom.java delete mode 100644 subprojects/store/src/main/java/tools/refinery/store/query/atom/RelationAtom.java (limited to 'subprojects/store/src') diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/internal/ImmutableNode.java b/subprojects/store/src/main/java/tools/refinery/store/map/internal/ImmutableNode.java index f68734ab..9397dede 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/map/internal/ImmutableNode.java +++ b/subprojects/store/src/main/java/tools/refinery/store/map/internal/ImmutableNode.java @@ -35,7 +35,7 @@ public class ImmutableNode extends Node { /** * Constructor that copies a mutable node to an immutable. - * + * * @param node A mutable node. * @param cache A cache of existing immutable nodes. It can be used to search * and place reference immutable nodes. It can be null, if no cache @@ -151,7 +151,7 @@ public class ImmutableNode extends Node { oldValue.setOldValue(value); return this; } else { - // update existing value + // update existing nodeId MutableNode mutable = this.toMutable(); return mutable.updateValue(value, oldValue, selectedHashFragment); } @@ -161,7 +161,7 @@ public class ImmutableNode extends Node { oldValue.setOldValue(defaultValue); return this; } else { - // add new key + value + // add new key + nodeId MutableNode mutable = this.toMutable(); return mutable.putValue(key, value, oldValue, hashProvider, defaultValue, hash, depth); } @@ -182,7 +182,7 @@ public class ImmutableNode extends Node { return mutable.updateWithSubNode(selectedHashFragment, newsubNode, value.equals(defaultValue)); } } else { - // add new key + value + // add new key + nodeId MutableNode mutable = this.toMutable(); return mutable.putValue(key, value, oldValue, hashProvider, defaultValue, hash, depth); } diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapDiffCursor.java b/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapDiffCursor.java index 42333635..a4ca813c 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapDiffCursor.java +++ b/subprojects/store/src/main/java/tools/refinery/store/map/internal/MapDiffCursor.java @@ -10,13 +10,13 @@ import tools.refinery.store.map.VersionedMap; /** * A cursor representing the difference between two states of a map. - * + * * @author Oszkar Semerath * */ public class MapDiffCursor implements DiffCursor, Cursor { /** - * Default value representing missing elements. + * Default nodeId representing missing elements. */ private V defaultValue; private MapCursor cursor1; diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/internal/MutableNode.java b/subprojects/store/src/main/java/tools/refinery/store/map/internal/MutableNode.java index 54853010..7c3cf7e8 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/map/internal/MutableNode.java +++ b/subprojects/store/src/main/java/tools/refinery/store/map/internal/MutableNode.java @@ -31,7 +31,7 @@ public class MutableNode extends Node { /** * Constructs a {@link MutableNode} as a copy of an {@link ImmutableNode} - * + * * @param node */ protected MutableNode(ImmutableNode node) { @@ -107,18 +107,18 @@ public class MutableNode extends Node { } } } else { - // If it does not have key, check for value + // If it does not have key, check for nodeId @SuppressWarnings("unchecked") var nodeCandidate = (Node) content[2 * selectedHashFragment + 1]; if (nodeCandidate != null) { - // If it has value, it is a subnode -> upate that + // If it has nodeId, it is a subnode -> upate that var newNode = nodeCandidate.putValue(key, value, oldValueBox, hashProvider, defaultValue, newHash(hashProvider, key, hash, depth + 1), depth + 1); return updateWithSubNode(selectedHashFragment, newNode, value.equals(defaultValue)); } else { - // If it does not have value, put it in the empty place + // If it does not have nodeId, put it in the empty place if (value == defaultValue) { - // dont need to add new key-value pair + // dont need to add new key-nodeId pair oldValueBox.setOldValue(defaultValue); return this; } else { @@ -138,8 +138,8 @@ public class MutableNode extends Node { } /** - * Updates an entry in a selected hash-fragment to a non-default value. - * + * Updates an entry in a selected hash-fragment to a non-default nodeId. + * * @param value * @param selectedHashFragment * @return @@ -153,7 +153,7 @@ public class MutableNode extends Node { } /** - * + * * @param selectedHashFragment * @param newNode * @return @@ -400,7 +400,7 @@ public class MutableNode extends Node { V value = (V) this.content[2 * i + 1]; if (value == defaultValue) { - throw new IllegalStateException("Node contains default value!"); + throw new IllegalStateException("Node contains default nodeId!"); } int hashCode = hashProvider.getHash(key, hashDepth(depth)); int shiftDepth = shiftDepth(depth); diff --git a/subprojects/store/src/main/java/tools/refinery/store/map/internal/Node.java b/subprojects/store/src/main/java/tools/refinery/store/map/internal/Node.java index 234a4ff3..2260cd5b 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/map/internal/Node.java +++ b/subprojects/store/src/main/java/tools/refinery/store/map/internal/Node.java @@ -10,7 +10,7 @@ public abstract class Node{ protected static final int NUMBER_OF_FACTORS = Integer.SIZE / BRANCHING_FACTOR_BITS; protected static final int FACTOR_MASK = FACTOR-1; public static final int EFFECTIVE_BITS = BRANCHING_FACTOR_BITS * NUMBER_OF_FACTORS; - + /** * Calculates the index for the continuous hash. * @param depth The depth of the node in the tree. @@ -19,10 +19,10 @@ public abstract class Node{ protected static int hashDepth(int depth) { return depth/NUMBER_OF_FACTORS; } - + /** * Calculates the which segment of a single hash should be used. - * @param depth The depth of the node in the tree. + * @param depth The depth of the node in the tree. * @return The segment of a hash code. */ protected static int shiftDepth(int depth) { @@ -38,7 +38,7 @@ public abstract class Node{ if(shiftDepth<0 || Node.NUMBER_OF_FACTORS variables) { - if (kind.isPositive()) { - super.collectAllVariables(variables); - } - } -} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/BasicCallKind.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/BasicCallKind.java new file mode 100644 index 00000000..cf2ffc07 --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/BasicCallKind.java @@ -0,0 +1,26 @@ +package tools.refinery.store.query.atom; + +public enum BasicCallKind implements CallKind { + POSITIVE(true, false), + NEGATIVE(false, false), + TRANSITIVE(true, true); + + private final boolean positive; + + private final boolean transitive; + + BasicCallKind(boolean positive, boolean transitive) { + this.positive = positive; + this.transitive = transitive; + } + + @Override + public boolean isPositive() { + return positive; + } + + @Override + public boolean isTransitive() { + return transitive; + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/CallAtom.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CallAtom.java new file mode 100644 index 00000000..e52e33ac --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CallAtom.java @@ -0,0 +1,66 @@ +package tools.refinery.store.query.atom; + +import tools.refinery.store.model.RelationLike; +import tools.refinery.store.query.Variable; + +import java.util.List; +import java.util.Objects; +import java.util.Set; + +public final class CallAtom extends AbstractSubstitutionAtom { + private final CallKind kind; + + public CallAtom(CallKind kind, T target, List substitution) { + super(target, substitution); + if (kind.isTransitive() && target.getArity() != 2) { + throw new IllegalArgumentException("Transitive closures can only take binary relations"); + } + this.kind = kind; + } + + public CallAtom(CallKind kind, T target, Variable... substitution) { + this(kind, target, List.of(substitution)); + } + + public CallAtom(boolean positive, T target, List substitution) { + this(CallKind.fromBoolean(positive), target, substitution); + } + + public CallAtom(boolean positive, T target, Variable... substitution) { + this(positive, target, List.of(substitution)); + } + + public CallAtom(T target, List substitution) { + this(true, target, substitution); + } + + public CallAtom(T target, Variable... substitution) { + this(target, List.of(substitution)); + } + + public CallKind getKind() { + return kind; + } + + @Override + public void collectAllVariables(Set variables) { + if (kind.isPositive()) { + super.collectAllVariables(variables); + } + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CallAtom that = (CallAtom) o; + return Objects.equals(kind, that.kind) + && Objects.equals(getTarget(), that.getTarget()) + && Objects.equals(getSubstitution(), that.getSubstitution()); + } + + @Override + public int hashCode() { + return Objects.hash(kind, getTarget(), getSubstitution()); + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/CallKind.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CallKind.java index c7cbc955..86066b8e 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/query/atom/CallKind.java +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CallKind.java @@ -1,24 +1,11 @@ package tools.refinery.store.query.atom; -public enum CallKind { - POSITIVE(true, false), - NEGATIVE(false, false), - TRANSITIVE(true, true); +public sealed interface CallKind permits BasicCallKind, CountCallKind { + boolean isPositive(); - private final boolean positive; + boolean isTransitive(); - private final boolean transitive; - - CallKind(boolean positive, boolean transitive) { - this.positive = positive; - this.transitive = transitive; - } - - public boolean isPositive() { - return positive; - } - - public boolean isTransitive() { - return transitive; + static CallKind fromBoolean(boolean positive) { + return positive ? BasicCallKind.POSITIVE : BasicCallKind.NEGATIVE; } } diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/ComparisonOperator.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/ComparisonOperator.java new file mode 100644 index 00000000..ca113181 --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/ComparisonOperator.java @@ -0,0 +1,22 @@ +package tools.refinery.store.query.atom; + +public enum ComparisonOperator { + EQUALS, + NOT_EQUALS, + LESS, + LESS_EQUALS, + GREATER, + GREATER_EQUALS; + + @Override + public String toString() { + return switch (this) { + case EQUALS -> "=="; + case NOT_EQUALS -> "!="; + case LESS -> "<"; + case LESS_EQUALS -> "<="; + case GREATER -> ">"; + case GREATER_EQUALS -> ">="; + }; + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/ConstantAtom.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/ConstantAtom.java new file mode 100644 index 00000000..13dae7d0 --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/ConstantAtom.java @@ -0,0 +1,12 @@ +package tools.refinery.store.query.atom; + +import tools.refinery.store.query.Variable; + +import java.util.Set; + +public record ConstantAtom(Variable variable, int nodeId) implements DNFAtom { + @Override + public void collectAllVariables(Set variables) { + variables.add(variable); + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/CountCallKind.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CountCallKind.java new file mode 100644 index 00000000..2c85cb4f --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CountCallKind.java @@ -0,0 +1,13 @@ +package tools.refinery.store.query.atom; + +public record CountCallKind(ComparisonOperator operator, int threshold) implements CallKind { + @Override + public boolean isPositive() { + return false; + } + + @Override + public boolean isTransitive() { + return false; + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/CountNotEqualsAtom.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CountNotEqualsAtom.java new file mode 100644 index 00000000..312e5fb8 --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/CountNotEqualsAtom.java @@ -0,0 +1,31 @@ +package tools.refinery.store.query.atom; + +import tools.refinery.store.model.RelationLike; +import tools.refinery.store.query.Variable; + +import java.util.List; +import java.util.Set; + +public record CountNotEqualsAtom(boolean must, int threshold, T mayTarget, T mustTarget, + List substitution) implements DNFAtom { + public CountNotEqualsAtom { + if (substitution.size() != mayTarget.getArity()) { + throw new IllegalArgumentException("%s needs %d arguments, but got %s".formatted(mayTarget.getName(), + mayTarget.getArity(), substitution.size())); + } + if (substitution.size() != mustTarget.getArity()) { + throw new IllegalArgumentException("%s needs %d arguments, but got %s".formatted(mustTarget.getName(), + mustTarget.getArity(), substitution.size())); + } + } + + public CountNotEqualsAtom(boolean must, int threshold, T mayTarget, T mustTarget, Variable... substitution) { + this(must, threshold, mayTarget, mustTarget, List.of(substitution)); + } + + @Override + public void collectAllVariables(Set variables) { + // No variables to collect, because all variables should either appear in other clauses, + // or are quantified by this clause. + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/DNFCallAtom.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/DNFCallAtom.java deleted file mode 100644 index 9d2efa53..00000000 --- a/subprojects/store/src/main/java/tools/refinery/store/query/atom/DNFCallAtom.java +++ /dev/null @@ -1,40 +0,0 @@ -package tools.refinery.store.query.atom; - -import tools.refinery.store.query.DNF; -import tools.refinery.store.query.Variable; - -import java.util.List; -import java.util.Objects; - -public final class DNFCallAtom extends AbstractCallAtom { - public DNFCallAtom(CallKind kind, DNF target, List substitution) { - super(kind, target, substitution); - } - - public DNFCallAtom(CallKind kind, DNF target, Variable... substitution) { - super(kind, target, List.of(substitution)); - } - - public DNFCallAtom(DNF target, List substitution) { - this(CallKind.POSITIVE, target, substitution); - } - - public DNFCallAtom(DNF target, Variable... substitution) { - this(target, List.of(substitution)); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - DNFCallAtom dnfCallAtom = (DNFCallAtom) o; - return Objects.equals(getKind(), dnfCallAtom.getKind()) - && Objects.equals(getTarget(), dnfCallAtom.getTarget()) - && Objects.equals(getSubstitution(), dnfCallAtom.getSubstitution()); - } - - @Override - public int hashCode() { - return Objects.hash(getKind(), getTarget(), getSubstitution()); - } -} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelation.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelation.java new file mode 100644 index 00000000..1e4f8f55 --- /dev/null +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelation.java @@ -0,0 +1,17 @@ +package tools.refinery.store.query.atom; + +import tools.refinery.store.model.RelationLike; +import tools.refinery.store.model.representation.Relation; +import tools.refinery.store.model.representation.TruthValue; + +public record ModalRelation(Modality modality, Relation relation) implements RelationLike { + @Override + public String getName() { + return "%s %s".formatted(modality, relation); + } + + @Override + public int getArity() { + return relation.getArity(); + } +} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelationAtom.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelationAtom.java deleted file mode 100644 index 2480e82e..00000000 --- a/subprojects/store/src/main/java/tools/refinery/store/query/atom/ModalRelationAtom.java +++ /dev/null @@ -1,38 +0,0 @@ -package tools.refinery.store.query.atom; - -import tools.refinery.store.model.representation.Relation; -import tools.refinery.store.model.representation.TruthValue; -import tools.refinery.store.query.Variable; - -import java.util.List; -import java.util.Objects; - -public final class ModalRelationAtom extends AbstractCallAtom> { - private final Modality modality; - - public ModalRelationAtom(CallKind kind, Modality modality, Relation target, - List substitution) { - super(kind, target, substitution); - this.modality = modality; - } - - public ModalRelationAtom(Modality modality, Relation target, List substitution) { - this(CallKind.POSITIVE, modality, target, substitution); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - ModalRelationAtom modalRelationAtom = (ModalRelationAtom) o; - return Objects.equals(getKind(), modalRelationAtom.getKind()) - && modality == modalRelationAtom.modality - && Objects.equals(getTarget(), modalRelationAtom.getTarget()) - && Objects.equals(getSubstitution(), modalRelationAtom.getSubstitution()); - } - - @Override - public int hashCode() { - return Objects.hash(getKind(), modality, getTarget(), getSubstitution()); - } -} diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/Modality.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/Modality.java index 9344a9d3..bc107b76 100644 --- a/subprojects/store/src/main/java/tools/refinery/store/query/atom/Modality.java +++ b/subprojects/store/src/main/java/tools/refinery/store/query/atom/Modality.java @@ -1,7 +1,14 @@ package tools.refinery.store.query.atom; +import java.util.Locale; + public enum Modality { MUST, MAY, CURRENT; + + @Override + public String toString() { + return name().toLowerCase(Locale.ROOT); + } } diff --git a/subprojects/store/src/main/java/tools/refinery/store/query/atom/RelationAtom.java b/subprojects/store/src/main/java/tools/refinery/store/query/atom/RelationAtom.java deleted file mode 100644 index 06098139..00000000 --- a/subprojects/store/src/main/java/tools/refinery/store/query/atom/RelationAtom.java +++ /dev/null @@ -1,32 +0,0 @@ -package tools.refinery.store.query.atom; - -import tools.refinery.store.model.representation.Relation; -import tools.refinery.store.query.Variable; - -import java.util.List; -import java.util.Objects; - -public final class RelationAtom extends AbstractCallAtom> { - public RelationAtom(CallKind kind, Relation target, List substitution) { - super(kind, target, substitution); - } - - public RelationAtom(Relation target, List substitution) { - this(CallKind.POSITIVE, target, substitution); - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - RelationAtom relationAtom = (RelationAtom) o; - return Objects.equals(getKind(), relationAtom.getKind()) - && Objects.equals(getTarget(), relationAtom.getTarget()) - && Objects.equals(getSubstitution(), relationAtom.getSubstitution()); - } - - @Override - public int hashCode() { - return Objects.hash(getKind(), getTarget(), getSubstitution()); - } -} diff --git a/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java b/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java index 991b4f51..a4ba7441 100644 --- a/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java +++ b/subprojects/store/src/test/java/tools/refinery/store/map/tests/utils/MapTestEnvironment.java @@ -56,7 +56,7 @@ public class MapTestEnvironment { } } - + public static void compareTwoMaps(String title, VersionedMapImpl map1, VersionedMapImpl map2) { compareTwoMaps(title, map1, map2, null); @@ -112,7 +112,7 @@ public class MapTestEnvironment { oldOracleValue = oracle.remove(key); } if(oldSutValue == sut.getDefaultValue() && oldOracleValue != null) { - fail("After put, SUT old value was default, but oracle old walue was " + oldOracleValue); + fail("After put, SUT old nodeId was default, but oracle old walue was " + oldOracleValue); } if(oldSutValue != sut.getDefaultValue()) { assertEquals(oldOracleValue, oldSutValue); @@ -127,8 +127,8 @@ public class MapTestEnvironment { fail(title + ": " + e.getMessage()); } - // 1. Checking: if Reference contains pair, then SUT contains - // pair. + // 1. Checking: if Reference contains pair, then SUT contains + // pair. // Tests get functions for (Entry entry : oracle.entrySet()) { V sutValue = sut.get(entry.getKey()); @@ -140,8 +140,8 @@ public class MapTestEnvironment { } } - // 2. Checking: if SUT contains pair, then Reference contains - // pair. + // 2. Checking: if SUT contains pair, then Reference contains + // pair. // Tests iterators int elementsInSutEntrySet = 0; Cursor cursor = sut.getAll(); @@ -160,7 +160,7 @@ public class MapTestEnvironment { } // 3. Checking sizes - // Counting of non-default value pairs. + // Counting of non-default nodeId pairs. int oracleSize = oracle.entrySet().size(); long sutSize = sut.getSize(); if (oracleSize != sutSize || oracleSize != elementsInSutEntrySet) { -- cgit v1.2.3-70-g09d2