From 4dfc289ae5c8b55da6eab017e151972a1d803d5f Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 22 Jan 2020 17:19:21 +0100 Subject: [PATCH] compiler,runtime: support operations on nil map The index expression and delete keyword are valid on nil maps, so the runtime must be modified to support this. --- compiler/map.go | 19 ++++++++++++++----- src/runtime/hashmap.go | 27 ++++++++++++++++++++------- testdata/map.go | 2 ++ testdata/map.txt | 1 + 4 files changed, 37 insertions(+), 12 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index e4998d4b..5faf7aed 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -51,14 +51,23 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu // 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, mapValueSize := c.createTemporaryAlloca(llvmValueType, "hashmap.value") + mapValueAlloca, mapValuePtr, mapValueAllocaSize := c.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() > c.uintptrType.IntTypeWidth() { + mapValueSize = llvm.ConstTrunc(mapValueSize, c.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} + params := []llvm.Value{m, key, mapValuePtr, mapValueSize} commaOkValue = c.createRuntimeCall("hashmapStringGet", params, "") } else if hashmapIsBinaryKey(keyType) { // key can be compared with runtime.memequal @@ -67,7 +76,7 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu mapKeyAlloca, mapKeyPtr, mapKeySize := c.createTemporaryAlloca(key.Type(), "hashmap.key") c.builder.CreateStore(key, mapKeyAlloca) // Fetch the value from the hashmap. - params := []llvm.Value{m, mapKeyPtr, mapValuePtr} + params := []llvm.Value{m, mapKeyPtr, mapValuePtr, mapValueSize} commaOkValue = c.createRuntimeCall("hashmapBinaryGet", params, "") c.emitLifetimeEnd(mapKeyPtr, mapKeySize) } else { @@ -77,14 +86,14 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu // Not already an interface, so convert it to an interface now. itfKey = c.parseMakeInterface(key, keyType, pos) } - params := []llvm.Value{m, itfKey, mapValuePtr} + params := []llvm.Value{m, itfKey, mapValuePtr, mapValueSize} commaOkValue = c.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 := c.builder.CreateLoad(mapValueAlloca, "") - c.emitLifetimeEnd(mapValuePtr, mapValueSize) + c.emitLifetimeEnd(mapValuePtr, mapValueAllocaSize) if commaOk { tuple := llvm.Undef(c.ctx.StructType([]llvm.Type{llvmValueType, c.ctx.Int1Type()}, false)) diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index f9e164c4..bd940c75 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -166,7 +166,14 @@ func hashmapInsertIntoNewBucket(m *hashmap, key, value unsafe.Pointer, tophash u // Get the value of a specified key, or zero the value if not found. //go:nobounds -func hashmapGet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint32, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) bool { +func hashmapGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr, hash uint32, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) 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 + } numBuckets := uintptr(1) << m.bucketBits bucketNumber := (uintptr(hash) & (numBuckets - 1)) bucketSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8 @@ -207,6 +214,12 @@ func hashmapGet(m *hashmap, key unsafe.Pointer, value unsafe.Pointer, hash uint3 // map. //go:nobounds func hashmapDelete(m *hashmap, key unsafe.Pointer, hash uint32, keyEqual func(x, y unsafe.Pointer, n uintptr) bool) { + 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 + } numBuckets := uintptr(1) << m.bucketBits bucketNumber := (uintptr(hash) & (numBuckets - 1)) bucketSize := unsafe.Sizeof(hashmapBucket{}) + uintptr(m.keySize)*8 + uintptr(m.valueSize)*8 @@ -284,9 +297,9 @@ func hashmapBinarySet(m *hashmap, key, value unsafe.Pointer) { hashmapSet(m, key, value, hash, memequal) } -func hashmapBinaryGet(m *hashmap, key, value unsafe.Pointer) bool { +func hashmapBinaryGet(m *hashmap, key, value unsafe.Pointer, valueSize uintptr) bool { hash := hashmapHash(key, uintptr(m.keySize)) - return hashmapGet(m, key, value, hash, memequal) + return hashmapGet(m, key, value, valueSize, hash, memequal) } func hashmapBinaryDelete(m *hashmap, key unsafe.Pointer) { @@ -310,9 +323,9 @@ func hashmapStringSet(m *hashmap, key string, value unsafe.Pointer) { hashmapSet(m, unsafe.Pointer(&key), value, hash, hashmapStringEqual) } -func hashmapStringGet(m *hashmap, key string, value unsafe.Pointer) bool { +func hashmapStringGet(m *hashmap, key string, value unsafe.Pointer, valueSize uintptr) bool { hash := hashmapStringHash(key) - return hashmapGet(m, unsafe.Pointer(&key), value, hash, hashmapStringEqual) + return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash, hashmapStringEqual) } func hashmapStringDelete(m *hashmap, key string) { @@ -381,9 +394,9 @@ func hashmapInterfaceSet(m *hashmap, key interface{}, value unsafe.Pointer) { hashmapSet(m, unsafe.Pointer(&key), value, hash, hashmapInterfaceEqual) } -func hashmapInterfaceGet(m *hashmap, key interface{}, value unsafe.Pointer) bool { +func hashmapInterfaceGet(m *hashmap, key interface{}, value unsafe.Pointer, valueSize uintptr) bool { hash := hashmapInterfaceHash(key) - return hashmapGet(m, unsafe.Pointer(&key), value, hash, hashmapInterfaceEqual) + return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash, hashmapInterfaceEqual) } func hashmapInterfaceDelete(m *hashmap, key interface{}) { diff --git a/testdata/map.go b/testdata/map.go index 78834dd1..b0230eb9 100644 --- a/testdata/map.go +++ b/testdata/map.go @@ -44,6 +44,8 @@ func main() { var nilmap map[string]int println(m == nil, m != nil, len(m)) println(nilmap == nil, nilmap != nil, len(nilmap)) + delete(nilmap, "foo") + println("nilmap:", nilmap["bar"]) println(testmapIntInt[2]) testmapIntInt[2] = 42 println(testmapIntInt[2]) diff --git a/testdata/map.txt b/testdata/map.txt index bffd0307..e6141add 100644 --- a/testdata/map.txt +++ b/testdata/map.txt @@ -50,6 +50,7 @@ lookup with comma-ok: eight 8 true lookup with comma-ok: nokey 0 false false true 2 true false 0 +nilmap: 0 4 42 4321