
convex.core.data.BlobMap Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of convex-core Show documentation
Show all versions of convex-core Show documentation
Convex core libraries and common utilities
package convex.core.data;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Predicate;
import convex.core.exceptions.BadFormatException;
import convex.core.exceptions.InvalidDataException;
import convex.core.lang.RT;
import convex.core.util.Bits;
import convex.core.util.Utils;
/**
* BlobMap node implementation supporting:
*
*
* - An optional prefix string
* - An optional entry with this prefix
* - Up to 16 child entries at the next level of depth
*
* @param Type of Keys
* @param Type of values
*/
public final class BlobMap extends ABlobMap {
@SuppressWarnings({ "unchecked", "rawtypes" })
private static final Ref[] EMPTY_CHILDREN = new Ref[0];
/**
* Empty BlobMap singleton
*/
public static final BlobMap EMPTY = new BlobMap(0, 0, null, EMPTY_CHILDREN,
(short) 0, 0L);
static {
// Set empty Ref flags as internal embedded constant
EMPTY.getRef().setFlags(Ref.INTERNAL_FLAGS);
}
/**
* Child entries, i.e. nodes with keys where this node is a common prefix. Only contains children where mask is set.
* Child entries must have at least one entry.
*/
private final Ref>[] children;
/**
* Entry for this node of the radix tree. Invariant assumption that the prefix
* is correct. May be null if there is no entry at this node.
*/
private final MapEntry entry;
/**
* Mask of child entries, 16 bits for each hex digit that may be present.
*/
private final short mask;
/**
* Depth of radix tree in number of hex digits. Top level is 0.
* Children should have depth = parent depth + parent prefixLength + 1
*/
private final long depth;
/**
* Length of prefix, where the tree branches beyond depth. 0 = no prefix.
*/
private final long prefixLength;
@SuppressWarnings({ "rawtypes", "unchecked" })
protected BlobMap(long depth, long prefixLength, MapEntry entry, Ref[] entries,
short mask, long count) {
super(count);
this.depth = depth;
this.prefixLength = prefixLength;
this.entry = entry;
int cn = Utils.bitCount(mask);
if (cn != entries.length) throw new IllegalArgumentException(
"Illegal mask: " + Utils.toHexString(mask) + " for given number of children: " + cn);
this.children = (Ref[]) entries;
this.mask = mask;
}
public static BlobMap create(MapEntry me) {
ACell k=me.getKey();
if (!(k instanceof ABlob)) return null;
long hexLength = ((ABlob)k).hexLength();
return new BlobMap(0, hexLength, me, EMPTY_CHILDREN, (short) 0, 1L);
}
private static BlobMap createAtDepth(MapEntry me, long depth) {
Blob prefix = me.getKey().toFlatBlob();
long hexLength = prefix.hexLength();
if (depth > hexLength)
throw new IllegalArgumentException("Depth " + depth + " too deep for key with hexLength: " + hexLength);
return new BlobMap(depth, hexLength - depth, me, EMPTY_CHILDREN, (short) 0, 1L);
}
public static BlobMap create(K k, V v) {
MapEntry me = MapEntry.create(k, v);
long hexLength = k.hexLength();
return new BlobMap(0, hexLength, me, EMPTY_CHILDREN, (short) 0, 1L);
}
public static BlobMap of(Object k, Object v) {
return create(RT.cvm(k),RT.cvm(v));
}
@Override
public boolean isCanonical() {
return true;
}
@Override public final boolean isCVMValue() {
// A BlobMap is only a first class CVM value at depth 0.
return (depth==0);
}
@SuppressWarnings("unchecked")
@Override
public BlobMap updateRefs(IRefFunction func) {
MapEntry newEntry = (entry == null) ? null : entry.updateRefs(func);
Ref>[] newChildren = Ref.updateRefs(children, func);
if ((entry == newEntry) && (children == newChildren)) return this;
BlobMap result= new BlobMap(depth, prefixLength, newEntry, (Ref[])newChildren, mask, count);
result.attachEncoding(encoding); // this is an optimisation to avoid re-encoding
return result;
}
@Override
public V get(ABlob key) {
MapEntry me = getEntry(key);
if (me == null) return null;
return me.getValue();
}
@Override
public MapEntry getEntry(ABlob key) {
long kl = key.hexLength();
long pl = depth + prefixLength;
if (kl < pl) return null; // key is too short to start with current prefix
if (kl == pl) {
if ((entry!=null)&&(key.equalsBytes(entry.getKey()))) return entry; // we matched this key exactly!
return null; // entry does not exist
}
int digit = key.getHexDigit(pl);
BlobMap cc = getChild(digit);
if (cc == null) return null;
return cc.getEntry(key);
}
/**
* Gets the child for a specific digit, or null if not found
*
* @param digit
* @return
*/
private BlobMap getChild(int digit) {
int i = Bits.indexForDigit(digit, mask);
if (i < 0) return null;
return (BlobMap) children[i].getValue();
}
@Override
public int getRefCount() {
// note entry might be null
return Utils.refCount(entry) + children.length;
}
@SuppressWarnings("unchecked")
@Override
public Ref getRef(int i) {
if (entry != null) {
int erc = entry.getRefCount();
if (i < erc) return entry.getRef(i);
i -= erc;
}
int cl = children.length;
if (i < cl) return (Ref) children[i];
throw new IndexOutOfBoundsException("No ref for index:" + i);
}
@SuppressWarnings("unchecked")
public BlobMap assoc(ACell key, ACell value) {
if (!(key instanceof ABlob)) return null;
return assocEntry(MapEntry.create((K)key, (V)value));
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public BlobMap dissoc(ABlob k) {
if (count <= 1) {
if (count == 0) return this; // Must already be empty singleton
if (entry.getKey().equalsBytes(k)) {
return (depth==0)?empty():null;
}
return this; // leave existing entry in place
}
long pDepth = depth + prefixLength; // hex depth of this node including prefix
long kl = k.hexLength(); // hex length of key to dissoc
if (kl < pDepth) {
// no match for sure, so no change
return this;
}
if (kl == pDepth) {
// need to check for match with current entry
if (entry == null) return this;
if (!k.equalsBytes(entry.getKey())) return this;
// at this point have matched entry exactly. So need to remove it safely while
// preserving invariants
if (children.length == 1) {
// need to promote child to the current depth
BlobMap c = (BlobMap) children[0].getValue();
return new BlobMap(depth, (c.depth + c.prefixLength) - depth, c.entry, c.children, c.mask,
count - 1);
} else {
// Clearing current entry, keeping existing children (must be 2+)
return new BlobMap(depth, prefixLength, null, children, mask, count - 1);
}
}
// dissoc beyond current prefix length, so need to check children
int digit = k.getHexDigit(pDepth);
int childIndex = Bits.indexForDigit(digit, mask);
if (childIndex < 0) return this; // key miss
// we know we need to replace a child
BlobMap oldChild = (BlobMap) children[childIndex].getValue();
BlobMap newChild = oldChild.dissoc(k);
BlobMap r=this.withChild(digit, oldChild, newChild);
// check if whole blobmap was emptied
if ((r==null)&&(depth==0)) r= empty();
return r;
}
/**
* Prefix blob, must contain hex digits in the range [depth,depth+prefixLength).
*
* May contain more hex digits in memory, this is irrelevant from the
* perspective of serialisation.
*
* Typically we populate with the key of the first entry added to avoid
* unnecessary blob instances being created.
*/
private ABlob getPrefix() {
if (entry!=null) return entry.getKey();
int n=children.length;
if (n==0) return Blob.EMPTY;
return children[0].getValue().getPrefix();
}
@Override
protected void accumulateEntrySet(HashSet> h) {
for (int i = 0; i < children.length; i++) {
children[i].getValue().accumulateEntrySet(h);
}
if (entry != null) h.add(entry);
}
@Override
protected void accumulateKeySet(HashSet h) {
for (int i = 0; i < children.length; i++) {
children[i].getValue().accumulateKeySet(h);
}
if (entry != null) h.add(entry.getKey());
}
@Override
protected void accumulateValues(ArrayList al) {
// add this entry first, since we want lexicographic order
if (entry != null) al.add(entry.getValue());
for (int i = 0; i < children.length; i++) {
children[i].getValue().accumulateValues(al);
}
}
@Override
public void forEach(BiConsumer super K, ? super V> action) {
if (entry != null) action.accept(entry.getKey(), entry.getValue());
for (int i = 0; i < children.length; i++) {
children[i].getValue().forEach(action);
}
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Override
public BlobMap assocEntry(MapEntry e) {
if (count == 0L) return create(e);
if (count == 1L) {
assert (mask == (short) 0); // should be no children
if (entry.keyEquals(e)) {
if (entry == e) return this;
// recreate, preserving current depth
return createAtDepth(e, depth);
}
}
ACell maybeValidKey=e.getKey();
if (!(maybeValidKey instanceof ABlob)) return null; // invalid key type!
ABlob k = (ABlob)maybeValidKey;
long pDepth = this.prefixDepth(); // hex depth of this node including prefix
long newKeyLength = k.hexLength(); // hex length of new key
long mkl; // matched key length
ABlob prefix=getPrefix();
if (newKeyLength >= pDepth) {
// constrain relevant key length by match with current prefix
mkl = depth + k.hexMatchLength(prefix, depth, prefixLength);
} else {
mkl = depth + k.hexMatchLength(prefix, depth, newKeyLength - depth);
}
if (mkl < pDepth) {
// we collide at a point shorter than the current prefix length
if (mkl == newKeyLength) {
// new key is subset of the current prefix, so split prefix at key position mkl
// doesn't need to adjust child depths, since they are splitting at the same
// point
long newDepth=mkl+1; // depth for new child
BlobMap split = new BlobMap(newDepth, pDepth - newDepth, entry, (Ref[]) children, mask,
count);
int splitDigit = prefix.getHexDigit(mkl);
short splitMask = (short) (1 << splitDigit);
BlobMap result = new BlobMap(depth, mkl - depth, e, new Ref[] { split.getRef() },
splitMask, count + 1);
return result;
} else {
// we need to fork the current prefix in two at position mkl
long newDepth=mkl+1; // depth for new children
BlobMap branch1 = new BlobMap(newDepth, pDepth - newDepth, entry, (Ref[]) children, mask,
count);
BlobMap branch2 = new BlobMap(newDepth, newKeyLength - newDepth, e, (Ref[]) EMPTY_CHILDREN,
(short) 0, 1L);
int d1 = prefix.getHexDigit(mkl);
int d2 = k.getHexDigit(mkl);
if (d1 > d2) {
// swap to get in right order
BlobMap temp = branch1;
branch1 = branch2;
branch2 = temp;
}
Ref[] newChildren = new Ref[] { branch1.getRef(), branch2.getRef() };
short newMask = (short) ((1 << d1) | (1 << d2));
BlobMap fork = new BlobMap(depth, mkl - depth, null, newChildren, newMask, count + 1L);
return fork;
}
}
assert (newKeyLength >= pDepth);
if (newKeyLength == pDepth) {
// we must have matched the current entry exactly
if (entry == null) {
// just add entry at this position
return new BlobMap(depth, prefixLength, e, (Ref[]) children, mask, count + 1);
}
if (entry == e) return this;
// swap entry, no need to change count
return new BlobMap(depth, prefixLength, e, (Ref[]) children, mask, count);
}
// at this point we have matched full prefix, but new key length is longer.
// so we need to update (or add) exactly one child
int childDigit = k.getHexDigit(pDepth);
BlobMap oldChild = getChild(childDigit);
BlobMap newChild;
if (oldChild == null) {
newChild = createAtDepth(e, pDepth+1); // Myst be at least 1 beyond current prefix
} else {
newChild = oldChild.assocEntry(e);
}
return withChild(childDigit, oldChild, newChild); // can't be null since associng
}
/**
* Updates this BlobMap with a new child.
*
* Either oldChild or newChild may be null. Empty maps are treated as null.
*
* @param childDigit Digit for new child
* @param newChild
* @return BlobMap with child removed, or null if BlobMap was deleted entirely
*/
@SuppressWarnings({ "rawtypes", "unchecked"})
private BlobMap withChild(int childDigit, BlobMap oldChild, BlobMap newChild) {
// consider empty children as null
//if (oldChild == EMPTY) oldChild = null;
//if (newChild == EMPTY) newChild = null;
if (oldChild == newChild) return this;
int n = children.length;
// we need a new child array
Ref[] newChildren = children;
if (oldChild == null) {
// definitely need a new entry
newChildren = new Ref[n + 1];
int newPos = Bits.positionForDigit(childDigit, mask);
short newMask = (short) (mask | (1 << childDigit));
System.arraycopy(children, 0, newChildren, 0, newPos); // earlier entries
newChildren[newPos] = newChild.getRef();
System.arraycopy(children, newPos, newChildren, newPos + 1, n - newPos); // later entries
return new BlobMap(depth, prefixLength, entry, newChildren, newMask,
count + newChild.count());
} else {
// dealing with an existing child
if (newChild == null) {
// need to delete an existing child
int delPos = Bits.positionForDigit(childDigit, mask);
// handle special case where we need to promote the remaining child
if (entry == null) {
if (n == 2) {
BlobMap rm = (BlobMap) children[1 - delPos].getValue();
long newPLength = prefixLength + rm.prefixLength+1;
return new BlobMap(depth, newPLength, rm.entry, (Ref[]) rm.children, rm.mask,
rm.count());
} else if (n == 1) {
// deleting entire BlobMap!
return null;
}
}
if (n==0) {
System.out.print("BlobMap Bad!");
}
newChildren = new Ref[n - 1];
short newMask = (short) (mask & ~(1 << childDigit));
System.arraycopy(children, 0, newChildren, 0, delPos); // earlier entries
System.arraycopy(children, delPos + 1, newChildren, delPos, n - delPos - 1); // later entries
return new BlobMap(depth, prefixLength, entry, newChildren, newMask,
count - oldChild.count());
} else {
// need to replace a child
int childPos = Bits.positionForDigit(childDigit, mask);
newChildren = children.clone();
newChildren[childPos] = newChild.getRef();
long newCount = count + newChild.count() - oldChild.count();
return new BlobMap(depth, prefixLength, entry, newChildren, mask, newCount);
}
}
}
@Override
public R reduceValues(BiFunction super R, ? super V, ? extends R> func, R initial) {
if (entry != null) initial = func.apply(initial, entry.getValue());
int n = children.length;
for (int i = 0; i < n; i++) {
initial = children[i].getValue().reduceValues(func, initial);
}
return initial;
}
@Override
public R reduceEntries(BiFunction super R, MapEntry, ? extends R> func, R initial) {
if (entry != null) initial = func.apply(initial, entry);
int n = children.length;
for (int i = 0; i < n; i++) {
initial = children[i].getValue().reduceEntries(func, initial);
}
return initial;
}
@Override
public BlobMap filterValues(Predicate pred) {
BlobMap r=this;
for (int i=0; i<16; i++) {
if (r==null) break; // might be null from dissoc
BlobMap oldChild=r.getChild(i);
if (oldChild==null) continue;
BlobMap newChild=oldChild.filterValues(pred);
r=r.withChild(i, oldChild, newChild);
}
// check entry at this level. A child might have moved here during the above loop!
if (r!=null) {
if ((r.entry!=null)&&!pred.test(r.entry.getValue())) r=r.dissoc(r.entry.getKey());
}
// check if whole blobmap was emptied
if (r==null) {
// everything deleted, but need
if (depth==0) r=empty();
}
return r;
}
@Override
public int encode(byte[] bs, int pos) {
bs[pos++]=Tag.BLOBMAP;
return encodeRaw(bs,pos);
}
@Override
public int encodeRaw(byte[] bs, int pos) {
pos = Format.writeVLCLong(bs,pos, count);
if (count == 0) return pos; // nothing more to know... this must be the empty singleton
pos = Format.writeVLCLong(bs,pos, depth);
pos = Format.writeVLCLong(bs,pos, prefixLength);
pos = MapEntry.encodeCompressed(entry,bs,pos); // entry may be null
if (count == 1) return pos; // must be a single entry
// finally write children
pos = Utils.writeShort(bs,pos,mask);
int n = children.length;
for (int i = 0; i < n; i++) {
pos = encodeChild(bs,pos,i);
}
return pos;
}
private int encodeChild(byte[] bs, int pos, int i) {
Ref> cref = children[i];
return cref.encode(bs, pos);
// TODO: maybe compress single entries?
// ABlobMap c=cref.getValue();
// if (c.count==1) {
// MapEntry me=c.entryAt(0);
// pos = me.getRef().encode(bs, pos);
// } else {
// pos = cref.encode(bs,pos);
// }
// return pos;
}
@Override
public int estimatedEncodingSize() {
return 100 + (children.length*2+1) * Format.MAX_EMBEDDED_LENGTH;
}
@SuppressWarnings({ "unchecked", "rawtypes" })
public static BlobMap read(Blob b, int pos) throws BadFormatException {
long count = Format.readVLCLong(b,pos+1);
if (count < 0) throw new BadFormatException("Negative count!");
if (count == 0) return (BlobMap) EMPTY;
int epos=pos+1+Format.getVLCLength(count);
long depth = Format.readVLCLong(b,epos);
if (depth < 0) throw new BadFormatException("Negative depth!");
epos+=Format.getVLCLength(depth);
long prefixLength = Format.readVLCLong(b,epos);
if (prefixLength < 0) throw new BadFormatException("Negative prefix length!");
epos+=Format.getVLCLength(prefixLength);
byte etype=b.byteAt(epos++);
MapEntry me;
if (etype==Tag.NULL) {
me=null;
} else if (etype==Tag.VECTOR){
Ref kr=Format.readRef(b,epos);
epos+=kr.getEncodingLength();
Ref vr=Format.readRef(b,epos);
epos+=vr.getEncodingLength();
me=MapEntry.createRef(kr, vr);
} else {
throw new BadFormatException("Invalid MapEntry tag in BlobMap: "+etype);
}
BlobMap result;
if (count == 1) {
// single entry map
result = new BlobMap(depth, prefixLength, me, EMPTY_CHILDREN, (short) 0, 1L);
result.attachEncoding(b.slice(pos, epos));
} else {
// Need to include children
short mask = b.shortAt(epos);
epos+=2;
int n = Utils.bitCount(mask);
Ref[] children = new Ref[n];
for (int i = 0; i < n; i++) {
Ref cr=Format.readRef(b,epos);
epos+=cr.getEncodingLength();
children[i] =cr;
}
result= new BlobMap(depth, prefixLength, me, children, mask, count);
}
result.attachEncoding(b.slice(pos, epos));
return result;
}
@Override
protected MapEntry getEntryByHash(Hash hash) {
throw new UnsupportedOperationException();
}
@SuppressWarnings("unchecked")
@Override
public void validate() throws InvalidDataException {
super.validate();
long ecount = (entry == null) ? 0 : 1;
int n = children.length;
long pDepth = prefixDepth();
for (int i = 0; i < n; i++) {
ACell o = children[i].getValue();
if (!(o instanceof BlobMap))
throw new InvalidDataException("Illegal BlobMap child type: " + Utils.getClass(o), this);
BlobMap c = (BlobMap) o;
long ccount=c.count();
if (ccount==0) {
throw new InvalidDataException("Child "+i+" should not be empty! At depth "+depth,this);
}
if (c.depth != (pDepth+1)) {
throw new InvalidDataException("Child must have depth: " + (pDepth+1) + " but was: " + c.depth,
this);
}
if (c.prefixDepth() <= prefixDepth()) {
throw new InvalidDataException("Child must have greater total prefix depth than " + prefixDepth()
+ " but was: " + c.prefixDepth(), this);
}
c.validate();
ecount += ccount;
}
if (count != ecount) throw new InvalidDataException("Bad entry count: " + ecount + " expected: " + count, this);
}
/**
* Gets the total depth of this node including prefix
*
* @return
*/
private long prefixDepth() {
return depth + prefixLength;
}
@Override
public void validateCell() throws InvalidDataException {
if (prefixLength < 0) throw new InvalidDataException("Negative prefix length!" + prefixLength, this);
if (count == 0) {
if (this != EMPTY) throw new InvalidDataException("Non-singleton empty BlobMap", this);
return;
} else if (count == 1) {
if (entry == null) throw new InvalidDataException("Single entry BlobMap with null entry?", this);
if (mask != 0) throw new InvalidDataException("Single entry BlobMap with child mask?", this);
return;
}
// at least count 2 from this point
int cn = Utils.bitCount(mask);
if (cn != children.length) throw new InvalidDataException(
"Illegal mask: " + Utils.toHexString(mask) + " for given number of children: " + children.length, this);
if (entry != null) {
entry.validateCell();
if (cn == 0)
throw new InvalidDataException("BlobMap with entry and count=" + count + " must have children", this);
} else {
if (cn <= 1) throw new InvalidDataException(
"BlobMap with no entry and count=" + count + " must have two or more children", this);
}
}
@SuppressWarnings("unchecked")
@Override
public BlobMap empty() {
return (BlobMap) EMPTY;
}
@Override
public MapEntry entryAt(long ix) {
if (entry != null) {
if (ix == 0L) return entry;
ix -= 1;
}
int n = children.length;
for (int i = 0; i < n; i++) {
BlobMap c = children[i].getValue();
long cc = c.count();
if (ix < cc) return c.entryAt(ix);
ix -= cc;
}
throw new IndexOutOfBoundsException(ix);
}
/**
* Slices this BlobMap, starting at the specified position
*
* Removes n leading entries from this BlobMap, in key order.
*
* @param start Start position of entries to keep
* @return Updated BlobMap with leading entries removed, or null if invalid slice
*/
@Override
public BlobMap slice(long start) {
return slice(start,count);
}
/**
* Returns a slice of this BlobMap
*
* @param start Start position of slice (inclusive)
* @param end End position of slice (exclusive)
* @return Slice of BlobMap, or null if invalid slice
*/
@Override
public BlobMap slice(long start, long end) {
if ((start<0)||(end>count)) return null;
if (end bm = this;
for (long i=count-1; i>=end; i--) {
MapEntry me = bm.entryAt(i);
bm = bm.dissoc(me.getKey());
}
for (long i = 0; i < start; i++) {
MapEntry me = bm.entryAt(0);
bm = bm.dissoc(me.getKey());
}
return bm;
}
@SuppressWarnings("unchecked")
@Override
public boolean equals(ACell a) {
if (this == a) return true; // important optimisation for e.g. hashmap equality
if (!(a instanceof BlobMap)) return false;
// Must be a BlobMap
return equals((BlobMap)a);
}
/**
* Checks this BlobMap for equality with another BlobMap
*
* @param a BlobMap to compare with
* @return true if maps are equal, false otherwise.
*/
public boolean equals(BlobMap a) {
if (a==null) return false;
long n=this.count();
if (n != a.count()) return false;
if (this.mask!=a.mask) return false;
if (!Utils.equals(this.entry, a.entry)) return false;
return getHash().equals(a.getHash());
}
@Override
public byte getTag() {
return Tag.BLOBMAP;
}
@Override
public ACell toCanonical() {
return this;
}
@Override
public boolean containsValue(ACell value) {
if ((entry!=null)&&Utils.equals(value, entry.getValue())) return true;
for (Ref> cr : children) {
if (cr.getValue().containsValue(value)) return true;
}
return false;
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy