github.com/tinygo-org/tinygo@v0.31.3-0.20240404173401-90b0bf646c27/compiler/map.go (about) 1 package compiler 2 3 // This file emits the correct map intrinsics for map operations. 4 5 import ( 6 "go/token" 7 "go/types" 8 9 "golang.org/x/tools/go/ssa" 10 "tinygo.org/x/go-llvm" 11 ) 12 13 // constants for hashmap algorithms; must match src/runtime/hashmap.go 14 const ( 15 hashmapAlgorithmBinary = iota 16 hashmapAlgorithmString 17 hashmapAlgorithmInterface 18 ) 19 20 // createMakeMap creates a new map object (runtime.hashmap) by allocating and 21 // initializing an appropriately sized object. 22 func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) { 23 mapType := expr.Type().Underlying().(*types.Map) 24 keyType := mapType.Key().Underlying() 25 llvmValueType := b.getLLVMType(mapType.Elem().Underlying()) 26 var llvmKeyType llvm.Type 27 var alg uint64 // must match values in src/runtime/hashmap.go 28 if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { 29 // String keys. 30 llvmKeyType = b.getLLVMType(keyType) 31 alg = hashmapAlgorithmString 32 } else if hashmapIsBinaryKey(keyType) { 33 // Trivially comparable keys. 34 llvmKeyType = b.getLLVMType(keyType) 35 alg = hashmapAlgorithmBinary 36 } else { 37 // All other keys. Implemented as map[interface{}]valueType for ease of 38 // implementation. 39 llvmKeyType = b.getLLVMRuntimeType("_interface") 40 alg = hashmapAlgorithmInterface 41 } 42 keySize := b.targetData.TypeAllocSize(llvmKeyType) 43 valueSize := b.targetData.TypeAllocSize(llvmValueType) 44 llvmKeySize := llvm.ConstInt(b.uintptrType, keySize, false) 45 llvmValueSize := llvm.ConstInt(b.uintptrType, valueSize, false) 46 sizeHint := llvm.ConstInt(b.uintptrType, 8, false) 47 algEnum := llvm.ConstInt(b.ctx.Int8Type(), alg, false) 48 if expr.Reserve != nil { 49 sizeHint = b.getValue(expr.Reserve, getPos(expr)) 50 var err error 51 sizeHint, err = b.createConvert(expr.Reserve.Type(), types.Typ[types.Uintptr], sizeHint, expr.Pos()) 52 if err != nil { 53 return llvm.Value{}, err 54 } 55 } 56 hashmap := b.createRuntimeCall("hashmapMake", []llvm.Value{llvmKeySize, llvmValueSize, sizeHint, algEnum}, "") 57 return hashmap, nil 58 } 59 60 // createMapLookup returns the value in a map. It calls a runtime function 61 // depending on the map key type to load the map value and its comma-ok value. 62 func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Value, commaOk bool, pos token.Pos) (llvm.Value, error) { 63 llvmValueType := b.getLLVMType(valueType) 64 65 // Allocate the memory for the resulting type. Do not zero this memory: it 66 // will be zeroed by the hashmap get implementation if the key is not 67 // present in the map. 68 mapValueAlloca, mapValueAllocaSize := b.createTemporaryAlloca(llvmValueType, "hashmap.value") 69 70 // We need the map size (with type uintptr) to pass to the hashmap*Get 71 // functions. This is necessary because those *Get functions are valid on 72 // nil maps, and they'll need to zero the value pointer by that number of 73 // bytes. 74 mapValueSize := mapValueAllocaSize 75 if mapValueSize.Type().IntTypeWidth() > b.uintptrType.IntTypeWidth() { 76 mapValueSize = llvm.ConstTrunc(mapValueSize, b.uintptrType) 77 } 78 79 // Do the lookup. How it is done depends on the key type. 80 var commaOkValue llvm.Value 81 origKeyType := keyType 82 keyType = keyType.Underlying() 83 if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { 84 // key is a string 85 params := []llvm.Value{m, key, mapValueAlloca, mapValueSize} 86 commaOkValue = b.createRuntimeCall("hashmapStringGet", params, "") 87 } else if hashmapIsBinaryKey(keyType) { 88 // key can be compared with runtime.memequal 89 // Store the key in an alloca, in the entry block to avoid dynamic stack 90 // growth. 91 mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") 92 b.CreateStore(key, mapKeyAlloca) 93 b.zeroUndefBytes(b.getLLVMType(keyType), mapKeyAlloca) 94 // Fetch the value from the hashmap. 95 params := []llvm.Value{m, mapKeyAlloca, mapValueAlloca, mapValueSize} 96 commaOkValue = b.createRuntimeCall("hashmapBinaryGet", params, "") 97 b.emitLifetimeEnd(mapKeyAlloca, mapKeySize) 98 } else { 99 // Not trivially comparable using memcmp. Make it an interface instead. 100 itfKey := key 101 if _, ok := keyType.(*types.Interface); !ok { 102 // Not already an interface, so convert it to an interface now. 103 itfKey = b.createMakeInterface(key, origKeyType, pos) 104 } 105 params := []llvm.Value{m, itfKey, mapValueAlloca, mapValueSize} 106 commaOkValue = b.createRuntimeCall("hashmapInterfaceGet", params, "") 107 } 108 109 // Load the resulting value from the hashmap. The value is set to the zero 110 // value if the key doesn't exist in the hashmap. 111 mapValue := b.CreateLoad(llvmValueType, mapValueAlloca, "") 112 b.emitLifetimeEnd(mapValueAlloca, mapValueAllocaSize) 113 114 if commaOk { 115 tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{llvmValueType, b.ctx.Int1Type()}, false)) 116 tuple = b.CreateInsertValue(tuple, mapValue, 0, "") 117 tuple = b.CreateInsertValue(tuple, commaOkValue, 1, "") 118 return tuple, nil 119 } else { 120 return mapValue, nil 121 } 122 } 123 124 // createMapUpdate updates a map key to a given value, by creating an 125 // appropriate runtime call. 126 func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, pos token.Pos) { 127 valueAlloca, valueSize := b.createTemporaryAlloca(value.Type(), "hashmap.value") 128 b.CreateStore(value, valueAlloca) 129 origKeyType := keyType 130 keyType = keyType.Underlying() 131 if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { 132 // key is a string 133 params := []llvm.Value{m, key, valueAlloca} 134 b.createRuntimeCall("hashmapStringSet", params, "") 135 } else if hashmapIsBinaryKey(keyType) { 136 // key can be compared with runtime.memequal 137 keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") 138 b.CreateStore(key, keyAlloca) 139 b.zeroUndefBytes(b.getLLVMType(keyType), keyAlloca) 140 params := []llvm.Value{m, keyAlloca, valueAlloca} 141 b.createRuntimeCall("hashmapBinarySet", params, "") 142 b.emitLifetimeEnd(keyAlloca, keySize) 143 } else { 144 // Key is not trivially comparable, so compare it as an interface instead. 145 itfKey := key 146 if _, ok := keyType.(*types.Interface); !ok { 147 // Not already an interface, so convert it to an interface first. 148 itfKey = b.createMakeInterface(key, origKeyType, pos) 149 } 150 params := []llvm.Value{m, itfKey, valueAlloca} 151 b.createRuntimeCall("hashmapInterfaceSet", params, "") 152 } 153 b.emitLifetimeEnd(valueAlloca, valueSize) 154 } 155 156 // createMapDelete deletes a key from a map by calling the appropriate runtime 157 // function. It is the implementation of the Go delete() builtin. 158 func (b *builder) createMapDelete(keyType types.Type, m, key llvm.Value, pos token.Pos) error { 159 origKeyType := keyType 160 keyType = keyType.Underlying() 161 if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { 162 // key is a string 163 params := []llvm.Value{m, key} 164 b.createRuntimeCall("hashmapStringDelete", params, "") 165 return nil 166 } else if hashmapIsBinaryKey(keyType) { 167 keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") 168 b.CreateStore(key, keyAlloca) 169 b.zeroUndefBytes(b.getLLVMType(keyType), keyAlloca) 170 params := []llvm.Value{m, keyAlloca} 171 b.createRuntimeCall("hashmapBinaryDelete", params, "") 172 b.emitLifetimeEnd(keyAlloca, keySize) 173 return nil 174 } else { 175 // Key is not trivially comparable, so compare it as an interface 176 // instead. 177 itfKey := key 178 if _, ok := keyType.(*types.Interface); !ok { 179 // Not already an interface, so convert it to an interface first. 180 itfKey = b.createMakeInterface(key, origKeyType, pos) 181 } 182 params := []llvm.Value{m, itfKey} 183 b.createRuntimeCall("hashmapInterfaceDelete", params, "") 184 return nil 185 } 186 } 187 188 // Clear the given map. 189 func (b *builder) createMapClear(m llvm.Value) { 190 b.createRuntimeCall("hashmapClear", []llvm.Value{m}, "") 191 } 192 193 // createMapIteratorNext lowers the *ssa.Next instruction for iterating over a 194 // map. It returns a tuple of {bool, key, value} with the result of the 195 // iteration. 196 func (b *builder) createMapIteratorNext(rangeVal ssa.Value, llvmRangeVal, it llvm.Value) llvm.Value { 197 // Determine the type of the values to return from the *ssa.Next 198 // instruction. It is returned as {bool, keyType, valueType}. 199 keyType := rangeVal.Type().Underlying().(*types.Map).Key() 200 valueType := rangeVal.Type().Underlying().(*types.Map).Elem() 201 llvmKeyType := b.getLLVMType(keyType) 202 llvmValueType := b.getLLVMType(valueType) 203 204 // There is a special case in which keys are stored as an interface value 205 // instead of the value they normally are. This happens for non-trivially 206 // comparable types such as float32 or some structs. 207 isKeyStoredAsInterface := false 208 if t, ok := keyType.Underlying().(*types.Basic); ok && t.Info()&types.IsString != 0 { 209 // key is a string 210 } else if hashmapIsBinaryKey(keyType) { 211 // key can be compared with runtime.memequal 212 } else { 213 // The key is stored as an interface value, and may or may not be an 214 // interface type (for example, float32 keys are stored as an interface 215 // value). 216 if _, ok := keyType.Underlying().(*types.Interface); !ok { 217 isKeyStoredAsInterface = true 218 } 219 } 220 221 // Determine the type of the key as stored in the map. 222 llvmStoredKeyType := llvmKeyType 223 if isKeyStoredAsInterface { 224 llvmStoredKeyType = b.getLLVMRuntimeType("_interface") 225 } 226 227 // Extract the key and value from the map. 228 mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(llvmStoredKeyType, "range.key") 229 mapValueAlloca, mapValueSize := b.createTemporaryAlloca(llvmValueType, "range.value") 230 ok := b.createRuntimeCall("hashmapNext", []llvm.Value{llvmRangeVal, it, mapKeyAlloca, mapValueAlloca}, "range.next") 231 mapKey := b.CreateLoad(llvmStoredKeyType, mapKeyAlloca, "") 232 mapValue := b.CreateLoad(llvmValueType, mapValueAlloca, "") 233 234 if isKeyStoredAsInterface { 235 // The key is stored as an interface but it isn't of interface type. 236 // Extract the underlying value. 237 mapKey = b.extractValueFromInterface(mapKey, llvmKeyType) 238 } 239 240 // End the lifetimes of the allocas, because we're done with them. 241 b.emitLifetimeEnd(mapKeyAlloca, mapKeySize) 242 b.emitLifetimeEnd(mapValueAlloca, mapValueSize) 243 244 // Construct the *ssa.Next return value: {ok, mapKey, mapValue} 245 tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{b.ctx.Int1Type(), llvmKeyType, llvmValueType}, false)) 246 tuple = b.CreateInsertValue(tuple, ok, 0, "") 247 tuple = b.CreateInsertValue(tuple, mapKey, 1, "") 248 tuple = b.CreateInsertValue(tuple, mapValue, 2, "") 249 250 return tuple 251 } 252 253 // Returns true if this key type does not contain strings, interfaces etc., so 254 // can be compared with runtime.memequal. Note that padding bytes are undef 255 // and can alter two "equal" structs being equal when compared with memequal. 256 func hashmapIsBinaryKey(keyType types.Type) bool { 257 switch keyType := keyType.(type) { 258 case *types.Basic: 259 return keyType.Info()&(types.IsBoolean|types.IsInteger) != 0 260 case *types.Pointer: 261 return true 262 case *types.Struct: 263 for i := 0; i < keyType.NumFields(); i++ { 264 fieldType := keyType.Field(i).Type().Underlying() 265 if !hashmapIsBinaryKey(fieldType) { 266 return false 267 } 268 } 269 return true 270 case *types.Array: 271 return hashmapIsBinaryKey(keyType.Elem()) 272 case *types.Named: 273 return hashmapIsBinaryKey(keyType.Underlying()) 274 default: 275 return false 276 } 277 } 278 279 func (b *builder) zeroUndefBytes(llvmType llvm.Type, ptr llvm.Value) error { 280 // We know that hashmapIsBinaryKey is true, so we only have to handle those types that can show up there. 281 // To zero all undefined bytes, we iterate over all the fields in the type. For each element, compute the 282 // offset of that element. If it's Basic type, there are no internal padding bytes. For compound types, we recurse to ensure 283 // we handle nested types. Next, we determine if there are any padding bytes before the next 284 // element and zero those as well. 285 286 zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) 287 288 switch llvmType.TypeKind() { 289 case llvm.IntegerTypeKind: 290 // no padding bytes 291 return nil 292 case llvm.PointerTypeKind: 293 // mo padding bytes 294 return nil 295 case llvm.ArrayTypeKind: 296 llvmArrayType := llvmType 297 llvmElemType := llvmType.ElementType() 298 299 for i := 0; i < llvmArrayType.ArrayLength(); i++ { 300 idx := llvm.ConstInt(b.uintptrType, uint64(i), false) 301 elemPtr := b.CreateInBoundsGEP(llvmArrayType, ptr, []llvm.Value{zero, idx}, "") 302 303 // zero any padding bytes in this element 304 b.zeroUndefBytes(llvmElemType, elemPtr) 305 } 306 307 case llvm.StructTypeKind: 308 llvmStructType := llvmType 309 numFields := llvmStructType.StructElementTypesCount() 310 llvmElementTypes := llvmStructType.StructElementTypes() 311 312 for i := 0; i < numFields; i++ { 313 idx := llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false) 314 elemPtr := b.CreateInBoundsGEP(llvmStructType, ptr, []llvm.Value{zero, idx}, "") 315 316 // zero any padding bytes in this field 317 llvmElemType := llvmElementTypes[i] 318 b.zeroUndefBytes(llvmElemType, elemPtr) 319 320 // zero any padding bytes before the next field, if any 321 offset := b.targetData.ElementOffset(llvmStructType, i) 322 storeSize := b.targetData.TypeStoreSize(llvmElemType) 323 fieldEndOffset := offset + storeSize 324 325 var nextOffset uint64 326 if i < numFields-1 { 327 nextOffset = b.targetData.ElementOffset(llvmStructType, i+1) 328 } else { 329 // Last field? Next offset is the total size of the allcoate struct. 330 nextOffset = b.targetData.TypeAllocSize(llvmStructType) 331 } 332 333 if fieldEndOffset != nextOffset { 334 n := llvm.ConstInt(b.uintptrType, nextOffset-fieldEndOffset, false) 335 llvmStoreSize := llvm.ConstInt(b.uintptrType, storeSize, false) 336 paddingStart := b.CreateInBoundsGEP(b.ctx.Int8Type(), elemPtr, []llvm.Value{llvmStoreSize}, "") 337 b.createRuntimeCall("memzero", []llvm.Value{paddingStart, n}, "") 338 } 339 } 340 } 341 342 return nil 343 }