package runtime // This is a hashmap implementation for the map[T]T type. // It is very roughly based on the implementation of the Go hashmap: // // https://golang.org/src/runtime/map.go import ( "internal/reflectlite" "tinygo" "unsafe" ) // The underlying hashmap structure for Go. type hashmap struct { buckets unsafe.Pointer // pointer to array of buckets seed uintptr count uintptr keySize uintptr valueSize uintptr keySlotSize uintptr // == keySize, or sizeof(ptr) if indirect valueSlotSize uintptr // == valueSize, or sizeof(ptr) if indirect bucketBits uint8 flags uint8 keyEqual func(x, y unsafe.Pointer, n uintptr) bool keyHash func(key unsafe.Pointer, size, seed uintptr) uint32 } const ( hashmapMaxKeySize = 128 hashmapMaxValueSize = 128 hashmapFlagIndirectKey = 1 << 0 hashmapFlagIndirectValue = 1 << 1 ) // A hashmap bucket. A bucket is a container of 8 key/value pairs: first the // following two entries, then the 8 keys, then the 8 values. This somewhat odd // ordering is to make sure the keys and values are well aligned when one of // them is smaller than the system word size. type hashmapBucket struct { tophash [8]uint8 next *hashmapBucket // next bucket (if there are more than 8 in a chain) // Followed by the actual keys, and then the actual values. These are // allocated but as they're of variable size they can't be shown here. } // hashmapBucketHeaderSize is the offset in bytes from the start of a bucket to // the first key, aligned to 8 bytes. This ensures that keys requiring 8-byte // alignment (float64, complex128, uint64 on strict-alignment architectures // like MIPS) are properly aligned in the bucket. const hashmapBucketHeaderSize = (unsafe.Sizeof(hashmapBucket{}) + 7) &^ 7 // hashmapKeySlotSize returns the size of a key slot in the bucket. For indirect // keys, this is the pointer size; otherwise the actual key size. // //go:inline func hashmapKeySlotSize(m *hashmap) uintptr { return m.keySlotSize } // hashmapValueSlotSize returns the size of a value slot in the bucket. // //go:inline func hashmapValueSlotSize(m *hashmap) uintptr { return m.valueSlotSize } // hashmapSlotKeyData returns a pointer to the actual key data for a given slot. // For indirect keys, the slot contains a pointer that must be dereferenced. // //go:inline func hashmapSlotKeyData(m *hashmap, slotKey unsafe.Pointer) unsafe.Pointer { if m.flags&hashmapFlagIndirectKey != 0 { return *(*unsafe.Pointer)(slotKey) } return slotKey } // hashmapSlotValueData returns a pointer to the actual value data for a given slot. // //go:inline func hashmapSlotValueData(m *hashmap, slotValue unsafe.Pointer) unsafe.Pointer { if m.flags&hashmapFlagIndirectValue != 0 { return *(*unsafe.Pointer)(slotValue) } return slotValue } type hashmapIterator struct { buckets unsafe.Pointer // pointer to array of hashapBuckets numBuckets uintptr // length of buckets array bucketNumber uintptr // current index into buckets array startBucket uintptr // starting location for iterator bucket *hashmapBucket // current bucket in chain bucketIndex uint8 // current index into bucket startIndex uint8 // starting bucket index for iterator wrapped bool // true if the iterator has wrapped } func hashmapNewIterator() unsafe.Pointer { return unsafe.Pointer(new(hashmapIterator)) } // Get the topmost 8 bits of the hash, without using a special value (like 0). func hashmapTopHash(hash uint32) uint8 { tophash := uint8(hash >> 24) if tophash < 1 { // 0 means empty slot, so make it bigger. tophash += 1 } return tophash } // Create a new hashmap with the given keySize and valueSize. func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) *hashmap { bucketBits := uint8(0) for hashmapHasSpaceToGrow(bucketBits) && hashmapOverLoadFactor(sizeHint, bucketBits) { bucketBits++ } var flags uint8 keySlotSize := keySize if keySize > hashmapMaxKeySize { flags |= hashmapFlagIndirectKey keySlotSize = unsafe.Sizeof(unsafe.Pointer(nil)) } valueSlotSize := valueSize if valueSize > hashmapMaxValueSize { flags |= hashmapFlagIndirectValue valueSlotSize = unsafe.Sizeof(unsafe.Pointer(nil)) } bucketBufSize := hashmapBucketHeaderSize + keySlotSize*8 + valueSlotSize*8 buckets := alloc(bucketBufSize*(1< If the map or slice is nil, clear is a no-op. return } m.count = 0 numBuckets := uintptr(1) << m.bucketBits bucketSize := hashmapBucketSize(m) for i := uintptr(0); i < numBuckets; i++ { bucket := hashmapBucketAddr(m, m.buckets, i) for bucket != nil { // Clear the tophash, to mark these keys/values as removed. bucket.tophash = [8]uint8{} // Clear the keys and values in the bucket so that the GC won't pin // these allocations. memzero(unsafe.Add(unsafe.Pointer(bucket), unsafe.Sizeof(hashmapBucket{})), bucketSize-unsafe.Sizeof(hashmapBucket{})) // Move on to the next bucket in the chain. bucket = bucket.next } } } func hashmapKeyEqualAlg(alg tinygo.HashmapAlgorithm) func(x, y unsafe.Pointer, n uintptr) bool { switch alg { case tinygo.HashmapAlgorithmBinary: return memequal case tinygo.HashmapAlgorithmString: return hashmapStringEqual default: // compiler bug :( return nil } } func hashmapKeyHashAlg(alg tinygo.HashmapAlgorithm) func(key unsafe.Pointer, n, seed uintptr) uint32 { switch alg { case tinygo.HashmapAlgorithmBinary: return hash32 case tinygo.HashmapAlgorithmString: return hashmapStringPtrHash default: // compiler bug :( return nil } } func hashmapHasSpaceToGrow(bucketBits uint8) bool { // Over this limit, we're likely to overflow uintptrs during calculations // or numbers of hash elements. Don't allow any more growth. // With 29 bits, this is 2^32 elements anyway. return bucketBits <= uint8((unsafe.Sizeof(uintptr(0))*8)-3) } func hashmapOverLoadFactor(n uintptr, bucketBits uint8) bool { // "maximum" number of elements is 0.75 * buckets * elements per bucket // to avoid overflow, this is calculated as // max = 3 * (1/4 * buckets * elements per bucket) // = 3 * (buckets * (elements per bucket)/4) // = 3 * (buckets * (8/4) // = 3 * (buckets * 2) // = 6 * buckets max := (uintptr(6) << bucketBits) return n > max } // Return the number of entries in this hashmap, called from the len builtin. // A nil hashmap is defined as having length 0. // //go:inline func hashmapLen(m *hashmap) int { if m == nil { return 0 } return int(m.count) } //go:inline func hashmapBucketSize(m *hashmap) uintptr { return hashmapBucketHeaderSize + hashmapKeySlotSize(m)*8 + hashmapValueSlotSize(m)*8 } //go:inline func hashmapBucketAddr(m *hashmap, buckets unsafe.Pointer, n uintptr) *hashmapBucket { bucketSize := hashmapBucketSize(m) bucket := (*hashmapBucket)(unsafe.Add(buckets, bucketSize*n)) return bucket } //go:inline func hashmapBucketAddrForHash(m *hashmap, hash uint32) *hashmapBucket { numBuckets := uintptr(1) << m.bucketBits bucketNumber := (uintptr(hash) & (numBuckets - 1)) return hashmapBucketAddr(m, m.buckets, bucketNumber) } //go:inline func hashmapSlotKey(m *hashmap, bucket *hashmapBucket, slot uint8) unsafe.Pointer { slotKeyOffset := hashmapBucketHeaderSize + hashmapKeySlotSize(m)*uintptr(slot) return unsafe.Add(unsafe.Pointer(bucket), slotKeyOffset) } //go:inline func hashmapSlotValue(m *hashmap, bucket *hashmapBucket, slot uint8) unsafe.Pointer { slotValueOffset := hashmapBucketHeaderSize + hashmapKeySlotSize(m)*8 + hashmapValueSlotSize(m)*uintptr(slot) return unsafe.Add(unsafe.Pointer(bucket), slotValueOffset) } // Set a specified key to a given value. Grow the map if necessary. // //go:nobounds func hashmapSet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint32) { if hashmapHasSpaceToGrow(m.bucketBits) && hashmapOverLoadFactor(m.count, m.bucketBits) { hashmapGrow(m) // seed changed when we grew; rehash key with new seed hash = m.keyHash(key, m.keySize, m.seed) } tophash := hashmapTopHash(hash) bucket := hashmapBucketAddrForHash(m, hash) var lastBucket *hashmapBucket // See whether the key already exists somewhere. var emptySlotKey unsafe.Pointer var emptySlotValue unsafe.Pointer var emptySlotTophash *byte for bucket != nil { for i := uint8(0); i < 8; i++ { slotKey := hashmapSlotKey(m, bucket, i) slotValue := hashmapSlotValue(m, bucket, i) if bucket.tophash[i] == 0 && emptySlotKey == nil { // Found an empty slot, store it for if we couldn't find an // existing slot. emptySlotKey = slotKey emptySlotValue = slotValue emptySlotTophash = &bucket.tophash[i] } if bucket.tophash[i] == tophash { // Could be an existing key that's the same. if m.keyEqual(key, hashmapSlotKeyData(m, slotKey), m.keySize) { // found same key, replace the value hashmapStoreValue(m, slotValue, value) return } } } lastBucket = bucket bucket = bucket.next } if emptySlotKey == nil { // Add a new bucket to the bucket chain. // TODO: rebalance if necessary to avoid O(n) insert and lookup time. lastBucket.next = (*hashmapBucket)(hashmapInsertIntoNewBucket(m, key, value, tophash)) return } m.count++ hashmapStoreKey(m, emptySlotKey, key) hashmapStoreValue(m, emptySlotValue, value) *emptySlotTophash = tophash } // hashmapStoreKey stores a key into a bucket slot, allocating backing storage // if the key is indirect (first insert) or copying into it (shouldn't happen // for keys, but handles it correctly). // //go:inline func hashmapStoreKey(m *hashmap, slotKey, key unsafe.Pointer) { if m.flags&hashmapFlagIndirectKey != 0 { p := alloc(m.keySize, nil) memcpy(p, key, m.keySize) *(*unsafe.Pointer)(slotKey) = p } else { memcpy(slotKey, key, m.keySize) } } // hashmapStoreValue stores a value into a bucket slot. For indirect values, // it allocates backing storage on first insert or copies into the existing // backing on overwrite. // //go:inline func hashmapStoreValue(m *hashmap, slotValue, value unsafe.Pointer) { if m.flags&hashmapFlagIndirectValue != 0 { p := *(*unsafe.Pointer)(slotValue) if p == nil { // First insert: allocate backing storage. p = alloc(m.valueSize, nil) *(*unsafe.Pointer)(slotValue) = p } memcpy(p, value, m.valueSize) } else { memcpy(slotValue, value, m.valueSize) } } // hashmapInsertIntoNewBucket creates a new bucket, inserts the given key and // value into the bucket, and returns a pointer to this bucket. func hashmapInsertIntoNewBucket(m *hashmap, key, value unsafe.Pointer, tophash uint8) *hashmapBucket { bucketBufSize := hashmapBucketSize(m) bucketBuf := alloc(bucketBufSize, nil) bucket := (*hashmapBucket)(bucketBuf) // Insert into the first slot, which is empty as it has just been allocated. slotKey := hashmapSlotKey(m, bucket, 0) slotValue := hashmapSlotValue(m, bucket, 0) m.count++ hashmapStoreKey(m, slotKey, key) hashmapStoreValue(m, slotValue, value) bucket.tophash[0] = tophash return bucket } func hashmapGrow(m *hashmap) { // allocate our new buckets twice as big n := hashmapCopy(m, m.bucketBits+1) *m = n } //go:linkname hashmapClone maps.clone func hashmapClone(intf _interface) _interface { typ, val := decomposeInterface(intf) m := (*hashmap)(val) n := hashmapCopy(m, m.bucketBits) return composeInterface(typ, unsafe.Pointer(&n)) } func hashmapCopy(m *hashmap, sizeBits uint8) hashmap { // clone map as empty n := *m n.count = 0 n.seed = uintptr(fastrand()) n.bucketBits = sizeBits numBuckets := uintptr(1) << n.bucketBits bucketBufSize := hashmapBucketSize(m) n.buckets = alloc(bucketBufSize*numBuckets, nil) // use a hashmap iterator to go through the old map var it hashmapIterator var key = alloc(m.keySize, nil) var value = alloc(m.valueSize, nil) for hashmapNext(m, &it, key, value) { h := n.keyHash(key, uintptr(n.keySize), n.seed) hashmapSet(&n, key, value, h) } return n } // Get the value of a specified key, or zero the value if not found. // //go:nobounds func hashmapGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr, hash uint32) bool { if m == nil { // Getting a value out of a nil map is valid. From the spec: // > if the map is nil or does not contain such an entry, a[x] is the // > zero value for the element type of M memzero(value, uintptr(valueSize)) return false } tophash := hashmapTopHash(hash) bucket := hashmapBucketAddrForHash(m, hash) // Try to find the key. for bucket != nil { for i := uint8(0); i < 8; i++ { slotKey := hashmapSlotKey(m, bucket, i) if bucket.tophash[i] == tophash { // This could be the key we're looking for. if m.keyEqual(key, hashmapSlotKeyData(m, slotKey), m.keySize) { // Found the key, copy it. slotValue := hashmapSlotValue(m, bucket, i) memcpy(value, hashmapSlotValueData(m, slotValue), m.valueSize) return true } } } bucket = bucket.next } // Did not find the key. memzero(value, m.valueSize) return false } // Delete a given key from the map. No-op when the key does not exist in the // map. // //go:nobounds func hashmapDelete(m *hashmap, key unsafe.Pointer, hash uint32) { if m == nil { // The delete builtin is defined even when the map is nil. From the spec: // > If the map m is nil or the element m[k] does not exist, delete is a // > no-op. return } tophash := hashmapTopHash(hash) bucket := hashmapBucketAddrForHash(m, hash) // Try to find the key. for bucket != nil { for i := uint8(0); i < 8; i++ { slotKey := hashmapSlotKey(m, bucket, i) if bucket.tophash[i] == tophash { // This could be the key we're looking for. if m.keyEqual(key, hashmapSlotKeyData(m, slotKey), m.keySize) { // Found the key, delete it. bucket.tophash[i] = 0 // Zero out the slot so the GC won't pin the allocations. keySlotSize := hashmapKeySlotSize(m) memzero(slotKey, keySlotSize) slotValue := hashmapSlotValue(m, bucket, i) valueSlotSize := hashmapValueSlotSize(m) memzero(slotValue, valueSlotSize) m.count-- return } } } bucket = bucket.next } } // Iterate over a hashmap. // //go:nobounds func hashmapNext(m *hashmap, it *hashmapIterator, key, value unsafe.Pointer) bool { if m == nil { // From the spec: If the map is nil, the number of iterations is 0. return false } if it.buckets == nil { // initialize iterator it.buckets = m.buckets it.numBuckets = uintptr(1) << m.bucketBits it.startBucket = uintptr(fastrand()) & (it.numBuckets - 1) it.startIndex = uint8(fastrand() & 7) it.bucketNumber = it.startBucket it.bucket = hashmapBucketAddr(m, it.buckets, it.bucketNumber) it.bucketIndex = it.startIndex } for { // If we've wrapped and we're back at our starting location, terminate the iteration. if it.wrapped && it.bucketNumber == it.startBucket && it.bucketIndex == it.startIndex { return false } if it.bucketIndex >= 8 { // end of bucket, move to the next in the chain it.bucketIndex = 0 it.bucket = it.bucket.next } if it.bucket == nil { it.bucketNumber++ // next bucket if it.bucketNumber >= it.numBuckets { // went through all buckets -- wrap around it.bucketNumber = 0 it.wrapped = true } it.bucket = hashmapBucketAddr(m, it.buckets, it.bucketNumber) continue } if it.bucket.tophash[it.bucketIndex] == 0 { // slot is empty - move on it.bucketIndex++ continue } // Found a key. slotKey := hashmapSlotKey(m, it.bucket, it.bucketIndex) memcpy(key, hashmapSlotKeyData(m, slotKey), m.keySize) if it.buckets == m.buckets { // Our view of the buckets is the same as the parent map. // Just copy the value we have slotValue := hashmapSlotValue(m, it.bucket, it.bucketIndex) memcpy(value, hashmapSlotValueData(m, slotValue), m.valueSize) it.bucketIndex++ } else { it.bucketIndex++ // Our view of the buckets doesn't match the parent map. // Look up the key in the new buckets and return that value if it exists hash := m.keyHash(key, m.keySize, m.seed) ok := hashmapGet(m, key, value, m.valueSize, hash) if !ok { // doesn't exist in parent map; try next key continue } // All good. } return true } } // Hashmap with plain binary data keys (not containing strings etc.). func hashmapBinarySet(m *hashmap, key, value unsafe.Pointer) { if m == nil { nilMapPanic() } hash := hash32(key, m.keySize, m.seed) hashmapSet(m, key, value, hash) } func hashmapBinaryGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr) bool { if m == nil { memzero(value, uintptr(valueSize)) return false } hash := hash32(key, m.keySize, m.seed) return hashmapGet(m, key, value, valueSize, hash) } func hashmapBinaryDelete(m *hashmap, key unsafe.Pointer) { if m == nil { return } hash := hash32(key, m.keySize, m.seed) hashmapDelete(m, key, hash) } // Hashmap with compiler-generated key hash/equal functions. // Unlike the binary path (which uses hash32/memequal), these use the // type-specific keyHash and keyEqual function pointers stored in the hashmap // struct. This is used for composite key types (e.g. structs containing // strings) where the compiler generates specialized hash/equal functions. func hashmapGenericSet(m *hashmap, key, value unsafe.Pointer) { if m == nil { nilMapPanic() } hash := m.keyHash(key, m.keySize, m.seed) hashmapSet(m, key, value, hash) } func hashmapGenericGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr) bool { if m == nil { memzero(value, uintptr(valueSize)) return false } hash := m.keyHash(key, m.keySize, m.seed) return hashmapGet(m, key, value, valueSize, hash) } func hashmapGenericDelete(m *hashmap, key unsafe.Pointer) { if m == nil { return } hash := m.keyHash(key, m.keySize, m.seed) hashmapDelete(m, key, hash) } // hashmapMakeGeneric creates a new hashmap with compiler-provided hash and // equal functions. This avoids the interface/reflection path for composite // key types like structs containing strings. func hashmapMakeGeneric(keySize, valueSize uintptr, sizeHint uintptr, keyHash func(key unsafe.Pointer, size, seed uintptr) uint32, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) *hashmap { bucketBits := uint8(0) for hashmapHasSpaceToGrow(bucketBits) && hashmapOverLoadFactor(sizeHint, bucketBits) { bucketBits++ } var flags uint8 keySlotSize := keySize if keySize > hashmapMaxKeySize { flags |= hashmapFlagIndirectKey keySlotSize = unsafe.Sizeof(unsafe.Pointer(nil)) } valueSlotSize := valueSize if valueSize > hashmapMaxValueSize { flags |= hashmapFlagIndirectValue valueSlotSize = unsafe.Sizeof(unsafe.Pointer(nil)) } bucketBufSize := hashmapBucketHeaderSize + keySlotSize*8 + valueSlotSize*8 buckets := alloc(bucketBufSize*(1<