
Some map keys are hard to compare, such as floats. They are stored as if the map keys are of interface type instead of the key type itself. This makes working with them in the runtime package easier: they are compared as regular interfaces. Iterating over maps didn't care about this special case though. It just returns the key, value pair as it is stored in the map. This is buggy, and this commit fixes this bug.
253 строки
10 КиБ
Go
253 строки
10 КиБ
Go
package compiler
|
|
|
|
// This file emits the correct map intrinsics for map operations.
|
|
|
|
import (
|
|
"go/token"
|
|
"go/types"
|
|
|
|
"golang.org/x/tools/go/ssa"
|
|
"tinygo.org/x/go-llvm"
|
|
)
|
|
|
|
// createMakeMap creates a new map object (runtime.hashmap) by allocating and
|
|
// initializing an appropriately sized object.
|
|
func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) {
|
|
mapType := expr.Type().Underlying().(*types.Map)
|
|
keyType := mapType.Key().Underlying()
|
|
llvmValueType := b.getLLVMType(mapType.Elem().Underlying())
|
|
var llvmKeyType llvm.Type
|
|
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
|
|
// String keys.
|
|
llvmKeyType = b.getLLVMType(keyType)
|
|
} else if hashmapIsBinaryKey(keyType) {
|
|
// Trivially comparable keys.
|
|
llvmKeyType = b.getLLVMType(keyType)
|
|
} else {
|
|
// All other keys. Implemented as map[interface{}]valueType for ease of
|
|
// implementation.
|
|
llvmKeyType = b.getLLVMRuntimeType("_interface")
|
|
}
|
|
keySize := b.targetData.TypeAllocSize(llvmKeyType)
|
|
valueSize := b.targetData.TypeAllocSize(llvmValueType)
|
|
llvmKeySize := llvm.ConstInt(b.ctx.Int8Type(), keySize, false)
|
|
llvmValueSize := llvm.ConstInt(b.ctx.Int8Type(), valueSize, false)
|
|
sizeHint := llvm.ConstInt(b.uintptrType, 8, false)
|
|
if expr.Reserve != nil {
|
|
sizeHint = b.getValue(expr.Reserve)
|
|
var err error
|
|
sizeHint, err = b.createConvert(expr.Reserve.Type(), types.Typ[types.Uintptr], sizeHint, expr.Pos())
|
|
if err != nil {
|
|
return llvm.Value{}, err
|
|
}
|
|
}
|
|
hashmap := b.createRuntimeCall("hashmapMake", []llvm.Value{llvmKeySize, llvmValueSize, sizeHint}, "")
|
|
return hashmap, nil
|
|
}
|
|
|
|
// createMapLookup returns the value in a map. It calls a runtime function
|
|
// depending on the map key type to load the map value and its comma-ok value.
|
|
func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Value, commaOk bool, pos token.Pos) (llvm.Value, error) {
|
|
llvmValueType := b.getLLVMType(valueType)
|
|
|
|
// Allocate the memory for the resulting type. Do not zero this memory: it
|
|
// will be zeroed by the hashmap get implementation if the key is not
|
|
// present in the map.
|
|
mapValueAlloca, mapValuePtr, mapValueAllocaSize := b.createTemporaryAlloca(llvmValueType, "hashmap.value")
|
|
|
|
// We need the map size (with type uintptr) to pass to the hashmap*Get
|
|
// functions. This is necessary because those *Get functions are valid on
|
|
// nil maps, and they'll need to zero the value pointer by that number of
|
|
// bytes.
|
|
mapValueSize := mapValueAllocaSize
|
|
if mapValueSize.Type().IntTypeWidth() > b.uintptrType.IntTypeWidth() {
|
|
mapValueSize = llvm.ConstTrunc(mapValueSize, b.uintptrType)
|
|
}
|
|
|
|
// Do the lookup. How it is done depends on the key type.
|
|
var commaOkValue llvm.Value
|
|
keyType = keyType.Underlying()
|
|
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
|
|
// key is a string
|
|
params := []llvm.Value{m, key, mapValuePtr, mapValueSize}
|
|
commaOkValue = b.createRuntimeCall("hashmapStringGet", params, "")
|
|
} else if hashmapIsBinaryKey(keyType) {
|
|
// key can be compared with runtime.memequal
|
|
// Store the key in an alloca, in the entry block to avoid dynamic stack
|
|
// growth.
|
|
mapKeyAlloca, mapKeyPtr, mapKeySize := b.createTemporaryAlloca(key.Type(), "hashmap.key")
|
|
b.CreateStore(key, mapKeyAlloca)
|
|
// Fetch the value from the hashmap.
|
|
params := []llvm.Value{m, mapKeyPtr, mapValuePtr, mapValueSize}
|
|
commaOkValue = b.createRuntimeCall("hashmapBinaryGet", params, "")
|
|
b.emitLifetimeEnd(mapKeyPtr, mapKeySize)
|
|
} else {
|
|
// Not trivially comparable using memcmp. Make it an interface instead.
|
|
itfKey := key
|
|
if _, ok := keyType.(*types.Interface); !ok {
|
|
// Not already an interface, so convert it to an interface now.
|
|
itfKey = b.createMakeInterface(key, keyType, pos)
|
|
}
|
|
params := []llvm.Value{m, itfKey, mapValuePtr, mapValueSize}
|
|
commaOkValue = b.createRuntimeCall("hashmapInterfaceGet", params, "")
|
|
}
|
|
|
|
// Load the resulting value from the hashmap. The value is set to the zero
|
|
// value if the key doesn't exist in the hashmap.
|
|
mapValue := b.CreateLoad(mapValueAlloca, "")
|
|
b.emitLifetimeEnd(mapValuePtr, mapValueAllocaSize)
|
|
|
|
if commaOk {
|
|
tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{llvmValueType, b.ctx.Int1Type()}, false))
|
|
tuple = b.CreateInsertValue(tuple, mapValue, 0, "")
|
|
tuple = b.CreateInsertValue(tuple, commaOkValue, 1, "")
|
|
return tuple, nil
|
|
} else {
|
|
return mapValue, nil
|
|
}
|
|
}
|
|
|
|
// createMapUpdate updates a map key to a given value, by creating an
|
|
// appropriate runtime call.
|
|
func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, pos token.Pos) {
|
|
valueAlloca, valuePtr, valueSize := b.createTemporaryAlloca(value.Type(), "hashmap.value")
|
|
b.CreateStore(value, valueAlloca)
|
|
keyType = keyType.Underlying()
|
|
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
|
|
// key is a string
|
|
params := []llvm.Value{m, key, valuePtr}
|
|
b.createRuntimeCall("hashmapStringSet", params, "")
|
|
} else if hashmapIsBinaryKey(keyType) {
|
|
// key can be compared with runtime.memequal
|
|
keyAlloca, keyPtr, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key")
|
|
b.CreateStore(key, keyAlloca)
|
|
params := []llvm.Value{m, keyPtr, valuePtr}
|
|
b.createRuntimeCall("hashmapBinarySet", params, "")
|
|
b.emitLifetimeEnd(keyPtr, keySize)
|
|
} else {
|
|
// Key is not trivially comparable, so compare it as an interface instead.
|
|
itfKey := key
|
|
if _, ok := keyType.(*types.Interface); !ok {
|
|
// Not already an interface, so convert it to an interface first.
|
|
itfKey = b.createMakeInterface(key, keyType, pos)
|
|
}
|
|
params := []llvm.Value{m, itfKey, valuePtr}
|
|
b.createRuntimeCall("hashmapInterfaceSet", params, "")
|
|
}
|
|
b.emitLifetimeEnd(valuePtr, valueSize)
|
|
}
|
|
|
|
// createMapDelete deletes a key from a map by calling the appropriate runtime
|
|
// function. It is the implementation of the Go delete() builtin.
|
|
func (b *builder) createMapDelete(keyType types.Type, m, key llvm.Value, pos token.Pos) error {
|
|
keyType = keyType.Underlying()
|
|
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
|
|
// key is a string
|
|
params := []llvm.Value{m, key}
|
|
b.createRuntimeCall("hashmapStringDelete", params, "")
|
|
return nil
|
|
} else if hashmapIsBinaryKey(keyType) {
|
|
keyAlloca, keyPtr, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key")
|
|
b.CreateStore(key, keyAlloca)
|
|
params := []llvm.Value{m, keyPtr}
|
|
b.createRuntimeCall("hashmapBinaryDelete", params, "")
|
|
b.emitLifetimeEnd(keyPtr, keySize)
|
|
return nil
|
|
} else {
|
|
// Key is not trivially comparable, so compare it as an interface
|
|
// instead.
|
|
itfKey := key
|
|
if _, ok := keyType.(*types.Interface); !ok {
|
|
// Not already an interface, so convert it to an interface first.
|
|
itfKey = b.createMakeInterface(key, keyType, pos)
|
|
}
|
|
params := []llvm.Value{m, itfKey}
|
|
b.createRuntimeCall("hashmapInterfaceDelete", params, "")
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// createMapIteratorNext lowers the *ssa.Next instruction for iterating over a
|
|
// map. It returns a tuple of {bool, key, value} with the result of the
|
|
// iteration.
|
|
func (b *builder) createMapIteratorNext(rangeVal ssa.Value, llvmRangeVal, it llvm.Value) llvm.Value {
|
|
// Determine the type of the values to return from the *ssa.Next
|
|
// instruction. It is returned as {bool, keyType, valueType}.
|
|
keyType := rangeVal.Type().Underlying().(*types.Map).Key()
|
|
valueType := rangeVal.Type().Underlying().(*types.Map).Elem()
|
|
llvmKeyType := b.getLLVMType(keyType)
|
|
llvmValueType := b.getLLVMType(valueType)
|
|
|
|
// There is a special case in which keys are stored as an interface value
|
|
// instead of the value they normally are. This happens for non-trivially
|
|
// comparable types such as float32 or some structs.
|
|
isKeyStoredAsInterface := false
|
|
if t, ok := keyType.Underlying().(*types.Basic); ok && t.Info()&types.IsString != 0 {
|
|
// key is a string
|
|
} else if hashmapIsBinaryKey(keyType) {
|
|
// key can be compared with runtime.memequal
|
|
} else {
|
|
// The key is stored as an interface value, and may or may not be an
|
|
// interface type (for example, float32 keys are stored as an interface
|
|
// value).
|
|
if _, ok := keyType.Underlying().(*types.Interface); !ok {
|
|
isKeyStoredAsInterface = true
|
|
}
|
|
}
|
|
|
|
// Determine the type of the key as stored in the map.
|
|
llvmStoredKeyType := llvmKeyType
|
|
if isKeyStoredAsInterface {
|
|
llvmStoredKeyType = b.getLLVMRuntimeType("_interface")
|
|
}
|
|
|
|
// Extract the key and value from the map.
|
|
mapKeyAlloca, mapKeyPtr, mapKeySize := b.createTemporaryAlloca(llvmStoredKeyType, "range.key")
|
|
mapValueAlloca, mapValuePtr, mapValueSize := b.createTemporaryAlloca(llvmValueType, "range.value")
|
|
ok := b.createRuntimeCall("hashmapNext", []llvm.Value{llvmRangeVal, it, mapKeyPtr, mapValuePtr}, "range.next")
|
|
mapKey := b.CreateLoad(mapKeyAlloca, "")
|
|
mapValue := b.CreateLoad(mapValueAlloca, "")
|
|
|
|
if isKeyStoredAsInterface {
|
|
// The key is stored as an interface but it isn't of interface type.
|
|
// Extract the underlying value.
|
|
mapKey = b.extractValueFromInterface(mapKey, llvmKeyType)
|
|
}
|
|
|
|
// End the lifetimes of the allocas, because we're done with them.
|
|
b.emitLifetimeEnd(mapKeyPtr, mapKeySize)
|
|
b.emitLifetimeEnd(mapValuePtr, mapValueSize)
|
|
|
|
// Construct the *ssa.Next return value: {ok, mapKey, mapValue}
|
|
tuple := llvm.Undef(b.ctx.StructType([]llvm.Type{b.ctx.Int1Type(), llvmKeyType, llvmValueType}, false))
|
|
tuple = b.CreateInsertValue(tuple, ok, 0, "")
|
|
tuple = b.CreateInsertValue(tuple, mapKey, 1, "")
|
|
tuple = b.CreateInsertValue(tuple, mapValue, 2, "")
|
|
|
|
return tuple
|
|
}
|
|
|
|
// Returns true if this key type does not contain strings, interfaces etc., so
|
|
// can be compared with runtime.memequal.
|
|
func hashmapIsBinaryKey(keyType types.Type) bool {
|
|
switch keyType := keyType.(type) {
|
|
case *types.Basic:
|
|
return keyType.Info()&(types.IsBoolean|types.IsInteger) != 0
|
|
case *types.Pointer:
|
|
return true
|
|
case *types.Struct:
|
|
for i := 0; i < keyType.NumFields(); i++ {
|
|
fieldType := keyType.Field(i).Type().Underlying()
|
|
if !hashmapIsBinaryKey(fieldType) {
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
case *types.Array:
|
|
return hashmapIsBinaryKey(keyType.Elem())
|
|
case *types.Named:
|
|
return hashmapIsBinaryKey(keyType.Underlying())
|
|
default:
|
|
return false
|
|
}
|
|
}
|