All Downloads are FREE. Search and download functionalities are using the official Maven repository.

convex.core.data.BlobMap Maven / Gradle / Ivy

There is a newer version: 0.7.15
Show newest version
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 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 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, ? 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