Skip to content

Commit 0d34f93

Browse files
aykevldeadprogram
authored andcommitted
compiler,runtime: implement maps for arbitrary keys
This implementation simply casts types without special support to an interface, to make the implementation simpler and possibly reducing the code size too. It will likely be slower than the canonical Go implementation though (which builds special compare and hash functions at compile time).
1 parent 440dc8e commit 0d34f93

File tree

4 files changed

+171
-6
lines changed

4 files changed

+171
-6
lines changed

compiler/map.go

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,20 @@ import (
1414
// initializing an appropriately sized object.
1515
func (c *Compiler) emitMakeMap(frame *Frame, expr *ssa.MakeMap) (llvm.Value, error) {
1616
mapType := expr.Type().Underlying().(*types.Map)
17-
llvmKeyType := c.getLLVMType(mapType.Key().Underlying())
17+
keyType := mapType.Key().Underlying()
1818
llvmValueType := c.getLLVMType(mapType.Elem().Underlying())
19+
var llvmKeyType llvm.Type
20+
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
21+
// String keys.
22+
llvmKeyType = c.getLLVMType(keyType)
23+
} else if hashmapIsBinaryKey(keyType) {
24+
// Trivially comparable keys.
25+
llvmKeyType = c.getLLVMType(keyType)
26+
} else {
27+
// All other keys. Implemented as map[interface{}]valueType for ease of
28+
// implementation.
29+
llvmKeyType = c.getLLVMRuntimeType("_interface")
30+
}
1931
keySize := c.targetData.TypeAllocSize(llvmKeyType)
2032
valueSize := c.targetData.TypeAllocSize(llvmValueType)
2133
llvmKeySize := llvm.ConstInt(c.ctx.Int8Type(), keySize, false)
@@ -43,6 +55,7 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu
4355

4456
// Do the lookup. How it is done depends on the key type.
4557
var commaOkValue llvm.Value
58+
keyType = keyType.Underlying()
4659
if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 {
4760
// key is a string
4861
params := []llvm.Value{m, key, mapValuePtr}
@@ -58,8 +71,14 @@ func (c *Compiler) emitMapLookup(keyType, valueType types.Type, m, key llvm.Valu
5871
commaOkValue = c.createRuntimeCall("hashmapBinaryGet", params, "")
5972
c.emitLifetimeEnd(mapKeyPtr, mapKeySize)
6073
} else {
61-
// Not trivially comparable using memcmp.
62-
return llvm.Value{}, c.makeError(pos, "only strings, bools, ints, pointers or structs of bools/ints are supported as map keys, but got: "+keyType.String())
74+
// Not trivially comparable using memcmp. Make it an interface instead.
75+
itfKey := key
76+
if _, ok := keyType.(*types.Interface); !ok {
77+
// Not already an interface, so convert it to an interface now.
78+
itfKey = c.parseMakeInterface(key, keyType, pos)
79+
}
80+
params := []llvm.Value{m, itfKey, mapValuePtr}
81+
commaOkValue = c.createRuntimeCall("hashmapInterfaceGet", params, "")
6382
}
6483

6584
// Load the resulting value from the hashmap. The value is set to the zero
@@ -93,7 +112,14 @@ func (c *Compiler) emitMapUpdate(keyType types.Type, m, key, value llvm.Value, p
93112
c.createRuntimeCall("hashmapBinarySet", params, "")
94113
c.emitLifetimeEnd(keyPtr, keySize)
95114
} else {
96-
c.addError(pos, "only strings, bools, ints, pointers or structs of bools/ints are supported as map keys, but got: "+keyType.String())
115+
// Key is not trivially comparable, so compare it as an interface instead.
116+
itfKey := key
117+
if _, ok := keyType.(*types.Interface); !ok {
118+
// Not already an interface, so convert it to an interface first.
119+
itfKey = c.parseMakeInterface(key, keyType, pos)
120+
}
121+
params := []llvm.Value{m, itfKey, valuePtr}
122+
c.createRuntimeCall("hashmapInterfaceSet", params, "")
97123
}
98124
c.emitLifetimeEnd(valuePtr, valueSize)
99125
}
@@ -113,7 +139,16 @@ func (c *Compiler) emitMapDelete(keyType types.Type, m, key llvm.Value, pos toke
113139
c.emitLifetimeEnd(keyPtr, keySize)
114140
return nil
115141
} else {
116-
return c.makeError(pos, "only strings, bools, ints, pointers or structs of bools/ints are supported as map keys, but got: "+keyType.String())
142+
// Key is not trivially comparable, so compare it as an interface
143+
// instead.
144+
itfKey := key
145+
if _, ok := keyType.(*types.Interface); !ok {
146+
// Not already an interface, so convert it to an interface first.
147+
itfKey = c.parseMakeInterface(key, keyType, pos)
148+
}
149+
params := []llvm.Value{m, itfKey}
150+
c.createRuntimeCall("hashmapInterfaceDelete", params, "")
151+
return nil
117152
}
118153
}
119154

src/runtime/hashmap.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package runtime
66
// https://golang.org/src/runtime/map.go
77

88
import (
9+
"reflect"
910
"unsafe"
1011
)
1112

@@ -318,3 +319,74 @@ func hashmapStringDelete(m *hashmap, key string) {
318319
hash := hashmapStringHash(key)
319320
hashmapDelete(m, unsafe.Pointer(&key), hash, hashmapStringEqual)
320321
}
322+
323+
// Hashmap with interface keys (for everything else).
324+
325+
func hashmapInterfaceHash(itf interface{}) uint32 {
326+
x := reflect.ValueOf(itf)
327+
if x.Type() == 0 {
328+
return 0 // nil interface
329+
}
330+
331+
value := (*_interface)(unsafe.Pointer(&itf)).value
332+
ptr := value
333+
if x.Type().Size() <= unsafe.Sizeof(uintptr(0)) {
334+
// Value fits in pointer, so it's directly stored in the pointer.
335+
ptr = unsafe.Pointer(&value)
336+
}
337+
338+
switch x.Type().Kind() {
339+
case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64:
340+
return hashmapHash(ptr, x.Type().Size())
341+
case reflect.Bool, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Uintptr:
342+
return hashmapHash(ptr, x.Type().Size())
343+
case reflect.Float32, reflect.Float64, reflect.Complex64, reflect.Complex128:
344+
// It should be possible to just has the contents. However, NaN != NaN
345+
// so if you're using lots of NaNs as map keys (you shouldn't) then hash
346+
// time may become exponential. To fix that, it would be better to
347+
// return a random number instead:
348+
// https://research.swtch.com/randhash
349+
return hashmapHash(ptr, x.Type().Size())
350+
case reflect.String:
351+
return hashmapStringHash(x.String())
352+
case reflect.Chan, reflect.Ptr, reflect.UnsafePointer:
353+
// It might seem better to just return the pointer, but that won't
354+
// result in an evenly distributed hashmap. Instead, hash the pointer
355+
// like most other types.
356+
return hashmapHash(ptr, x.Type().Size())
357+
case reflect.Array:
358+
var hash uint32
359+
for i := 0; i < x.Len(); i++ {
360+
hash |= hashmapInterfaceHash(x.Index(i).Interface())
361+
}
362+
return hash
363+
case reflect.Struct:
364+
var hash uint32
365+
for i := 0; i < x.NumField(); i++ {
366+
hash |= hashmapInterfaceHash(x.Field(i).Interface())
367+
}
368+
return hash
369+
default:
370+
runtimePanic("comparing un-comparable type")
371+
return 0 // unreachable
372+
}
373+
}
374+
375+
func hashmapInterfaceEqual(x, y unsafe.Pointer, n uintptr) bool {
376+
return *(*interface{})(x) == *(*interface{})(y)
377+
}
378+
379+
func hashmapInterfaceSet(m *hashmap, key interface{}, value unsafe.Pointer) {
380+
hash := hashmapInterfaceHash(key)
381+
hashmapSet(m, unsafe.Pointer(&key), value, hash, hashmapInterfaceEqual)
382+
}
383+
384+
func hashmapInterfaceGet(m *hashmap, key interface{}, value unsafe.Pointer) bool {
385+
hash := hashmapInterfaceHash(key)
386+
return hashmapGet(m, unsafe.Pointer(&key), value, hash, hashmapInterfaceEqual)
387+
}
388+
389+
func hashmapInterfaceDelete(m *hashmap, key interface{}) {
390+
hash := hashmapInterfaceHash(key)
391+
hashmapDelete(m, unsafe.Pointer(&key), hash, hashmapInterfaceEqual)
392+
}

testdata/map.go

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,11 @@ var testMapArrayKey = map[ArrayKey]int{
2424
}
2525
var testmapIntInt = map[int]int{1: 1, 2: 4, 3: 9}
2626

27+
type namedFloat struct {
28+
s string
29+
f float32
30+
}
31+
2732
func main() {
2833
m := map[string]int{"answer": 42, "foo": 3}
2934
readMap(m, "answer")
@@ -48,6 +53,44 @@ func main() {
4853
testMapArrayKey[arrKey] = 5555
4954
println(testMapArrayKey[arrKey])
5055

56+
// test maps with interface keys
57+
itfMap := map[interface{}]int{
58+
3.14: 3,
59+
8: 8,
60+
uint8(8): 80,
61+
"eight": 800,
62+
[2]int{5, 2}: 52,
63+
true: 1,
64+
}
65+
println("itfMap[3]:", itfMap[3]) // doesn't exist
66+
println("itfMap[3.14]:", itfMap[3.14])
67+
println("itfMap[8]:", itfMap[8])
68+
println("itfMap[uint8(8)]:", itfMap[uint8(8)])
69+
println(`itfMap["eight"]:`, itfMap["eight"])
70+
println(`itfMap[[2]int{5, 2}]:`, itfMap[[2]int{5, 2}])
71+
println("itfMap[true]:", itfMap[true])
72+
delete(itfMap, 8)
73+
println("itfMap[8]:", itfMap[8])
74+
75+
// test map with float keys
76+
floatMap := map[float32]int{
77+
42: 84,
78+
}
79+
println("floatMap[42]:", floatMap[42])
80+
println("floatMap[43]:", floatMap[43])
81+
delete(floatMap, 42)
82+
println("floatMap[42]:", floatMap[42])
83+
84+
// test maps with struct keys
85+
structMap := map[namedFloat]int{
86+
namedFloat{"tau", 6.28}: 5,
87+
}
88+
println(`structMap[{"tau", 6.28}]:`, structMap[namedFloat{"tau", 6.28}])
89+
println(`structMap[{"Tau", 6.28}]:`, structMap[namedFloat{"Tau", 6.28}])
90+
println(`structMap[{"tau", 3.14}]:`, structMap[namedFloat{"tau", 3.14}])
91+
delete(structMap, namedFloat{"tau", 6.28})
92+
println(`structMap[{"tau", 6.28}]:`, structMap[namedFloat{"tau", 6.28}])
93+
5194
// test preallocated map
5295
squares := make(map[int]int, 200)
5396
testBigMap(squares, 100)
@@ -79,7 +122,7 @@ func testBigMap(squares map[int]int, n int) {
79122
if len(squares) != i {
80123
println("unexpected length:", len(squares), "at i =", i)
81124
}
82-
squares[i] = i*i
125+
squares[i] = i * i
83126
for j := 0; j <= i; j++ {
84127
if v, ok := squares[j]; !ok || v != j*j {
85128
if !ok {

testdata/map.txt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,20 @@ true false 0
5454
42
5555
4321
5656
5555
57+
itfMap[3]: 0
58+
itfMap[3.14]: 3
59+
itfMap[8]: 8
60+
itfMap[uint8(8)]: 80
61+
itfMap["eight"]: 800
62+
itfMap[[2]int{5, 2}]: 52
63+
itfMap[true]: 1
64+
itfMap[8]: 0
65+
floatMap[42]: 84
66+
floatMap[43]: 0
67+
floatMap[42]: 0
68+
structMap[{"tau", 6.28}]: 5
69+
structMap[{"Tau", 6.28}]: 0
70+
structMap[{"tau", 3.14}]: 0
71+
structMap[{"tau", 6.28}]: 0
5772
tested preallocated map
5873
tested growing of a map

0 commit comments

Comments
 (0)