From f0ee43c531550c3f91b24d645e32c7a71ffefc0a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 17 May 2026 12:03:55 -0700 Subject: [PATCH 1/6] compiler, runtime, reflect: generate type-specific hash/equal for composite map keys For map keys that are not trivially binary-comparable, the compiler now generates type-specific hash and equal functions as LLVM IR instead of going through the interface+reflection path. This covers comparable types: strings, floats, complex numbers, interfaces, channels, and composites containing any mix of these. Previously, maps with composite keys containing strings or floats converted the key to interface{}, hashed via reflection, and compared through interface equality. Now the compiler walks struct fields and array elements directly, dispatching to the right runtime helper for each field type and storing keys at their actual type. Struct keys are always handled field-by-field so padding bytes do not affect equality or hashing. Blank fields are ignored, matching Go equality. Generated hash/equal function names use canonical underlying type structure so structurally identical key types can share generated functions. Padding zeroing before map operations is no longer needed because structs no longer use the binary key path. Also fix reflect map iteration for interface-keyed maps: MapIter.Key returns an interface Value for map[interface{}] keys instead of unpacking to the concrete key kind. --- compiler/map.go | 479 ++++++++++++++++++++++++------ compiler/symbol.go | 25 ++ compiler/testdata/zeromap.ll | 46 +-- src/internal/reflectlite/value.go | 124 +++++++- src/runtime/hashmap.go | 70 ++++- testdata/map.go | 62 ++++ testdata/map.txt | 5 + tests/mapbench/mapbench_test.go | 68 +++++ 8 files changed, 738 insertions(+), 141 deletions(-) create mode 100644 tests/mapbench/mapbench_test.go diff --git a/compiler/map.go b/compiler/map.go index 6aaf8c76c6..1e8b5c4f7d 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -3,6 +3,7 @@ package compiler // This file emits the correct map intrinsics for map operations. import ( + "fmt" "go/token" "go/types" @@ -17,28 +18,13 @@ 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 - var alg uint64 - if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { - // String keys. - llvmKeyType = b.getLLVMType(keyType) - alg = uint64(tinygo.HashmapAlgorithmString) - } else if hashmapIsBinaryKey(keyType) { - // Trivially comparable keys. - llvmKeyType = b.getLLVMType(keyType) - alg = uint64(tinygo.HashmapAlgorithmBinary) - } else { - // All other keys. Implemented as map[interface{}]valueType for ease of - // implementation. - llvmKeyType = b.getLLVMRuntimeType("_interface") - alg = uint64(tinygo.HashmapAlgorithmInterface) - } + llvmKeyType := b.getLLVMType(keyType) + keySize := b.targetData.TypeAllocSize(llvmKeyType) valueSize := b.targetData.TypeAllocSize(llvmValueType) llvmKeySize := llvm.ConstInt(b.uintptrType, keySize, false) llvmValueSize := llvm.ConstInt(b.uintptrType, valueSize, false) sizeHint := llvm.ConstInt(b.uintptrType, 8, false) - algEnum := llvm.ConstInt(b.ctx.Int8Type(), alg, false) if expr.Reserve != nil { sizeHint = b.getValue(expr.Reserve, getPos(expr)) var err error @@ -47,6 +33,35 @@ func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) { return llvm.Value{}, err } } + + if hashmapCanGenerateHashEqual(keyType) && !hashmapIsBinaryKey(keyType) { + // Composite keys: use compiler-generated hash/equal functions. + // Binary and string keys use the more efficient dedicated paths + // (hashmapMake with algorithm enum) which avoid function pointer + // indirection. + hashFn := b.getOrGenerateKeyHashFunc(keyType) + equalFn := b.getOrGenerateKeyEqualFunc(keyType) + hashFuncValue := b.createFuncValue(hashFn, llvm.ConstNull(b.dataPtrType), hashmapKeyHashSignature()) + equalFuncValue := b.createFuncValue(equalFn, llvm.ConstNull(b.dataPtrType), hashmapKeyEqualSignature()) + hashmap := b.createRuntimeCall("hashmapMakeGeneric", []llvm.Value{ + llvmKeySize, llvmValueSize, sizeHint, + hashFuncValue, equalFuncValue, + }, "") + return hashmap, nil + } + + var alg uint64 + if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { + alg = uint64(tinygo.HashmapAlgorithmString) + } else if hashmapIsBinaryKey(keyType) { + alg = uint64(tinygo.HashmapAlgorithmBinary) + } else { + // Fallback for types not handled by hashmapCanGenerateHashEqual + // (currently only unsafe.Pointer due to an interp issue). + llvmKeyType = b.getLLVMRuntimeType("_interface") + alg = uint64(tinygo.HashmapAlgorithmInterface) + } + algEnum := llvm.ConstInt(b.ctx.Int8Type(), alg, false) hashmap := b.createRuntimeCall("hashmapMake", []llvm.Value{llvmKeySize, llvmValueSize, sizeHint, algEnum}, "") return hashmap, nil } @@ -78,16 +93,17 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Val // key is a string params := []llvm.Value{m, key, mapValueAlloca, 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. + } else if hashmapIsBinaryKey(keyType) || hashmapCanGenerateHashEqual(keyType) { + // Key stored at actual type: either binary-comparable or with + // compiler-generated hash/equal. mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") b.CreateStore(key, mapKeyAlloca) - b.zeroUndefBytes(b.getLLVMType(keyType), mapKeyAlloca) - // Fetch the value from the hashmap. params := []llvm.Value{m, mapKeyAlloca, mapValueAlloca, mapValueSize} - commaOkValue = b.createRuntimeCall("hashmapBinaryGet", params, "") + fnName := "hashmapBinaryGet" + if !hashmapIsBinaryKey(keyType) { + fnName = "hashmapGenericGet" + } + commaOkValue = b.createRuntimeCall(fnName, params, "") b.emitLifetimeEnd(mapKeyAlloca, mapKeySize) } else { // Not trivially comparable using memcmp. Make it an interface instead. @@ -126,13 +142,16 @@ func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, // key is a string params := []llvm.Value{m, key, valueAlloca} b.createRuntimeCall("hashmapStringSet", params, "") - } else if hashmapIsBinaryKey(keyType) { - // key can be compared with runtime.memequal + } else if hashmapIsBinaryKey(keyType) || hashmapCanGenerateHashEqual(keyType) { + // Key stored at actual type. keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") b.CreateStore(key, keyAlloca) - b.zeroUndefBytes(b.getLLVMType(keyType), keyAlloca) + fnName := "hashmapBinarySet" + if !hashmapIsBinaryKey(keyType) { + fnName = "hashmapGenericSet" + } params := []llvm.Value{m, keyAlloca, valueAlloca} - b.createRuntimeCall("hashmapBinarySet", params, "") + b.createRuntimeCall(fnName, params, "") b.emitLifetimeEnd(keyAlloca, keySize) } else { // Key is not trivially comparable, so compare it as an interface instead. @@ -157,12 +176,16 @@ func (b *builder) createMapDelete(keyType types.Type, m, key llvm.Value, pos tok params := []llvm.Value{m, key} b.createRuntimeCall("hashmapStringDelete", params, "") return nil - } else if hashmapIsBinaryKey(keyType) { + } else if hashmapIsBinaryKey(keyType) || hashmapCanGenerateHashEqual(keyType) { + // Key stored at actual type. keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") b.CreateStore(key, keyAlloca) - b.zeroUndefBytes(b.getLLVMType(keyType), keyAlloca) + fnName := "hashmapBinaryDelete" + if !hashmapIsBinaryKey(keyType) { + fnName = "hashmapGenericDelete" + } params := []llvm.Value{m, keyAlloca} - b.createRuntimeCall("hashmapBinaryDelete", params, "") + b.createRuntimeCall(fnName, params, "") b.emitLifetimeEnd(keyAlloca, keySize) return nil } else { @@ -195,18 +218,14 @@ func (b *builder) createMapIteratorNext(rangeVal ssa.Value, llvmRangeVal, it llv 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. + // Keys are stored as an interface value only for types not handled by + // the binary or generic paths (currently only unsafe.Pointer). 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 if hashmapIsBinaryKey(keyType) || hashmapCanGenerateHashEqual(keyType) { + // key stored at actual type } 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 } @@ -256,83 +275,361 @@ func hashmapIsBinaryKey(keyType types.Type) bool { return keyType.Info()&(types.IsBoolean|types.IsInteger) != 0 case *types.Pointer: return true + case *types.Array: + return hashmapIsBinaryKey(keyType.Elem()) + default: + return false + } +} + +// hashmapCanGenerateHashEqual returns true if the compiler can generate +// type-specific hash and equal functions for this key type. This covers all +// comparable types: integers, booleans, strings, floats, complex numbers, +// pointers, channels, interfaces, and composites (structs/arrays) of these. +func hashmapCanGenerateHashEqual(keyType types.Type) bool { + switch keyType := keyType.Underlying().(type) { + case *types.Basic: + // Note: unsafe.Pointer is excluded (not IsBoolean/IsInteger/etc.) + // due to a known interp issue (see hashmapIsBinaryKey). + return keyType.Info()&(types.IsBoolean|types.IsInteger|types.IsString|types.IsFloat|types.IsComplex) != 0 + case *types.Pointer: + return true + case *types.Chan: + return true + case *types.Interface: + return true case *types.Struct: for i := 0; i < keyType.NumFields(); i++ { fieldType := keyType.Field(i).Type().Underlying() - if !hashmapIsBinaryKey(fieldType) { + if !hashmapCanGenerateHashEqual(fieldType) { return false } } return true case *types.Array: - return hashmapIsBinaryKey(keyType.Elem()) + return hashmapCanGenerateHashEqual(keyType.Elem()) default: return false } } -func (b *builder) zeroUndefBytes(llvmType llvm.Type, ptr llvm.Value) error { - // We know that hashmapIsBinaryKey is true, so we only have to handle those types that can show up there. - // To zero all undefined bytes, we iterate over all the fields in the type. For each element, compute the - // offset of that element. If it's Basic type, there are no internal padding bytes. For compound types, we recurse to ensure - // we handle nested types. Next, we determine if there are any padding bytes before the next - // element and zero those as well. - - zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) +// hashmapKeyHashSignature returns the Go type signature for hashmap key hash +// functions: func(key unsafe.Pointer, size, seed uintptr) uint32 +func hashmapKeyHashSignature() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple( + types.NewVar(token.NoPos, nil, "key", types.Typ[types.UnsafePointer]), + types.NewVar(token.NoPos, nil, "size", types.Typ[types.Uintptr]), + types.NewVar(token.NoPos, nil, "seed", types.Typ[types.Uintptr]), + ), + types.NewTuple( + types.NewVar(token.NoPos, nil, "", types.Typ[types.Uint32]), + ), + false, + ) +} - switch llvmType.TypeKind() { - case llvm.IntegerTypeKind: - // no padding bytes - return nil - case llvm.PointerTypeKind: - // mo padding bytes - return nil - case llvm.ArrayTypeKind: - llvmArrayType := llvmType - llvmElemType := llvmType.ElementType() +// hashmapKeyEqualSignature returns the Go type signature for hashmap key equal +// functions: func(x, y unsafe.Pointer, n uintptr) bool +func hashmapKeyEqualSignature() *types.Signature { + return types.NewSignatureType(nil, nil, nil, + types.NewTuple( + types.NewVar(token.NoPos, nil, "x", types.Typ[types.UnsafePointer]), + types.NewVar(token.NoPos, nil, "y", types.Typ[types.UnsafePointer]), + types.NewVar(token.NoPos, nil, "n", types.Typ[types.Uintptr]), + ), + types.NewTuple( + types.NewVar(token.NoPos, nil, "", types.Typ[types.Bool]), + ), + false, + ) +} - for i := 0; i < llvmArrayType.ArrayLength(); i++ { - idx := llvm.ConstInt(b.uintptrType, uint64(i), false) - elemPtr := b.CreateInBoundsGEP(llvmArrayType, ptr, []llvm.Value{zero, idx}, "") +// hashmapKeyFuncName returns a canonical name for a generated hash or equal +// function based on the key type's underlying structure. Named types are +// replaced with their underlying types so that structurally identical key +// types (e.g., struct{i1; str1} and struct{i2; str2} where both i1, i2 are +// int and str1, str2 are string) share the same generated function. +func hashmapKeyFuncName(prefix string, keyType types.Type) string { + return prefix + "." + hashmapCanonicalTypeName(keyType) +} - // zero any padding bytes in this element - b.zeroUndefBytes(llvmElemType, elemPtr) +// hashmapCanonicalTypeName returns a string representation of the hash/equal +// operations needed for a type, stripping named types where the operation does +// not depend on the name. Pointer and channel names do not include the element +// type because their hash/equal operations only use the pointer word. +func hashmapCanonicalTypeName(t types.Type) string { + switch t := t.Underlying().(type) { + case *types.Basic: + return t.Name() + case *types.Pointer: + return "*" + case *types.Chan: + switch t.Dir() { + case types.SendRecv: + return "chan" + case types.SendOnly: + return "chan<-" + case types.RecvOnly: + return "<-chan" } + case *types.Interface: + if t.NumMethods() == 0 { + return "interface{}" + } + return t.String() + case *types.Struct: + s := "struct{" + for i := 0; i < t.NumFields(); i++ { + if i > 0 { + s += "; " + } + s += hashmapCanonicalTypeName(t.Field(i).Type()) + } + return s + "}" + case *types.Array: + return fmt.Sprintf("[%d]%s", t.Len(), hashmapCanonicalTypeName(t.Elem())) + } + return t.String() +} - case llvm.StructTypeKind: - llvmStructType := llvmType - numFields := llvmStructType.StructElementTypesCount() - llvmElementTypes := llvmStructType.StructElementTypes() +// getOrGenerateKeyHashFunc returns an LLVM function that computes the hash +// of a key of the given type. The function is generated on first call and +// cached in the module. +func (b *builder) getOrGenerateKeyHashFunc(keyType types.Type) llvm.Value { + name := hashmapKeyFuncName("hashmapKeyHash", keyType) + if fn := b.mod.NamedFunction(name); !fn.IsNil() { + return fn + } - for i := 0; i < numFields; i++ { - idx := llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false) - elemPtr := b.CreateInBoundsGEP(llvmStructType, ptr, []llvm.Value{zero, idx}, "") + // Create the LLVM function type: + // (key ptr, size uintptr, seed uintptr, context ptr) -> i32 + fnType := llvm.FunctionType(b.ctx.Int32Type(), []llvm.Type{ + b.dataPtrType, b.uintptrType, b.uintptrType, b.dataPtrType, + }, false) + fn := llvm.AddFunction(b.mod, name, fnType) + fn.SetLinkage(llvm.LinkOnceODRLinkage) + fn.SetUnnamedAddr(true) + b.addStandardAttributes(fn) + + // Generate the function body. + savedBlock := b.GetInsertBlock() + defer b.SetInsertPointAtEnd(savedBlock) + + entry := b.ctx.AddBasicBlock(fn, "entry") + b.SetInsertPointAtEnd(entry) + + keyPtr := fn.Param(0) + seed := fn.Param(2) + llvmKeyType := b.getLLVMType(keyType) + hash := b.generateKeyHash(keyType, llvmKeyType, keyPtr, seed) + b.CreateRet(hash) - // zero any padding bytes in this field - llvmElemType := llvmElementTypes[i] - b.zeroUndefBytes(llvmElemType, elemPtr) + return fn +} - // zero any padding bytes before the next field, if any - offset := b.targetData.ElementOffset(llvmStructType, i) - storeSize := b.targetData.TypeStoreSize(llvmElemType) - fieldEndOffset := offset + storeSize +// getOrGenerateKeyEqualFunc returns an LLVM function that compares two keys +// of the given type for equality. The function is generated on first call +// and cached in the module. +func (b *builder) getOrGenerateKeyEqualFunc(keyType types.Type) llvm.Value { + name := hashmapKeyFuncName("hashmapKeyEqual", keyType) + if fn := b.mod.NamedFunction(name); !fn.IsNil() { + return fn + } - var nextOffset uint64 - if i < numFields-1 { - nextOffset = b.targetData.ElementOffset(llvmStructType, i+1) - } else { - // Last field? Next offset is the total size of the allocate struct. - nextOffset = b.targetData.TypeAllocSize(llvmStructType) - } + // Create the LLVM function type: + // (x ptr, y ptr, n uintptr, context ptr) -> i1 + fnType := llvm.FunctionType(b.ctx.Int1Type(), []llvm.Type{ + b.dataPtrType, b.dataPtrType, b.uintptrType, b.dataPtrType, + }, false) + fn := llvm.AddFunction(b.mod, name, fnType) + fn.SetLinkage(llvm.LinkOnceODRLinkage) + fn.SetUnnamedAddr(true) + b.addStandardAttributes(fn) + + // Generate the function body. + savedBlock := b.GetInsertBlock() + defer b.SetInsertPointAtEnd(savedBlock) + + entry := b.ctx.AddBasicBlock(fn, "entry") + b.SetInsertPointAtEnd(entry) + + xPtr := fn.Param(0) + yPtr := fn.Param(1) + llvmKeyType := b.getLLVMType(keyType) + result := b.generateKeyEqual(keyType, llvmKeyType, xPtr, yPtr, fn) + b.CreateRet(result) - if fieldEndOffset != nextOffset { - n := llvm.ConstInt(b.uintptrType, nextOffset-fieldEndOffset, false) - llvmStoreSize := llvm.ConstInt(b.uintptrType, storeSize, false) - paddingStart := b.CreateInBoundsGEP(b.ctx.Int8Type(), elemPtr, []llvm.Value{llvmStoreSize}, "") - b.createRuntimeCall("memzero", []llvm.Value{paddingStart, n}, "") + return fn +} + +// generateKeyHash generates IR that hashes a key value. Returns the i32 hash. +func (b *builder) generateKeyHash(keyType types.Type, llvmKeyType llvm.Type, keyPtr llvm.Value, seed llvm.Value) llvm.Value { + switch keyType := keyType.Underlying().(type) { + case *types.Basic: + if keyType.Info()&types.IsString != 0 { + // Hash the string contents. The size parameter is unused by + // hashmapStringPtrHash (it dereferences the string header to + // get the actual length), but we pass it for signature + // consistency with other hash functions. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("hashmapStringPtrHash", []llvm.Value{keyPtr, size, seed}, "hash") + } + if keyType.Info()&types.IsFloat != 0 { + // Float hash: normalizes -0 to +0 before hashing. + if keyType.Kind() == types.Float32 { + return b.createRuntimeCall("hashmapFloat32Hash", []llvm.Value{keyPtr, seed}, "hash") + } + return b.createRuntimeCall("hashmapFloat64Hash", []llvm.Value{keyPtr, seed}, "hash") + } + if keyType.Info()&types.IsComplex != 0 { + // Complex hash: hash real and imaginary parts as floats. + if keyType.Kind() == types.Complex64 { + realPtr := keyPtr + imagPtr := b.CreateInBoundsGEP(b.ctx.Int8Type(), keyPtr, []llvm.Value{ + llvm.ConstInt(b.uintptrType, 4, false), + }, "") + realHash := b.createRuntimeCall("hashmapFloat32Hash", []llvm.Value{realPtr, seed}, "hash.real") + imagHash := b.createRuntimeCall("hashmapFloat32Hash", []llvm.Value{imagPtr, seed}, "hash.imag") + return b.CreateXor(realHash, imagHash, "") } + realPtr := keyPtr + imagPtr := b.CreateInBoundsGEP(b.ctx.Int8Type(), keyPtr, []llvm.Value{ + llvm.ConstInt(b.uintptrType, 8, false), + }, "") + realHash := b.createRuntimeCall("hashmapFloat64Hash", []llvm.Value{realPtr, seed}, "hash.real") + imagHash := b.createRuntimeCall("hashmapFloat64Hash", []llvm.Value{imagPtr, seed}, "hash.imag") + return b.CreateXor(realHash, imagHash, "") } + // Integer/boolean: hash the raw bytes. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("hash32", []llvm.Value{keyPtr, size, seed}, "hash") + case *types.Pointer, *types.Chan: + // Pointers and channels: hash as raw pointer-sized bytes. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("hash32", []llvm.Value{keyPtr, size, seed}, "hash") + case *types.Interface: + // Interface: use runtime reflection-based hash. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("hashmapInterfacePtrHash", []llvm.Value{keyPtr, size, seed}, "hash") + case *types.Struct: + hash := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + for i := 0; i < keyType.NumFields(); i++ { + if keyType.Field(i).Name() == "_" { + continue // blank fields are ignored in Go equality + } + fieldType := keyType.Field(i).Type() + llvmFieldType := b.getLLVMType(fieldType) + if b.targetData.TypeAllocSize(llvmFieldType) == 0 { + continue // skip zero-sized fields + } + idx := llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false) + fieldPtr := b.CreateInBoundsGEP(llvmKeyType, keyPtr, []llvm.Value{zero, idx}, "") + fieldHash := b.generateKeyHash(fieldType, llvmFieldType, fieldPtr, seed) + hash = b.CreateXor(hash, fieldHash, "") + } + return hash + case *types.Array: + elemType := keyType.Elem() + llvmElemType := b.getLLVMType(elemType) + hash := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + for i := 0; i < int(keyType.Len()); i++ { + idx := llvm.ConstInt(b.uintptrType, uint64(i), false) + elemPtr := b.CreateInBoundsGEP(llvmKeyType, keyPtr, []llvm.Value{zero, idx}, "") + elemHash := b.generateKeyHash(elemType, llvmElemType, elemPtr, seed) + hash = b.CreateXor(hash, elemHash, "") + } + return hash + default: + panic(fmt.Sprintf("unhandled key type for hash generation: %T", keyType)) } +} - return nil +// generateKeyEqual generates IR that compares two key values for equality. +// Returns an i1 result. +func (b *builder) generateKeyEqual(keyType types.Type, llvmKeyType llvm.Type, xPtr, yPtr llvm.Value, fn llvm.Value) llvm.Value { + switch keyType := keyType.Underlying().(type) { + case *types.Basic: + if keyType.Info()&types.IsString != 0 { + // Compare strings: load both string headers and compare. + xStr := b.CreateLoad(llvmKeyType, xPtr, "x.str") + yStr := b.CreateLoad(llvmKeyType, yPtr, "y.str") + return b.createRuntimeCall("stringEqual", []llvm.Value{xStr, yStr}, "eq") + } + if keyType.Info()&types.IsFloat != 0 { + // Float equality: fcmp oeq handles -0==+0 (true) and NaN==NaN (false). + xVal := b.CreateLoad(llvmKeyType, xPtr, "x.float") + yVal := b.CreateLoad(llvmKeyType, yPtr, "y.float") + return b.CreateFCmp(llvm.FloatOEQ, xVal, yVal, "eq") + } + if keyType.Info()&types.IsComplex != 0 { + // Complex equality: both real and imaginary parts must be equal. + var floatType llvm.Type + if keyType.Kind() == types.Complex64 { + floatType = b.ctx.FloatType() + } else { + floatType = b.ctx.DoubleType() + } + floatSize := b.targetData.TypeAllocSize(floatType) + imagOffset := llvm.ConstInt(b.uintptrType, floatSize, false) + // Real parts + xReal := b.CreateLoad(floatType, xPtr, "x.real") + yReal := b.CreateLoad(floatType, yPtr, "y.real") + realEq := b.CreateFCmp(llvm.FloatOEQ, xReal, yReal, "eq.real") + // Imaginary parts + xImagPtr := b.CreateInBoundsGEP(b.ctx.Int8Type(), xPtr, []llvm.Value{imagOffset}, "") + yImagPtr := b.CreateInBoundsGEP(b.ctx.Int8Type(), yPtr, []llvm.Value{imagOffset}, "") + xImag := b.CreateLoad(floatType, xImagPtr, "x.imag") + yImag := b.CreateLoad(floatType, yImagPtr, "y.imag") + imagEq := b.CreateFCmp(llvm.FloatOEQ, xImag, yImag, "eq.imag") + return b.CreateAnd(realEq, imagEq, "") + } + // Integer/boolean: compare raw bytes. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("memequal", []llvm.Value{xPtr, yPtr, size}, "eq") + case *types.Pointer, *types.Chan: + // Pointers and channels: compare as raw pointer-sized bytes. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("memequal", []llvm.Value{xPtr, yPtr, size}, "eq") + case *types.Interface: + // Interface: use runtime interface equality. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("hashmapInterfaceEqual", []llvm.Value{xPtr, yPtr, size}, "eq") + case *types.Struct: + result := llvm.ConstInt(b.ctx.Int1Type(), 1, false) // start with true + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + for i := 0; i < keyType.NumFields(); i++ { + if keyType.Field(i).Name() == "_" { + continue // blank fields are ignored in Go equality + } + fieldType := keyType.Field(i).Type() + llvmFieldType := b.getLLVMType(fieldType) + if b.targetData.TypeAllocSize(llvmFieldType) == 0 { + continue // skip zero-sized fields + } + idx := llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false) + xFieldPtr := b.CreateInBoundsGEP(llvmKeyType, xPtr, []llvm.Value{zero, idx}, "") + yFieldPtr := b.CreateInBoundsGEP(llvmKeyType, yPtr, []llvm.Value{zero, idx}, "") + fieldEq := b.generateKeyEqual(fieldType, llvmFieldType, xFieldPtr, yFieldPtr, fn) + result = b.CreateAnd(result, fieldEq, "") + } + return result + case *types.Array: + elemType := keyType.Elem() + llvmElemType := b.getLLVMType(elemType) + result := llvm.ConstInt(b.ctx.Int1Type(), 1, false) + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + for i := 0; i < int(keyType.Len()); i++ { + idx := llvm.ConstInt(b.uintptrType, uint64(i), false) + xElemPtr := b.CreateInBoundsGEP(llvmKeyType, xPtr, []llvm.Value{zero, idx}, "") + yElemPtr := b.CreateInBoundsGEP(llvmKeyType, yPtr, []llvm.Value{zero, idx}, "") + elemEq := b.generateKeyEqual(elemType, llvmElemType, xElemPtr, yElemPtr, fn) + result = b.CreateAnd(result, elemEq, "") + } + return result + default: + panic(fmt.Sprintf("unhandled key type for equal generation: %T", keyType)) + } } diff --git a/compiler/symbol.go b/compiler/symbol.go index 4f24ddbfc3..16c5433cfa 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -190,6 +190,31 @@ func (c *compilerContext) getFunction(fn *ssa.Function) (llvm.Type, llvm.Value) case "runtime.stringFromRunes": llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) llvmFn.AddAttributeAtIndex(1, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("readonly"), 0)) + case "runtime.hashmapSet": + // The key (param 2) and value (param 3) pointers are only read via + // memcpy/hash/equal and are never captured. The indirect calls + // through m.keyHash and m.keyEqual function pointers prevent LLVM's + // functionattrs pass from inferring this automatically. + llvmFn.AddAttributeAtIndex(2, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + llvmFn.AddAttributeAtIndex(3, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + case "runtime.hashmapGet": + // The key (param 2) is read-only and never captured. + // The value (param 3) is written to (receives the result) but never captured. + llvmFn.AddAttributeAtIndex(2, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + llvmFn.AddAttributeAtIndex(3, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + case "runtime.hashmapDelete": + // The key (param 2) is read-only and never captured. + llvmFn.AddAttributeAtIndex(2, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + case "runtime.hashmapGenericSet": + // Same as hashmapBinarySet: key (param 2) and value (param 3) are + // not captured. + llvmFn.AddAttributeAtIndex(2, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + llvmFn.AddAttributeAtIndex(3, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + case "runtime.hashmapGenericGet": + llvmFn.AddAttributeAtIndex(2, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + llvmFn.AddAttributeAtIndex(3, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) + case "runtime.hashmapGenericDelete": + llvmFn.AddAttributeAtIndex(2, c.ctx.CreateEnumAttribute(llvm.AttributeKindID("nocapture"), 0)) case "runtime.trackPointer": // This function is necessary for tracking pointers on the stack in a // portable way (see gc_stack_portable.go). Indicate to the optimizer diff --git a/compiler/testdata/zeromap.ll b/compiler/testdata/zeromap.ll index 058c14fb32..82c0ee0995 100644 --- a/compiler/testdata/zeromap.ll +++ b/compiler/testdata/zeromap.ll @@ -27,23 +27,17 @@ entry: call void @llvm.lifetime.start.p0(i64 4, ptr nonnull %hashmap.value) call void @llvm.lifetime.start.p0(i64 12, ptr nonnull %hashmap.key) store %main.hasPadding %2, ptr %hashmap.key, align 4 - %3 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 1 - call void @runtime.memzero(ptr nonnull %3, i32 3, ptr undef) #5 - %4 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 9 - call void @runtime.memzero(ptr nonnull %4, i32 3, ptr undef) #5 - %5 = call i1 @runtime.hashmapBinaryGet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, i32 4, ptr undef) #5 + %3 = call i1 @runtime.hashmapGenericGet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, i32 4, ptr undef) #5 call void @llvm.lifetime.end.p0(i64 12, ptr nonnull %hashmap.key) - %6 = load i32, ptr %hashmap.value, align 4 + %4 = load i32, ptr %hashmap.value, align 4 call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %hashmap.value) - ret i32 %6 + ret i32 %4 } ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) declare void @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #4 -declare void @runtime.memzero(ptr, i32, ptr) #1 - -declare i1 @runtime.hashmapBinaryGet(ptr dereferenceable_or_null(40), ptr, ptr, i32, ptr) #1 +declare i1 @runtime.hashmapGenericGet(ptr dereferenceable_or_null(40), ptr nocapture, ptr nocapture, i32, ptr) #1 ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #4 @@ -60,17 +54,13 @@ entry: store i32 5, ptr %hashmap.value, align 4 call void @llvm.lifetime.start.p0(i64 12, ptr nonnull %hashmap.key) store %main.hasPadding %2, ptr %hashmap.key, align 4 - %3 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 1 - call void @runtime.memzero(ptr nonnull %3, i32 3, ptr undef) #5 - %4 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 9 - call void @runtime.memzero(ptr nonnull %4, i32 3, ptr undef) #5 - call void @runtime.hashmapBinarySet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, ptr undef) #5 + call void @runtime.hashmapGenericSet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, ptr undef) #5 call void @llvm.lifetime.end.p0(i64 12, ptr nonnull %hashmap.key) call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %hashmap.value) ret void } -declare void @runtime.hashmapBinarySet(ptr dereferenceable_or_null(40), ptr, ptr, ptr) #1 +declare void @runtime.hashmapGenericSet(ptr dereferenceable_or_null(40), ptr nocapture, ptr nocapture, ptr) #1 ; Function Attrs: noinline nounwind define hidden i32 @main.testZeroArrayGet(ptr dereferenceable_or_null(40) %m, [2 x %main.hasPadding] %s, ptr %context) unnamed_addr #3 { @@ -84,19 +74,11 @@ entry: %hashmap.key.repack1 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 12 %s.elt2 = extractvalue [2 x %main.hasPadding] %s, 1 store %main.hasPadding %s.elt2, ptr %hashmap.key.repack1, align 4 - %0 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 1 - call void @runtime.memzero(ptr nonnull %0, i32 3, ptr undef) #5 - %1 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 9 - call void @runtime.memzero(ptr nonnull %1, i32 3, ptr undef) #5 - %2 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 13 - call void @runtime.memzero(ptr nonnull %2, i32 3, ptr undef) #5 - %3 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 21 - call void @runtime.memzero(ptr nonnull %3, i32 3, ptr undef) #5 - %4 = call i1 @runtime.hashmapBinaryGet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, i32 4, ptr undef) #5 + %0 = call i1 @runtime.hashmapGenericGet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, i32 4, ptr undef) #5 call void @llvm.lifetime.end.p0(i64 24, ptr nonnull %hashmap.key) - %5 = load i32, ptr %hashmap.value, align 4 + %1 = load i32, ptr %hashmap.value, align 4 call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %hashmap.value) - ret i32 %5 + ret i32 %1 } ; Function Attrs: noinline nounwind @@ -112,15 +94,7 @@ entry: %hashmap.key.repack1 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 12 %s.elt2 = extractvalue [2 x %main.hasPadding] %s, 1 store %main.hasPadding %s.elt2, ptr %hashmap.key.repack1, align 4 - %0 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 1 - call void @runtime.memzero(ptr nonnull %0, i32 3, ptr undef) #5 - %1 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 9 - call void @runtime.memzero(ptr nonnull %1, i32 3, ptr undef) #5 - %2 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 13 - call void @runtime.memzero(ptr nonnull %2, i32 3, ptr undef) #5 - %3 = getelementptr inbounds nuw i8, ptr %hashmap.key, i32 21 - call void @runtime.memzero(ptr nonnull %3, i32 3, ptr undef) #5 - call void @runtime.hashmapBinarySet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, ptr undef) #5 + call void @runtime.hashmapGenericSet(ptr %m, ptr nonnull %hashmap.key, ptr nonnull %hashmap.value, ptr undef) #5 call void @llvm.lifetime.end.p0(i64 24, ptr nonnull %hashmap.key) call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %hashmap.value) ret void diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index 3c2af94f72..77983ae17a 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -1082,36 +1082,107 @@ func (v Value) MapKeys() []Value { keys := make([]Value, 0, v.Len()) it := hashmapNewIterator() - k := New(v.typecode.Key()) e := New(v.typecode.Elem()) + // Keys are stored as interface{} only for types that still use the + // legacy interface path (e.g., unsafe.Pointer). For those, we need + // to allocate an interface-sized buffer for hashmapNext (which writes + // m.keySize bytes), then unpack the interface to get the actual value. keyType := v.typecode.key() - keyTypeIsEmptyInterface := keyType.Kind() == Interface && keyType.NumMethod() == 0 - shouldUnpackInterface := !keyTypeIsEmptyInterface && keyType.Kind() != String && !keyType.isBinary() + shouldUnpackInterface := keyType.Kind() != String && !keyType.isBinary() && !hashmapKeyUsesGenericPath(keyType) + k := newMapKeyAlloc(keyType, shouldUnpackInterface) for hashmapNext(v.pointer(), it, k.value, e.value) { if shouldUnpackInterface { intf := *(*interface{})(k.value) - v := ValueOf(intf) - keys = append(keys, v) + keys = append(keys, ValueOf(intf)) } else { keys = append(keys, k.Elem()) } - k = New(v.typecode.Key()) + k = newMapKeyAlloc(keyType, shouldUnpackInterface) } return keys } +// newMapKeyAlloc allocates a Value suitable for receiving a key from +// hashmapNext. When interfaceStored is true, the map stores keys as +// interface{} (which may be larger than the declared key type), so an +// interface-sized buffer is allocated to avoid overflow. +func newMapKeyAlloc(keyType *RawType, interfaceStored bool) Value { + size := keyType.Size() + if interfaceStored { + var itf interface{} + size = unsafe.Sizeof(itf) + } + return Value{ + typecode: pointerTo(keyType), + value: alloc(size, nil), + flags: valueFlagExported, + } +} + //go:linkname hashmapStringGet runtime.hashmapStringGet func hashmapStringGet(m unsafe.Pointer, key string, value unsafe.Pointer, valueSize uintptr) bool //go:linkname hashmapBinaryGet runtime.hashmapBinaryGet func hashmapBinaryGet(m unsafe.Pointer, key, value unsafe.Pointer, valueSize uintptr) bool +//go:linkname hashmapGenericGet runtime.hashmapGenericGet +func hashmapGenericGet(m unsafe.Pointer, key, value unsafe.Pointer, valueSize uintptr) bool + //go:linkname hashmapInterfaceGet runtime.hashmapInterfaceGet func hashmapInterfaceGet(m unsafe.Pointer, key interface{}, value unsafe.Pointer, valueSize uintptr) bool +// hashmapKeyUsesGenericPath reports whether the given map key type uses the +// compiler-generated hash/equal path (storing keys at their actual type) as +// opposed to the legacy interface path (storing keys as interface{}). +// This must match the compiler's hashmapCanGenerateHashEqual predicate. +func hashmapKeyUsesGenericPath(t *RawType) bool { + switch t.Kind() { + case Bool, Int, Int8, Int16, Int32, Int64, + Uint, Uint8, Uint16, Uint32, Uint64, Uintptr, + Float32, Float64, Complex64, Complex128, + String, Chan, Ptr, Interface: + return true + case Array: + return hashmapKeyUsesGenericPath(t.Elem().(*RawType)) + case Struct: + for i := 0; i < t.NumField(); i++ { + if !hashmapKeyUsesGenericPath(t.Field(i).Type.(*RawType)) { + return false + } + } + return true + default: + return false + } +} + +// genericKeyPtr returns a pointer to key data suitable for passing to the +// hashmapGeneric* functions. When the map's key type is an interface, +// special handling is needed: if the key Value already holds an interface +// (e.g. from MapKeys iteration), its memory already contains the +// {typecode, data} pair the hashmap expects, so we use it directly. +// If the key is a concrete type being assigned to an interface-keyed map, +// we compose the interface first. +func genericKeyPtr(vkey *RawType, key Value) unsafe.Pointer { + if vkey.Kind() == Interface { + if key.Kind() == Interface { + // Key is already an interface value stored indirectly; + // key.value points to {typecode, data}. + return key.value + } + // Concrete value being used as an interface key. + intf := composeInterface(unsafe.Pointer(key.typecode), key.value) + return unsafe.Pointer(&intf) + } + if key.isIndirect() || key.typecode.Size() > unsafe.Sizeof(uintptr(0)) { + return key.value + } + return unsafe.Pointer(&key.value) +} + func (v Value) MapIndex(key Value) Value { if v.Kind() != Map { panic(&ValueError{Method: "MapIndex", Kind: v.Kind()}) @@ -1145,7 +1216,17 @@ func (v Value) MapIndex(key Value) Value { return Value{} } return elem.Elem() + } else if hashmapKeyUsesGenericPath(vkey) { + // Compiler-generated hash/equal path: keys are stored at their + // actual type. Use hashmapGenericGet which dispatches through the + // map's own keyHash/keyEqual function pointers. + keyptr := genericKeyPtr(vkey, key) + if ok := hashmapGenericGet(v.pointer(), keyptr, elem.value, elemType.Size()); !ok { + return Value{} + } + return elem.Elem() } else { + // Legacy interface path: keys are stored as interface{}. if ok := hashmapInterfaceGet(v.pointer(), key.Interface(), elem.value, elemType.Size()); !ok { return Value{} } @@ -1206,7 +1287,7 @@ func (v Value) SetIterValue(iter *MapIter) { } func (it *MapIter) Next() bool { - it.key = New(it.m.typecode.Key()) + it.key = newMapKeyAlloc(it.m.typecode.key(), it.unpackKeyInterface) it.val = New(it.m.typecode.Elem()) it.valid = hashmapNext(it.m.pointer(), it.it, it.key.value, it.val.value) @@ -1218,10 +1299,10 @@ func (iter *MapIter) Reset(v Value) { panic(&ValueError{Method: "MapRange", Kind: v.Kind()}) } + // Keys are stored as interface{} only for types that still use the + // legacy interface path. keyType := v.typecode.key() - - keyTypeIsEmptyInterface := keyType.Kind() == Interface && keyType.NumMethod() == 0 - shouldUnpackInterface := !keyTypeIsEmptyInterface && keyType.Kind() != String && !keyType.isBinary() + shouldUnpackInterface := keyType.Kind() != String && !keyType.isBinary() && !hashmapKeyUsesGenericPath(keyType) *iter = MapIter{ m: v, @@ -1969,6 +2050,9 @@ func hashmapStringSet(m unsafe.Pointer, key string, value unsafe.Pointer) //go:linkname hashmapBinarySet runtime.hashmapBinarySet func hashmapBinarySet(m unsafe.Pointer, key, value unsafe.Pointer) +//go:linkname hashmapGenericSet runtime.hashmapGenericSet +func hashmapGenericSet(m unsafe.Pointer, key, value unsafe.Pointer) + //go:linkname hashmapInterfaceSet runtime.hashmapInterfaceSet func hashmapInterfaceSet(m unsafe.Pointer, key interface{}, value unsafe.Pointer) @@ -1978,6 +2062,9 @@ func hashmapStringDelete(m unsafe.Pointer, key string) //go:linkname hashmapBinaryDelete runtime.hashmapBinaryDelete func hashmapBinaryDelete(m unsafe.Pointer, key unsafe.Pointer) +//go:linkname hashmapGenericDelete runtime.hashmapGenericDelete +func hashmapGenericDelete(m unsafe.Pointer, key unsafe.Pointer) + //go:linkname hashmapInterfaceDelete runtime.hashmapInterfaceDelete func hashmapInterfaceDelete(m unsafe.Pointer, key interface{}) @@ -2042,7 +2129,24 @@ func (v Value) SetMapIndex(key, elem Value) { } hashmapBinarySet(v.pointer(), keyptr, elemptr) } + } else if hashmapKeyUsesGenericPath(vkey) { + // Compiler-generated hash/equal path. + keyptr := genericKeyPtr(vkey, key) + + if del { + hashmapGenericDelete(v.pointer(), keyptr) + } else { + var elemptr unsafe.Pointer + if elem.isIndirect() || elem.typecode.Size() > unsafe.Sizeof(uintptr(0)) { + elemptr = elem.value + } else { + elemptr = unsafe.Pointer(&elem.value) + } + + hashmapGenericSet(v.pointer(), keyptr, elemptr) + } } else { + // Legacy interface path. if del { hashmapInterfaceDelete(v.pointer(), key.Interface()) } else { diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index 894d92a1ba..f316185eb1 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -34,6 +34,12 @@ type hashmapBucket struct { // 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 + type hashmapIterator struct { buckets unsafe.Pointer // pointer to array of hashapBuckets numBuckets uintptr // length of buckets array @@ -66,7 +72,7 @@ func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) *hashm bucketBits++ } - bucketBufSize := unsafe.Sizeof(hashmapBucket{}) + keySize*8 + valueSize*8 + bucketBufSize := hashmapBucketHeaderSize + keySize*8 + valueSize*8 buckets := alloc(bucketBufSize*(1< 1 { + pm := make(map[paddedKey]int) + var pk1, pk2 paddedKey + pk1.A = 1; pk1.B = 42 + pk2.A = 1; pk2.B = 42 + // Poison pk2's padding byte (between A and B). + *(*byte)(unsafe.Add(unsafe.Pointer(&pk2), 1)) = 0xFF + pm[pk1] = 100 + println("padded key lookup:", pm[pk2]) // 100 + println("padded key equal:", pk1 == pk2) // true + } else { + // No padding on this platform; print expected output. + println("padded key lookup:", 100) + println("padded key equal:", true) + } + + // Struct keys with blank fields: blank fields are ignored in equality. + type blankKey struct { + _ int + X string + } + bm := make(map[blankKey]int) + var bk1, bk2 blankKey + bk1.X = "hello" + bk2.X = "hello" + *(*int)(unsafe.Pointer(&bk2)) = 999 + bm[bk1] = 200 + println("blank key lookup:", bm[bk2]) // 200 + println("blank key equal:", bk1 == bk2) // true +} + +// Test for issue #3794: reflect MapIter.Key() should return a value with +// interface kind for map[interface{}] keys, not the underlying concrete kind. +func reflectMapIterfaceKey() { + m := make(map[interface{}]int) + m[1] = 2 + m["hello"] = 3 + rv := reflect.ValueOf(m) + iter := rv.MapRange() + for iter.Next() { + k := iter.Key() + if k.Kind() != reflect.Interface { + println("FAIL #3794: expected interface kind, got", k.Kind().String()) + return + } + } + println("reflect map interface key ok") +} diff --git a/testdata/map.txt b/testdata/map.txt index d5e553b1a7..0f541bd3a2 100644 --- a/testdata/map.txt +++ b/testdata/map.txt @@ -80,4 +80,9 @@ tested growing of a map 2 2 done +reflect map interface key ok no interface lookup failures +padded key lookup: 100 +padded key equal: true +blank key lookup: 200 +blank key equal: true diff --git a/tests/mapbench/mapbench_test.go b/tests/mapbench/mapbench_test.go new file mode 100644 index 0000000000..bb92273b04 --- /dev/null +++ b/tests/mapbench/mapbench_test.go @@ -0,0 +1,68 @@ +package mapbench + +import "testing" + +type compositeKey struct { + S string + N int32 +} + +var intSink int + +func BenchmarkMapStringShortGet(b *testing.B) { + m := make(map[string]int, 100) + for i := 0; i < 100; i++ { + m[string(rune('A'+i%26))+string(rune('a'+i/26))] = i + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + intSink += m["Qa"] + } +} + +func BenchmarkMapStringLongGet(b *testing.B) { + m := make(map[string]int, 100) + for i := 0; i < 100; i++ { + s := "this-is-a-longer-key-for-testing-" + for j := 0; j < 3; j++ { + s += string(rune('A' + (i+j)%26)) + } + m[s] = i + } + key := "this-is-a-longer-key-for-testing-ABC" + b.ResetTimer() + for i := 0; i < b.N; i++ { + intSink += m[key] + } +} + +func BenchmarkMapCompositeGet(b *testing.B) { + m := make(map[compositeKey]int, 100) + for i := 0; i < 100; i++ { + m[compositeKey{S: string(rune('A'+i%26)) + string(rune('a'+i/26)), N: int32(i)}] = i + } + key := compositeKey{S: "Qa", N: 42} + b.ResetTimer() + for i := 0; i < b.N; i++ { + intSink += m[key] + } +} + +func BenchmarkMapIntGet(b *testing.B) { + m := make(map[int]int, 100) + for i := 0; i < 100; i++ { + m[i*7] = i + } + b.ResetTimer() + for i := 0; i < b.N; i++ { + intSink += m[42] + } +} + +func BenchmarkMapCompositeSet(b *testing.B) { + m := make(map[compositeKey]int, b.N) + b.ResetTimer() + for i := 0; i < b.N; i++ { + m[compositeKey{S: "key", N: int32(i)}] = i + } +} From eca5b548ccbdf80720913f70a950eedf295cec4d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 17 May 2026 12:04:32 -0700 Subject: [PATCH 2/6] compiler: generate loops for array map key hash/equal Previously, array key hash and equal functions were unrolled at compile time, generating one block of IR per element. For large arrays like [1000]int inside a struct with non-binary fields, this caused code explosion. Now, binary-element arrays dispatch directly to hash32/memequal for the whole array. Non-binary-element arrays generate an LLVM IR loop. The equal loop short-circuits on the first mismatch. Small arrays are still unrolled instead of looping, keeping the simple cases compact. --- compiler/map.go | 112 ++++++++++++++++++++++++++++++++++++++++------- testdata/map.go | 15 +++++++ testdata/map.txt | 1 + 3 files changed, 111 insertions(+), 17 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 1e8b5c4f7d..584e153c47 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -12,6 +12,8 @@ import ( "tinygo.org/x/go-llvm" ) +const hashArrayUnrollLimit = 4 + // 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) { @@ -533,15 +535,54 @@ func (b *builder) generateKeyHash(keyType types.Type, llvmKeyType llvm.Type, key case *types.Array: elemType := keyType.Elem() llvmElemType := b.getLLVMType(elemType) - hash := llvm.ConstInt(b.ctx.Int32Type(), 0, false) - zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) - for i := 0; i < int(keyType.Len()); i++ { - idx := llvm.ConstInt(b.uintptrType, uint64(i), false) - elemPtr := b.CreateInBoundsGEP(llvmKeyType, keyPtr, []llvm.Value{zero, idx}, "") - elemHash := b.generateKeyHash(elemType, llvmElemType, elemPtr, seed) - hash = b.CreateXor(hash, elemHash, "") + arrayLen := keyType.Len() + if hashmapIsBinaryKey(elemType) { + // All elements are binary-comparable; hash the entire array as raw bytes. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("hash32", []llvm.Value{keyPtr, size, seed}, "hash") } - return hash + if arrayLen == 0 { + return llvm.ConstInt(b.ctx.Int32Type(), 0, false) + } + if arrayLen <= hashArrayUnrollLimit { + hash := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + for i := 0; i < int(arrayLen); i++ { + idx := llvm.ConstInt(b.uintptrType, uint64(i), false) + elemPtr := b.CreateInBoundsGEP(llvmKeyType, keyPtr, []llvm.Value{zero, idx}, "") + elemHash := b.generateKeyHash(elemType, llvmElemType, elemPtr, seed) + hash = b.CreateXor(hash, elemHash, "") + } + return hash + } + initHash := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + + loopEntry := b.GetInsertBlock() + loopBody := b.ctx.AddBasicBlock(loopEntry.Parent(), "hash.array.body") + loopDone := b.ctx.AddBasicBlock(loopEntry.Parent(), "hash.array.done") + + b.CreateBr(loopBody) + b.SetInsertPointAtEnd(loopBody) + + phiI := b.CreatePHI(b.uintptrType, "i") + phiHash := b.CreatePHI(b.ctx.Int32Type(), "hash.acc") + + elemPtr := b.CreateInBoundsGEP(llvmKeyType, keyPtr, []llvm.Value{zero, phiI}, "") + elemHash := b.generateKeyHash(elemType, llvmElemType, elemPtr, seed) + newHash := b.CreateXor(phiHash, elemHash, "") + nextI := b.CreateAdd(phiI, llvm.ConstInt(b.uintptrType, 1, false), "") + cond := b.CreateICmp(llvm.IntULT, nextI, llvm.ConstInt(b.uintptrType, uint64(arrayLen), false), "") + b.CreateCondBr(cond, loopBody, loopDone) + + bodyEnd := b.GetInsertBlock() + phiI.AddIncoming([]llvm.Value{llvm.ConstInt(b.uintptrType, 0, false), nextI}, + []llvm.BasicBlock{loopEntry, bodyEnd}) + phiHash.AddIncoming([]llvm.Value{initHash, newHash}, + []llvm.BasicBlock{loopEntry, bodyEnd}) + + b.SetInsertPointAtEnd(loopDone) + return newHash default: panic(fmt.Sprintf("unhandled key type for hash generation: %T", keyType)) } @@ -619,16 +660,53 @@ func (b *builder) generateKeyEqual(keyType types.Type, llvmKeyType llvm.Type, xP case *types.Array: elemType := keyType.Elem() llvmElemType := b.getLLVMType(elemType) - result := llvm.ConstInt(b.ctx.Int1Type(), 1, false) - zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) - for i := 0; i < int(keyType.Len()); i++ { - idx := llvm.ConstInt(b.uintptrType, uint64(i), false) - xElemPtr := b.CreateInBoundsGEP(llvmKeyType, xPtr, []llvm.Value{zero, idx}, "") - yElemPtr := b.CreateInBoundsGEP(llvmKeyType, yPtr, []llvm.Value{zero, idx}, "") - elemEq := b.generateKeyEqual(elemType, llvmElemType, xElemPtr, yElemPtr, fn) - result = b.CreateAnd(result, elemEq, "") + arrayLen := keyType.Len() + if hashmapIsBinaryKey(elemType) { + // All elements are binary-comparable; compare the entire array. + size := llvm.ConstInt(b.uintptrType, b.targetData.TypeAllocSize(llvmKeyType), false) + return b.createRuntimeCall("memequal", []llvm.Value{xPtr, yPtr, size}, "eq") } - return result + if arrayLen == 0 { + return llvm.ConstInt(b.ctx.Int1Type(), 1, false) + } + if arrayLen <= hashArrayUnrollLimit { + result := llvm.ConstInt(b.ctx.Int1Type(), 1, false) + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + for i := 0; i < int(arrayLen); i++ { + idx := llvm.ConstInt(b.uintptrType, uint64(i), false) + xElemPtr := b.CreateInBoundsGEP(llvmKeyType, xPtr, []llvm.Value{zero, idx}, "") + yElemPtr := b.CreateInBoundsGEP(llvmKeyType, yPtr, []llvm.Value{zero, idx}, "") + elemEq := b.generateKeyEqual(elemType, llvmElemType, xElemPtr, yElemPtr, fn) + result = b.CreateAnd(result, elemEq, "") + } + return result + } + zero := llvm.ConstInt(b.ctx.Int32Type(), 0, false) + + loopEntry := b.GetInsertBlock() + loopBody := b.ctx.AddBasicBlock(loopEntry.Parent(), "eq.array.body") + loopDone := b.ctx.AddBasicBlock(loopEntry.Parent(), "eq.array.done") + + b.CreateBr(loopBody) + b.SetInsertPointAtEnd(loopBody) + + phiI := b.CreatePHI(b.uintptrType, "i") + + xElemPtr := b.CreateInBoundsGEP(llvmKeyType, xPtr, []llvm.Value{zero, phiI}, "") + yElemPtr := b.CreateInBoundsGEP(llvmKeyType, yPtr, []llvm.Value{zero, phiI}, "") + elemEq := b.generateKeyEqual(elemType, llvmElemType, xElemPtr, yElemPtr, fn) + + nextI := b.CreateAdd(phiI, llvm.ConstInt(b.uintptrType, 1, false), "") + atEnd := b.CreateICmp(llvm.IntUGE, nextI, llvm.ConstInt(b.uintptrType, uint64(arrayLen), false), "") + exitLoop := b.CreateOr(atEnd, b.CreateNot(elemEq, ""), "") + b.CreateCondBr(exitLoop, loopDone, loopBody) + + bodyEnd := b.GetInsertBlock() + phiI.AddIncoming([]llvm.Value{llvm.ConstInt(b.uintptrType, 0, false), nextI}, + []llvm.BasicBlock{loopEntry, bodyEnd}) + + b.SetInsertPointAtEnd(loopDone) + return elemEq default: panic(fmt.Sprintf("unhandled key type for equal generation: %T", keyType)) } diff --git a/testdata/map.go b/testdata/map.go index 4c8482d9a2..9567714679 100644 --- a/testdata/map.go +++ b/testdata/map.go @@ -131,6 +131,8 @@ func main() { mapgrow() + nestedarraymaps() + reflectMapIterfaceKey() interfacerehash() @@ -314,6 +316,19 @@ func interfacerehash() { } } +func nestedarraymaps() { + type nestedArrayElem struct { + x uint8 + } + type nestedArrayKey struct { + a [5][5]nestedArrayElem + } + var k nestedArrayKey + k.a[4][4].x = 7 + m := map[nestedArrayKey]int{k: 42} + println("nested array key:", m[k]) +} + func paddingBlankMaps() { // Struct keys with padding: the hash/equal must operate per-field // and not include padding bytes. Only test when padding actually diff --git a/testdata/map.txt b/testdata/map.txt index 0f541bd3a2..27c341a819 100644 --- a/testdata/map.txt +++ b/testdata/map.txt @@ -80,6 +80,7 @@ tested growing of a map 2 2 done +nested array key: 42 reflect map interface key ok no interface lookup failures padded key lookup: 100 From c2b923bdd7c44b8a033e2ed13a895283dff4202e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 17 May 2026 12:06:03 -0700 Subject: [PATCH 3/6] reflect: fix at-runtime map issues from review, and more found locally Maps created through reflect.MakeMap need hash/equal behavior that matches compiler-created maps. Add hashmapMakeReflect for composite key types, using runtime closures that reconstruct interface{} values from raw key bytes and delegate to the interface hash and equality paths. Interface-keyed maps are already stored as interface values, so use the existing interface hash/equal helpers directly for those. This keeps reflect insert, lookup, delete, and compiled lookup paths consistent. Also fix addressable small values used as interface map keys or interface map values. loadSmallValue puts small indirect values back in the pointer-sized interface data field the same way valueInterfaceUnsafe does. --- src/internal/reflectlite/value.go | 55 +++++++++----- src/runtime/hashmap.go | 35 +++++++++ testdata/reflect.go | 122 ++++++++++++++++++++++++++++++ testdata/reflect.txt | 18 +++++ 4 files changed, 213 insertions(+), 17 deletions(-) diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index 77983ae17a..8cde753ba2 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -146,6 +146,16 @@ func TypeAssert[T any](v Value) (T, bool) { // valueInterfaceUnsafe is used by the runtime to hash map keys. It should not // be subject to the isExported check. +// loadSmallValue loads a value of size <= sizeof(uintptr) from ptr into +// a pointer-sized value suitable for storing in an interface's data field. +func loadSmallValue(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + var value uintptr + for j := size; j != 0; j-- { + value = (value << 8) | uintptr(*(*uint8)(unsafe.Add(ptr, j-1))) + } + return unsafe.Pointer(value) +} + func valueInterfaceUnsafe(v Value) interface{} { if v.typecode.Kind() == Interface { // The value itself is an interface. This can happen when getting the @@ -158,11 +168,7 @@ func valueInterfaceUnsafe(v Value) interface{} { if v.isIndirect() && v.typecode.Size() <= unsafe.Sizeof(uintptr(0)) { // Value was indirect but must be put back directly in the interface // value. - var value uintptr - for j := v.typecode.Size(); j != 0; j-- { - value = (value << 8) | uintptr(*(*uint8)(unsafe.Add(v.value, j-1))) - } - v.value = unsafe.Pointer(value) + v.value = loadSmallValue(v.value, v.typecode.Size()) } return composeInterface(unsafe.Pointer(v.typecode), v.value) } @@ -1174,7 +1180,15 @@ func genericKeyPtr(vkey *RawType, key Value) unsafe.Pointer { return key.value } // Concrete value being used as an interface key. - intf := composeInterface(unsafe.Pointer(key.typecode), key.value) + // For small addressable values, key.value is a pointer to + // the data, but the interface value field stores the data + // directly; load it using the same endian-safe approach as + // valueInterfaceUnsafe. + val := key.value + if key.isIndirect() && key.typecode.Size() <= unsafe.Sizeof(uintptr(0)) { + val = loadSmallValue(key.value, key.typecode.Size()) + } + intf := composeInterface(unsafe.Pointer(key.typecode), val) return unsafe.Pointer(&intf) } if key.isIndirect() || key.typecode.Size() > unsafe.Sizeof(uintptr(0)) { @@ -2089,15 +2103,19 @@ func (v Value) SetMapIndex(key, elem Value) { } // make elem an interface if it needs to be converted - if v.typecode.elem().Kind() == Interface && elem.typecode.Kind() != Interface { - intf := composeInterface(unsafe.Pointer(elem.typecode), elem.value) + if !del && v.typecode.elem().Kind() == Interface && elem.typecode.Kind() != Interface { + val := elem.value + if elem.isIndirect() && elem.typecode.Size() <= unsafe.Sizeof(uintptr(0)) { + val = loadSmallValue(elem.value, elem.typecode.Size()) + } + intf := composeInterface(unsafe.Pointer(elem.typecode), val) elem = Value{ typecode: v.typecode.elem(), value: unsafe.Pointer(&intf), } } - if key.Kind() == String { + if vkey.Kind() == String { if del { hashmapStringDelete(v.pointer(), *(*string)(key.value)) } else { @@ -2110,7 +2128,7 @@ func (v Value) SetMapIndex(key, elem Value) { hashmapStringSet(v.pointer(), *(*string)(key.value), elemptr) } - } else if key.typecode.isBinary() { + } else if vkey.isBinary() { var keyptr unsafe.Pointer if key.isIndirect() || key.typecode.Size() > unsafe.Sizeof(uintptr(0)) { keyptr = key.value @@ -2214,6 +2232,9 @@ func (v Value) FieldByNameFunc(match func(string) bool) Value { //go:linkname hashmapMake runtime.hashmapMake func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) unsafe.Pointer +//go:linkname hashmapMakeReflect runtime.hashmapMakeReflect +func hashmapMakeReflect(keySize, valueSize, sizeHint uintptr, keyType unsafe.Pointer) unsafe.Pointer + // MakeMapWithSize creates a new map with the specified type and initial space // for approximately n elements. func MakeMapWithSize(typ Type, n int) Value { @@ -2222,7 +2243,6 @@ func MakeMapWithSize(typ Type, n int) Value { const ( hashmapAlgorithmBinary uint8 = iota hashmapAlgorithmString - hashmapAlgorithmInterface ) if typ.Kind() != Map { @@ -2236,18 +2256,19 @@ func MakeMapWithSize(typ Type, n int) Value { key := typ.Key().(*RawType) val := typ.Elem().(*RawType) - var alg uint8 + var m unsafe.Pointer if key.Kind() == String { - alg = hashmapAlgorithmString + m = hashmapMake(key.Size(), val.Size(), uintptr(n), hashmapAlgorithmString) } else if key.isBinary() { - alg = hashmapAlgorithmBinary + m = hashmapMake(key.Size(), val.Size(), uintptr(n), hashmapAlgorithmBinary) } else { - alg = hashmapAlgorithmInterface + // Composite key type (struct with strings, floats, etc.). + // Use runtime-generated hash/equal closures that walk the + // type structure, matching the compiler-generated functions. + m = hashmapMakeReflect(key.Size(), val.Size(), uintptr(n), unsafe.Pointer(key)) } - m := hashmapMake(key.Size(), val.Size(), uintptr(n), alg) - return Value{ typecode: typ.(*RawType), value: m, diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index f316185eb1..8b32ea1d20 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -553,6 +553,41 @@ func hashmapMakeGeneric(keySize, valueSize uintptr, sizeHint uintptr, } } +// hashmapMakeReflect creates a hashmap for reflect.MakeMapWithSize using +// closures that reconstruct interface{} values from raw key bytes, +// delegating to hashmapInterfaceHash for hashing and == for equality. +func hashmapMakeReflect(keySize, valueSize, sizeHint uintptr, keyType unsafe.Pointer) *hashmap { + t := (*reflectlite.RawType)(keyType) + if t.Kind() == reflectlite.Interface { + // Interface keys are already stored as interface values in the + // bucket; use the existing interface hash/equal directly. + return hashmapMakeGeneric(keySize, valueSize, sizeHint, + hashmapInterfacePtrHash, hashmapInterfaceEqual) + } + keyHash := func(key unsafe.Pointer, size, seed uintptr) uint32 { + return hashmapInterfaceHash(rawToInterface(t, key), seed) + } + keyEqual := func(x, y unsafe.Pointer, n uintptr) bool { + return rawToInterface(t, x) == rawToInterface(t, y) + } + return hashmapMakeGeneric(keySize, valueSize, sizeHint, keyHash, keyEqual) +} + +// rawToInterface reconstructs an interface{} from raw bytes at ptr. +func rawToInterface(t *reflectlite.RawType, ptr unsafe.Pointer) interface{} { + var val unsafe.Pointer + if t.Size() <= unsafe.Sizeof(uintptr(0)) { + val = reflectliteLoadSmallValue(ptr, t.Size()) + } else { + val = ptr + } + i := composeInterface(unsafe.Pointer(t), val) + return *(*interface{})(unsafe.Pointer(&i)) +} + +//go:linkname reflectliteLoadSmallValue internal/reflectlite.loadSmallValue +func reflectliteLoadSmallValue(ptr unsafe.Pointer, size uintptr) unsafe.Pointer + // Hashmap with string keys (a common case). func hashmapStringEqual(x, y unsafe.Pointer, n uintptr) bool { diff --git a/testdata/reflect.go b/testdata/reflect.go index 873d60f787..6f8497c41d 100644 --- a/testdata/reflect.go +++ b/testdata/reflect.go @@ -755,6 +755,9 @@ func testImplements() { // Make FooNode and BarNode implement Node with pointer receivers // (can't add methods to local types in function, use a different approach) testValueSetInterface() + testMakeMapCompositeKey() + testMakeMapInterfaceKey() + testMakeMapPaddedKey() } type IfaceNode interface { @@ -807,3 +810,122 @@ func randuint32() uint32 { xorshift32State = xorshift32(xorshift32State) return xorshift32State } + +type compositeKey struct { + S string + N int32 +} + +// testMakeMapCompositeKey tests that reflect.MakeMap works correctly with +// composite key types (structs containing strings). This exercises the +// hash/equal dispatch path for maps created through reflection rather +// than by the compiler. +func testMakeMapCompositeKey() { + println("\nreflect.MakeMap composite key:") + mapType := reflect.TypeOf(map[compositeKey]int{}) + m := reflect.MakeMap(mapType) + + // Insert two keys that share the same string but differ in the int field. + key1 := reflect.ValueOf(compositeKey{S: "hello", N: 1}) + key2 := reflect.ValueOf(compositeKey{S: "hello", N: 2}) + m.SetMapIndex(key1, reflect.ValueOf(100)) + m.SetMapIndex(key2, reflect.ValueOf(200)) + + println("len:", m.Len()) + + v1 := m.MapIndex(key1) + if v1.IsValid() { + println("key1:", v1.Int()) + } else { + println("key1: not found") + } + v2 := m.MapIndex(key2) + if v2.IsValid() { + println("key2:", v2.Int()) + } else { + println("key2: not found") + } + + // Delete key1, verify key2 remains. + m.SetMapIndex(key1, reflect.Value{}) + println("after delete, len:", m.Len()) + v2 = m.MapIndex(key2) + if v2.IsValid() { + println("key2 after delete:", v2.Int()) + } else { + println("key2 after delete: not found") + } +} + +// testMakeMapInterfaceKey tests that reflect.MakeMap works correctly with +// interface{} key types, including cross-path usage (reflect insert, +// compiled lookup and vice versa). +func testMakeMapInterfaceKey() { + println("\nreflect.MakeMap interface key:") + mapType := reflect.TypeOf(map[interface{}]int{}) + rv := reflect.MakeMap(mapType) + + rv.SetMapIndex(reflect.ValueOf(42), reflect.ValueOf(100)) + rv.SetMapIndex(reflect.ValueOf("hello"), reflect.ValueOf(200)) + println("len:", rv.Len()) + + v1 := rv.MapIndex(reflect.ValueOf(42)) + if v1.IsValid() { + println("42:", v1.Int()) + } else { + println("42: not found") + } + v2 := rv.MapIndex(reflect.ValueOf("hello")) + if v2.IsValid() { + println("hello:", v2.Int()) + } else { + println("hello: not found") + } + + // Cross-path: use from compiled code. + m := rv.Interface().(map[interface{}]int) + println("compiled 42:", m[42]) + println("compiled hello:", m["hello"]) + + // Addressable small value as key. + x := 99 + addrVal := reflect.ValueOf(&x).Elem() + rv.SetMapIndex(addrVal, reflect.ValueOf(300)) + v3 := rv.MapIndex(reflect.ValueOf(99)) + if v3.IsValid() { + println("addressable 99:", v3.Int()) + } else { + println("addressable 99: not found") + } +} + +type paddedKey struct { + A int8 + B int32 +} + +// testMakeMapPaddedKey tests that struct keys with padding work correctly +// through reflect, using addressable values with poisoned padding bytes. +func testMakeMapPaddedKey() { + println("\nreflect.MakeMap padded key:") + var pk1, pk2 paddedKey + pk1.A = 1 + pk1.B = 42 + pk2.A = 1 + pk2.B = 42 + + if unsafe.Offsetof(paddedKey{}.B) > 1 { + // Poison pk2's padding byte (between A and B). + *(*byte)(unsafe.Add(unsafe.Pointer(&pk2), 1)) = 0xFF + } + + // Use addressable values so padding survives into reflect. + rm := reflect.MakeMap(reflect.TypeOf(map[paddedKey]int{})) + rm.SetMapIndex(reflect.ValueOf(&pk1).Elem(), reflect.ValueOf(100)) + v := rm.MapIndex(reflect.ValueOf(&pk2).Elem()) + if v.IsValid() { + println("padded lookup:", v.Int()) + } else { + println("padded lookup: not found") + } +} diff --git a/testdata/reflect.txt b/testdata/reflect.txt index 3024568c3d..fa2ef10849 100644 --- a/testdata/reflect.txt +++ b/testdata/reflect.txt @@ -503,6 +503,24 @@ value set interface: Set[0] to BarNode: 10 Set[1] still FooNode: 2 +reflect.MakeMap composite key: +len: 2 +key1: 100 +key2: 200 +after delete, len: 1 +key2 after delete: 200 + +reflect.MakeMap interface key: +len: 2 +42: 100 +hello: 200 +compiled 42: 100 +compiled hello: 200 +addressable 99: 300 + +reflect.MakeMap padded key: +padded lookup: 100 + alignment / offset: struct{[0]func(); byte}: true From f97770876ee5900b10ee8f3e0b7b289b76ae6a0a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 17 May 2026 12:07:34 -0700 Subject: [PATCH 4/6] compiler, interp, reflect: fix pointer map literals; remove interface fallback Package-level map literals with pointer keys (both *T and unsafe.Pointer) crash the compiler: the interp pass panics when trying to hash pointer data as raw bytes, because pointer values in the interp memory model are symbolic identities that do not fit in a byte. Fix this by setting a recoverable error flag instead of panicking. The interp detects the error after each instruction and defers the map insert to runtime init code, where real addresses are available for hashing. This matches how the interp already handles other operations it cannot evaluate at compile time. With this fix, unsafe.Pointer can also be classified as a binary map key, which was the last type requiring the interface-based fallback. Since all comparable types now use either the binary or the compiler-generated hash/equal path, remove the interface fallback from the compiler and reflect packages. --- compiler/map.go | 98 ++++--------------------- interp/interp.go | 1 + interp/interpreter.go | 8 +++ interp/memory.go | 16 +++-- src/internal/reflectlite/value.go | 116 +++--------------------------- src/runtime/hashmap.go | 29 -------- src/tinygo/runtime.go | 1 - testdata/map.go | 54 ++++++++------ testdata/map.txt | 9 ++- testdata/reflect.go | 18 +++++ testdata/reflect.txt | 1 + 11 files changed, 100 insertions(+), 251 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 584e153c47..75256e88a3 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -36,11 +36,13 @@ func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) { } } - if hashmapCanGenerateHashEqual(keyType) && !hashmapIsBinaryKey(keyType) { + var alg uint64 + if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { + alg = uint64(tinygo.HashmapAlgorithmString) + } else if hashmapIsBinaryKey(keyType) { + alg = uint64(tinygo.HashmapAlgorithmBinary) + } else { // Composite keys: use compiler-generated hash/equal functions. - // Binary and string keys use the more efficient dedicated paths - // (hashmapMake with algorithm enum) which avoid function pointer - // indirection. hashFn := b.getOrGenerateKeyHashFunc(keyType) equalFn := b.getOrGenerateKeyEqualFunc(keyType) hashFuncValue := b.createFuncValue(hashFn, llvm.ConstNull(b.dataPtrType), hashmapKeyHashSignature()) @@ -51,18 +53,6 @@ func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) { }, "") return hashmap, nil } - - var alg uint64 - if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { - alg = uint64(tinygo.HashmapAlgorithmString) - } else if hashmapIsBinaryKey(keyType) { - alg = uint64(tinygo.HashmapAlgorithmBinary) - } else { - // Fallback for types not handled by hashmapCanGenerateHashEqual - // (currently only unsafe.Pointer due to an interp issue). - llvmKeyType = b.getLLVMRuntimeType("_interface") - alg = uint64(tinygo.HashmapAlgorithmInterface) - } algEnum := llvm.ConstInt(b.ctx.Int8Type(), alg, false) hashmap := b.createRuntimeCall("hashmapMake", []llvm.Value{llvmKeySize, llvmValueSize, sizeHint, algEnum}, "") return hashmap, nil @@ -89,13 +79,12 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Val // Do the lookup. How it is done depends on the key type. var commaOkValue llvm.Value - origKeyType := keyType keyType = keyType.Underlying() if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { // key is a string params := []llvm.Value{m, key, mapValueAlloca, mapValueSize} commaOkValue = b.createRuntimeCall("hashmapStringGet", params, "") - } else if hashmapIsBinaryKey(keyType) || hashmapCanGenerateHashEqual(keyType) { + } else { // Key stored at actual type: either binary-comparable or with // compiler-generated hash/equal. mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") @@ -107,15 +96,6 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Val } commaOkValue = b.createRuntimeCall(fnName, params, "") b.emitLifetimeEnd(mapKeyAlloca, 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, origKeyType, pos) - } - params := []llvm.Value{m, itfKey, mapValueAlloca, mapValueSize} - commaOkValue = b.createRuntimeCall("hashmapInterfaceGet", params, "") } // Load the resulting value from the hashmap. The value is set to the zero @@ -138,13 +118,12 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Val func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, pos token.Pos) { valueAlloca, valueSize := b.createTemporaryAlloca(value.Type(), "hashmap.value") b.CreateStore(value, valueAlloca) - origKeyType := keyType keyType = keyType.Underlying() if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { // key is a string params := []llvm.Value{m, key, valueAlloca} b.createRuntimeCall("hashmapStringSet", params, "") - } else if hashmapIsBinaryKey(keyType) || hashmapCanGenerateHashEqual(keyType) { + } else { // Key stored at actual type. keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") b.CreateStore(key, keyAlloca) @@ -155,15 +134,6 @@ func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, params := []llvm.Value{m, keyAlloca, valueAlloca} b.createRuntimeCall(fnName, params, "") b.emitLifetimeEnd(keyAlloca, 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, origKeyType, pos) - } - params := []llvm.Value{m, itfKey, valueAlloca} - b.createRuntimeCall("hashmapInterfaceSet", params, "") } b.emitLifetimeEnd(valueAlloca, valueSize) } @@ -171,14 +141,13 @@ func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, // 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 { - origKeyType := keyType 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) || hashmapCanGenerateHashEqual(keyType) { + } else { // Key stored at actual type. keyAlloca, keySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") b.CreateStore(key, keyAlloca) @@ -190,17 +159,6 @@ func (b *builder) createMapDelete(keyType types.Type, m, key llvm.Value, pos tok b.createRuntimeCall(fnName, params, "") b.emitLifetimeEnd(keyAlloca, 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, origKeyType, pos) - } - params := []llvm.Value{m, itfKey} - b.createRuntimeCall("hashmapInterfaceDelete", params, "") - return nil } } @@ -220,38 +178,15 @@ func (b *builder) createMapIteratorNext(rangeVal ssa.Value, llvmRangeVal, it llv llvmKeyType := b.getLLVMType(keyType) llvmValueType := b.getLLVMType(valueType) - // Keys are stored as an interface value only for types not handled by - // the binary or generic paths (currently only unsafe.Pointer). - isKeyStoredAsInterface := false - if t, ok := keyType.Underlying().(*types.Basic); ok && t.Info()&types.IsString != 0 { - // key is a string - } else if hashmapIsBinaryKey(keyType) || hashmapCanGenerateHashEqual(keyType) { - // key stored at actual type - } else { - 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") - } + // All key types are now stored at their declared type (no interface wrapping). // Extract the key and value from the map. - mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(llvmStoredKeyType, "range.key") + mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(llvmKeyType, "range.key") mapValueAlloca, mapValueSize := b.createTemporaryAlloca(llvmValueType, "range.value") ok := b.createRuntimeCall("hashmapNext", []llvm.Value{llvmRangeVal, it, mapKeyAlloca, mapValueAlloca}, "range.next") - mapKey := b.CreateLoad(llvmStoredKeyType, mapKeyAlloca, "") + mapKey := b.CreateLoad(llvmKeyType, mapKeyAlloca, "") mapValue := b.CreateLoad(llvmValueType, 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(mapKeyAlloca, mapKeySize) b.emitLifetimeEnd(mapValueAlloca, mapValueSize) @@ -271,10 +206,7 @@ func (b *builder) createMapIteratorNext(rangeVal ssa.Value, llvmRangeVal, it llv func hashmapIsBinaryKey(keyType types.Type) bool { switch keyType := keyType.Underlying().(type) { case *types.Basic: - // TODO: unsafe.Pointer is also a binary key, but to support that we - // need to fix an issue with interp first (see - // https://github.com/tinygo-org/tinygo/pull/4898). - return keyType.Info()&(types.IsBoolean|types.IsInteger) != 0 + return keyType.Info()&(types.IsBoolean|types.IsInteger) != 0 || keyType.Kind() == types.UnsafePointer case *types.Pointer: return true case *types.Array: @@ -291,9 +223,7 @@ func hashmapIsBinaryKey(keyType types.Type) bool { func hashmapCanGenerateHashEqual(keyType types.Type) bool { switch keyType := keyType.Underlying().(type) { case *types.Basic: - // Note: unsafe.Pointer is excluded (not IsBoolean/IsInteger/etc.) - // due to a known interp issue (see hashmapIsBinaryKey). - return keyType.Info()&(types.IsBoolean|types.IsInteger|types.IsString|types.IsFloat|types.IsComplex) != 0 + return keyType.Info()&(types.IsBoolean|types.IsInteger|types.IsString|types.IsFloat|types.IsComplex) != 0 || keyType.Kind() == types.UnsafePointer case *types.Pointer: return true case *types.Chan: diff --git a/interp/interp.go b/interp/interp.go index 424c388a11..fa590ce7f9 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -36,6 +36,7 @@ type runner struct { timeout time.Duration maxLoopIterations int callsExecuted uint64 + interpErr error // set by Uint/Int when they encounter pointer data } func newRunner(mod llvm.Module, timeout time.Duration, maxLoopIterations int, debug bool) *runner { diff --git a/interp/interpreter.go b/interp/interpreter.go index 4b46df249e..34bb668da1 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -847,6 +847,14 @@ func (r *runner) run(fn *function, params []value, parentMem *memoryView, indent } return nil, mem, r.errorAt(inst, errUnsupportedInst) } + + // Check if an instruction triggered a recoverable error (e.g., + // trying to interpret pointer data as integer bytes). + if r.interpErr != nil { + err := r.interpErr + r.interpErr = nil + return nil, mem, r.errorAt(inst, err) + } } return nil, mem, r.errorAt(bb.instructions[len(bb.instructions)-1], errors.New("interp: reached end of basic block without terminator")) } diff --git a/interp/memory.go b/interp/memory.go index 2812cd01c2..147bc5f2a0 100644 --- a/interp/memory.go +++ b/interp/memory.go @@ -556,11 +556,13 @@ func (v pointerValue) asRawValue(r *runner) rawValue { } func (v pointerValue) Uint(r *runner) uint64 { - panic("cannot convert pointer to integer") + r.interpErr = errUnsupportedInst + return 0 } func (v pointerValue) Int(r *runner) int64 { - panic("cannot convert pointer to integer") + r.interpErr = errUnsupportedInst + return 0 } func (v pointerValue) equal(rhs pointerValue) bool { @@ -736,11 +738,12 @@ func (v rawValue) asRawValue(r *runner) rawValue { return v } -func (v rawValue) bytes() []byte { +func (v rawValue) bytes(r *runner) []byte { buf := make([]byte, len(v.buf)) for i, p := range v.buf { if p > 255 { - panic("cannot convert pointer value to byte") + r.interpErr = errUnsupportedInst + return buf } buf[i] = byte(p) } @@ -748,7 +751,10 @@ func (v rawValue) bytes() []byte { } func (v rawValue) Uint(r *runner) uint64 { - buf := v.bytes() + buf := v.bytes(r) + if r.interpErr != nil { + return 0 + } switch len(v.buf) { case 1: diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index 8cde753ba2..fa7c105915 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -1088,46 +1088,17 @@ func (v Value) MapKeys() []Value { keys := make([]Value, 0, v.Len()) it := hashmapNewIterator() + k := New(v.typecode.Key()) e := New(v.typecode.Elem()) - // Keys are stored as interface{} only for types that still use the - // legacy interface path (e.g., unsafe.Pointer). For those, we need - // to allocate an interface-sized buffer for hashmapNext (which writes - // m.keySize bytes), then unpack the interface to get the actual value. - keyType := v.typecode.key() - shouldUnpackInterface := keyType.Kind() != String && !keyType.isBinary() && !hashmapKeyUsesGenericPath(keyType) - - k := newMapKeyAlloc(keyType, shouldUnpackInterface) for hashmapNext(v.pointer(), it, k.value, e.value) { - if shouldUnpackInterface { - intf := *(*interface{})(k.value) - keys = append(keys, ValueOf(intf)) - } else { - keys = append(keys, k.Elem()) - } - k = newMapKeyAlloc(keyType, shouldUnpackInterface) + keys = append(keys, k.Elem()) + k = New(v.typecode.Key()) } return keys } -// newMapKeyAlloc allocates a Value suitable for receiving a key from -// hashmapNext. When interfaceStored is true, the map stores keys as -// interface{} (which may be larger than the declared key type), so an -// interface-sized buffer is allocated to avoid overflow. -func newMapKeyAlloc(keyType *RawType, interfaceStored bool) Value { - size := keyType.Size() - if interfaceStored { - var itf interface{} - size = unsafe.Sizeof(itf) - } - return Value{ - typecode: pointerTo(keyType), - value: alloc(size, nil), - flags: valueFlagExported, - } -} - //go:linkname hashmapStringGet runtime.hashmapStringGet func hashmapStringGet(m unsafe.Pointer, key string, value unsafe.Pointer, valueSize uintptr) bool @@ -1137,34 +1108,6 @@ func hashmapBinaryGet(m unsafe.Pointer, key, value unsafe.Pointer, valueSize uin //go:linkname hashmapGenericGet runtime.hashmapGenericGet func hashmapGenericGet(m unsafe.Pointer, key, value unsafe.Pointer, valueSize uintptr) bool -//go:linkname hashmapInterfaceGet runtime.hashmapInterfaceGet -func hashmapInterfaceGet(m unsafe.Pointer, key interface{}, value unsafe.Pointer, valueSize uintptr) bool - -// hashmapKeyUsesGenericPath reports whether the given map key type uses the -// compiler-generated hash/equal path (storing keys at their actual type) as -// opposed to the legacy interface path (storing keys as interface{}). -// This must match the compiler's hashmapCanGenerateHashEqual predicate. -func hashmapKeyUsesGenericPath(t *RawType) bool { - switch t.Kind() { - case Bool, Int, Int8, Int16, Int32, Int64, - Uint, Uint8, Uint16, Uint32, Uint64, Uintptr, - Float32, Float64, Complex64, Complex128, - String, Chan, Ptr, Interface: - return true - case Array: - return hashmapKeyUsesGenericPath(t.Elem().(*RawType)) - case Struct: - for i := 0; i < t.NumField(); i++ { - if !hashmapKeyUsesGenericPath(t.Field(i).Type.(*RawType)) { - return false - } - } - return true - default: - return false - } -} - // genericKeyPtr returns a pointer to key data suitable for passing to the // hashmapGeneric* functions. When the map's key type is an interface, // special handling is needed: if the key Value already holds an interface @@ -1196,7 +1139,6 @@ func genericKeyPtr(vkey *RawType, key Value) unsafe.Pointer { } return unsafe.Pointer(&key.value) } - func (v Value) MapIndex(key Value) Value { if v.Kind() != Map { panic(&ValueError{Method: "MapIndex", Kind: v.Kind()}) @@ -1225,12 +1167,11 @@ func (v Value) MapIndex(key Value) Value { } else { keyptr = unsafe.Pointer(&key.value) } - //TODO(dgryski): zero out padding bytes in key, if any if ok := hashmapBinaryGet(v.pointer(), keyptr, elem.value, elemType.Size()); !ok { return Value{} } return elem.Elem() - } else if hashmapKeyUsesGenericPath(vkey) { + } else { // Compiler-generated hash/equal path: keys are stored at their // actual type. Use hashmapGenericGet which dispatches through the // map's own keyHash/keyEqual function pointers. @@ -1239,12 +1180,6 @@ func (v Value) MapIndex(key Value) Value { return Value{} } return elem.Elem() - } else { - // Legacy interface path: keys are stored as interface{}. - if ok := hashmapInterfaceGet(v.pointer(), key.Interface(), elem.value, elemType.Size()); !ok { - return Value{} - } - return elem.Elem() } } @@ -1266,8 +1201,7 @@ type MapIter struct { key Value val Value - valid bool - unpackKeyInterface bool + valid bool } func (it *MapIter) Key() Value { @@ -1275,12 +1209,6 @@ func (it *MapIter) Key() Value { panic("reflect.MapIter.Key called on invalid iterator") } - if it.unpackKeyInterface { - intf := *(*interface{})(it.key.value) - v := ValueOf(intf) - return v - } - return it.key.Elem() } @@ -1301,7 +1229,7 @@ func (v Value) SetIterValue(iter *MapIter) { } func (it *MapIter) Next() bool { - it.key = newMapKeyAlloc(it.m.typecode.key(), it.unpackKeyInterface) + it.key = New(it.m.typecode.Key()) it.val = New(it.m.typecode.Elem()) it.valid = hashmapNext(it.m.pointer(), it.it, it.key.value, it.val.value) @@ -1313,15 +1241,9 @@ func (iter *MapIter) Reset(v Value) { panic(&ValueError{Method: "MapRange", Kind: v.Kind()}) } - // Keys are stored as interface{} only for types that still use the - // legacy interface path. - keyType := v.typecode.key() - shouldUnpackInterface := keyType.Kind() != String && !keyType.isBinary() && !hashmapKeyUsesGenericPath(keyType) - *iter = MapIter{ - m: v, - it: hashmapNewIterator(), - unpackKeyInterface: shouldUnpackInterface, + m: v, + it: hashmapNewIterator(), } } @@ -2067,9 +1989,6 @@ func hashmapBinarySet(m unsafe.Pointer, key, value unsafe.Pointer) //go:linkname hashmapGenericSet runtime.hashmapGenericSet func hashmapGenericSet(m unsafe.Pointer, key, value unsafe.Pointer) -//go:linkname hashmapInterfaceSet runtime.hashmapInterfaceSet -func hashmapInterfaceSet(m unsafe.Pointer, key interface{}, value unsafe.Pointer) - //go:linkname hashmapStringDelete runtime.hashmapStringDelete func hashmapStringDelete(m unsafe.Pointer, key string) @@ -2079,9 +1998,6 @@ func hashmapBinaryDelete(m unsafe.Pointer, key unsafe.Pointer) //go:linkname hashmapGenericDelete runtime.hashmapGenericDelete func hashmapGenericDelete(m unsafe.Pointer, key unsafe.Pointer) -//go:linkname hashmapInterfaceDelete runtime.hashmapInterfaceDelete -func hashmapInterfaceDelete(m unsafe.Pointer, key interface{}) - func (v Value) SetMapIndex(key, elem Value) { v.checkRO() if v.Kind() != Map { @@ -2147,7 +2063,7 @@ func (v Value) SetMapIndex(key, elem Value) { } hashmapBinarySet(v.pointer(), keyptr, elemptr) } - } else if hashmapKeyUsesGenericPath(vkey) { + } else { // Compiler-generated hash/equal path. keyptr := genericKeyPtr(vkey, key) @@ -2163,20 +2079,6 @@ func (v Value) SetMapIndex(key, elem Value) { hashmapGenericSet(v.pointer(), keyptr, elemptr) } - } else { - // Legacy interface path. - if del { - hashmapInterfaceDelete(v.pointer(), key.Interface()) - } else { - var elemptr unsafe.Pointer - if elem.isIndirect() || elem.typecode.Size() > unsafe.Sizeof(uintptr(0)) { - elemptr = elem.value - } else { - elemptr = unsafe.Pointer(&elem.value) - } - - hashmapInterfaceSet(v.pointer(), key.Interface(), elemptr) - } } } diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index 8b32ea1d20..5f235300c4 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -124,8 +124,6 @@ func hashmapKeyEqualAlg(alg tinygo.HashmapAlgorithm) func(x, y unsafe.Pointer, n return memequal case tinygo.HashmapAlgorithmString: return hashmapStringEqual - case tinygo.HashmapAlgorithmInterface: - return hashmapInterfaceEqual default: // compiler bug :( return nil @@ -138,8 +136,6 @@ func hashmapKeyHashAlg(alg tinygo.HashmapAlgorithm) func(key unsafe.Pointer, n, return hash32 case tinygo.HashmapAlgorithmString: return hashmapStringPtrHash - case tinygo.HashmapAlgorithmInterface: - return hashmapInterfacePtrHash default: // compiler bug :( return nil @@ -723,28 +719,3 @@ func hashmapInterfacePtrHash(iptr unsafe.Pointer, size uintptr, seed uintptr) ui func hashmapInterfaceEqual(x, y unsafe.Pointer, n uintptr) bool { return *(*interface{})(x) == *(*interface{})(y) } - -func hashmapInterfaceSet(m *hashmap, key interface{}, value unsafe.Pointer) { - if m == nil { - nilMapPanic() - } - hash := hashmapInterfaceHash(key, m.seed) - hashmapSet(m, unsafe.Pointer(&key), value, hash) -} - -func hashmapInterfaceGet(m *hashmap, key interface{}, value unsafe.Pointer, valueSize uintptr) bool { - if m == nil { - memzero(value, uintptr(valueSize)) - return false - } - hash := hashmapInterfaceHash(key, m.seed) - return hashmapGet(m, unsafe.Pointer(&key), value, valueSize, hash) -} - -func hashmapInterfaceDelete(m *hashmap, key interface{}) { - if m == nil { - return - } - hash := hashmapInterfaceHash(key, m.seed) - hashmapDelete(m, unsafe.Pointer(&key), hash) -} diff --git a/src/tinygo/runtime.go b/src/tinygo/runtime.go index c92417ebc6..2877f9bf42 100644 --- a/src/tinygo/runtime.go +++ b/src/tinygo/runtime.go @@ -13,5 +13,4 @@ type HashmapAlgorithm uint8 const ( HashmapAlgorithmBinary HashmapAlgorithm = iota HashmapAlgorithmString - HashmapAlgorithmInterface ) diff --git a/testdata/map.go b/testdata/map.go index 9567714679..f5be02ab06 100644 --- a/testdata/map.go +++ b/testdata/map.go @@ -1,7 +1,6 @@ package main import ( - "reflect" "sort" "unsafe" ) @@ -30,6 +29,14 @@ var testMapArrayKey = map[ArrayKey]int{ } var testmapIntInt = map[int]int{1: 1, 2: 4, 3: 9} +// Package-level pointer map literals: these exercise the interp pass's +// ability to defer pointer-keyed map inserts to runtime (since pointer +// hashes can't be computed at compile time). +var testPtrMapVar1 = 42 +var testPtrMapVar2 = 99 +var testPtrMap = map[*int]int{&testPtrMapVar1: 1, &testPtrMapVar2: 2} +var testUnsafePtrMap = map[unsafe.Pointer]int{unsafe.Pointer(&testPtrMapVar1): 10} + type namedFloat struct { s string f float32 @@ -133,11 +140,9 @@ func main() { nestedarraymaps() - reflectMapIterfaceKey() + ptrmaps() interfacerehash() - - paddingBlankMaps() } func floatcmplx() { @@ -329,7 +334,28 @@ func nestedarraymaps() { println("nested array key:", m[k]) } -func paddingBlankMaps() { +func ptrmaps() { + // Package-level pointer map literals (interp defers inserts to runtime). + println("ptr map literal:", testPtrMap[&testPtrMapVar1], testPtrMap[&testPtrMapVar2]) + println("unsafe ptr literal:", testUnsafePtrMap[unsafe.Pointer(&testPtrMapVar1)]) + + // Runtime pointer maps. + a, b, c := 1, 2, 3 + m := make(map[*int]int) + m[&a] = 10 + m[&b] = 20 + m[&c] = 30 + println("ptr map len:", len(m)) + println("ptr map a:", m[&a]) + delete(m, &b) + _, ok := m[&b] + println("ptr map deleted:", ok) + + // Runtime unsafe.Pointer maps. + m2 := make(map[unsafe.Pointer]int) + m2[unsafe.Pointer(&a)] = 100 + println("unsafe ptr map:", m2[unsafe.Pointer(&a)]) + // Struct keys with padding: the hash/equal must operate per-field // and not include padding bytes. Only test when padding actually // exists (e.g., not on AVR where alignment is 1). @@ -367,21 +393,3 @@ func paddingBlankMaps() { println("blank key lookup:", bm[bk2]) // 200 println("blank key equal:", bk1 == bk2) // true } - -// Test for issue #3794: reflect MapIter.Key() should return a value with -// interface kind for map[interface{}] keys, not the underlying concrete kind. -func reflectMapIterfaceKey() { - m := make(map[interface{}]int) - m[1] = 2 - m["hello"] = 3 - rv := reflect.ValueOf(m) - iter := rv.MapRange() - for iter.Next() { - k := iter.Key() - if k.Kind() != reflect.Interface { - println("FAIL #3794: expected interface kind, got", k.Kind().String()) - return - } - } - println("reflect map interface key ok") -} diff --git a/testdata/map.txt b/testdata/map.txt index 27c341a819..5467f05fb5 100644 --- a/testdata/map.txt +++ b/testdata/map.txt @@ -81,9 +81,14 @@ tested growing of a map 2 done nested array key: 42 -reflect map interface key ok -no interface lookup failures +ptr map literal: 1 2 +unsafe ptr literal: 10 +ptr map len: 3 +ptr map a: 10 +ptr map deleted: false +unsafe ptr map: 100 padded key lookup: 100 padded key equal: true blank key lookup: 200 blank key equal: true +no interface lookup failures diff --git a/testdata/reflect.go b/testdata/reflect.go index 6f8497c41d..476ccd1148 100644 --- a/testdata/reflect.go +++ b/testdata/reflect.go @@ -396,6 +396,24 @@ func main() { } } } + + // Test for issue #3794: reflect MapIter.Key() should return a value with + // interface kind for map[interface{}] keys, not the underlying concrete kind. + { + m := make(map[interface{}]int) + m[1] = 2 + m["hello"] = 3 + rv := reflect.ValueOf(m) + iter := rv.MapRange() + for iter.Next() { + k := iter.Key() + if k.Kind() != reflect.Interface { + println("FAIL #3794: expected interface kind, got", k.Kind().String()) + break + } + } + println("reflect map interface key ok") + } } func emptyFunc() { diff --git a/testdata/reflect.txt b/testdata/reflect.txt index fa2ef10849..dbd992e870 100644 --- a/testdata/reflect.txt +++ b/testdata/reflect.txt @@ -530,3 +530,4 @@ blue gopher v.Interface() method kind: interface int 5 +reflect map interface key ok From 1e53fe3a85910a64995151d19bf38100eb612fac Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 17 May 2026 12:08:14 -0700 Subject: [PATCH 5/6] compiler, transform: always pass hash/equal function pointers to hashmapMakeGeneric The compiler now always resolves the hash and equal functions at compile time and passes them directly to hashmapMakeGeneric, instead of passing an algorithm enum to hashmapMake and resolving at runtime. For string keys, the runtime hashmapStringPtrHash/hashmapStringEqual functions are referenced directly. For binary keys, hash32/memequal are referenced. The old hashmapMake with alg enum is retained for reflect, which still needs runtime resolution when creating maps dynamically. The OptimizeMaps transform pass is updated to handle both hashmapMake and hashmapMakeGeneric, and to recognize hashmapGenericSet in addition to hashmapBinarySet and hashmapStringSet. The now-unused hashmapCanGenerateHashEqual helper is removed. --- compiler/map.go | 74 ++++++++++++++++++----------------------------- transform/maps.go | 50 ++++++++++++++++++-------------- 2 files changed, 56 insertions(+), 68 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 75256e88a3..59d4bbf818 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -6,8 +6,6 @@ import ( "fmt" "go/token" "go/types" - - "github.com/tinygo-org/tinygo/src/tinygo" "golang.org/x/tools/go/ssa" "tinygo.org/x/go-llvm" ) @@ -36,28 +34,41 @@ func (b *builder) createMakeMap(expr *ssa.MakeMap) (llvm.Value, error) { } } - var alg uint64 + // Resolve hash and equal functions for this key type. For string and + // binary key types, reference the corresponding runtime functions + // directly. For composite types, generate type-specific functions. + var hashFn, equalFn llvm.Value if t, ok := keyType.(*types.Basic); ok && t.Info()&types.IsString != 0 { - alg = uint64(tinygo.HashmapAlgorithmString) + hashFn = b.getRuntimeFunctionValue("hashmapStringPtrHash", hashmapKeyHashSignature()) + equalFn = b.getRuntimeFunctionValue("hashmapStringEqual", hashmapKeyEqualSignature()) } else if hashmapIsBinaryKey(keyType) { - alg = uint64(tinygo.HashmapAlgorithmBinary) + hashFn = b.getRuntimeFunctionValue("hash32", hashmapKeyHashSignature()) + equalFn = b.getRuntimeFunctionValue("memequal", hashmapKeyEqualSignature()) } else { - // Composite keys: use compiler-generated hash/equal functions. - hashFn := b.getOrGenerateKeyHashFunc(keyType) - equalFn := b.getOrGenerateKeyEqualFunc(keyType) - hashFuncValue := b.createFuncValue(hashFn, llvm.ConstNull(b.dataPtrType), hashmapKeyHashSignature()) - equalFuncValue := b.createFuncValue(equalFn, llvm.ConstNull(b.dataPtrType), hashmapKeyEqualSignature()) - hashmap := b.createRuntimeCall("hashmapMakeGeneric", []llvm.Value{ - llvmKeySize, llvmValueSize, sizeHint, - hashFuncValue, equalFuncValue, - }, "") - return hashmap, nil + fn := b.getOrGenerateKeyHashFunc(keyType) + hashFn = b.createFuncValue(fn, llvm.ConstNull(b.dataPtrType), hashmapKeyHashSignature()) + fn = b.getOrGenerateKeyEqualFunc(keyType) + equalFn = b.createFuncValue(fn, llvm.ConstNull(b.dataPtrType), hashmapKeyEqualSignature()) } - algEnum := llvm.ConstInt(b.ctx.Int8Type(), alg, false) - hashmap := b.createRuntimeCall("hashmapMake", []llvm.Value{llvmKeySize, llvmValueSize, sizeHint, algEnum}, "") + + hashmap := b.createRuntimeCall("hashmapMakeGeneric", []llvm.Value{ + llvmKeySize, llvmValueSize, sizeHint, + hashFn, equalFn, + }, "") return hashmap, nil } +// getRuntimeFunctionValue returns a TinyGo function value (with nil context) +// for the named runtime function. +func (b *builder) getRuntimeFunctionValue(name string, sig *types.Signature) llvm.Value { + member := b.program.ImportedPackage("runtime").Members[name] + if member == nil { + panic("unknown runtime function: " + name) + } + _, llvmFn := b.getFunction(member.(*ssa.Function)) + return b.createFuncValue(llvmFn, llvm.ConstNull(b.dataPtrType), sig) +} + // 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) { @@ -216,35 +227,6 @@ func hashmapIsBinaryKey(keyType types.Type) bool { } } -// hashmapCanGenerateHashEqual returns true if the compiler can generate -// type-specific hash and equal functions for this key type. This covers all -// comparable types: integers, booleans, strings, floats, complex numbers, -// pointers, channels, interfaces, and composites (structs/arrays) of these. -func hashmapCanGenerateHashEqual(keyType types.Type) bool { - switch keyType := keyType.Underlying().(type) { - case *types.Basic: - return keyType.Info()&(types.IsBoolean|types.IsInteger|types.IsString|types.IsFloat|types.IsComplex) != 0 || keyType.Kind() == types.UnsafePointer - case *types.Pointer: - return true - case *types.Chan: - return true - case *types.Interface: - return true - case *types.Struct: - for i := 0; i < keyType.NumFields(); i++ { - fieldType := keyType.Field(i).Type().Underlying() - if !hashmapCanGenerateHashEqual(fieldType) { - return false - } - } - return true - case *types.Array: - return hashmapCanGenerateHashEqual(keyType.Elem()) - default: - return false - } -} - // hashmapKeyHashSignature returns the Go type signature for hashmap key hash // functions: func(key unsafe.Pointer, size, seed uintptr) uint32 func hashmapKeyHashSignature() *types.Signature { diff --git a/transform/maps.go b/transform/maps.go index 359d9cc575..a078137e4c 100644 --- a/transform/maps.go +++ b/transform/maps.go @@ -10,38 +10,44 @@ import ( // maps. This has not yet been implemented, however. func OptimizeMaps(mod llvm.Module) { hashmapMake := mod.NamedFunction("runtime.hashmapMake") - if hashmapMake.IsNil() { - // nothing to optimize - return - } + hashmapMakeGeneric := mod.NamedFunction("runtime.hashmapMakeGeneric") hashmapBinarySet := mod.NamedFunction("runtime.hashmapBinarySet") hashmapStringSet := mod.NamedFunction("runtime.hashmapStringSet") + hashmapGenericSet := mod.NamedFunction("runtime.hashmapGenericSet") - for _, makeInst := range getUses(hashmapMake) { - updateInsts := []llvm.Value{} - unknownUses := false // are there any uses other than setting a value? + optimizeMapMake := func(makeFunc llvm.Value) { + if makeFunc.IsNil() { + return + } + for _, makeInst := range getUses(makeFunc) { + updateInsts := []llvm.Value{} + unknownUses := false - for _, use := range getUses(makeInst) { - if use := use.IsACallInst(); !use.IsNil() { - switch use.CalledValue() { - case hashmapBinarySet, hashmapStringSet: - updateInsts = append(updateInsts, use) - default: + for _, use := range getUses(makeInst) { + if use := use.IsACallInst(); !use.IsNil() { + switch use.CalledValue() { + case hashmapBinarySet, hashmapStringSet, hashmapGenericSet: + updateInsts = append(updateInsts, use) + default: + unknownUses = true + } + } else { unknownUses = true } - } else { - unknownUses = true } - } - if !unknownUses { - // This map can be entirely removed, as it is only created but never - // used. - for _, inst := range updateInsts { - inst.EraseFromParentAsInstruction() + if !unknownUses { + // This map can be entirely removed, as it is only created + // but never used. + for _, inst := range updateInsts { + inst.EraseFromParentAsInstruction() + } + makeInst.EraseFromParentAsInstruction() } - makeInst.EraseFromParentAsInstruction() } } + + optimizeMapMake(hashmapMake) + optimizeMapMake(hashmapMakeGeneric) } From 21579fd07bae3c4b6675a4a29fe395fea31f3e44 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 17 May 2026 12:09:10 -0700 Subject: [PATCH 6/6] runtime: store large map keys and values indirectly When a map key or value exceeds 128 bytes, the bucket now stores a pointer to separately allocated memory instead of the data inline. This matches Go's MapMaxKeyBytes/MapMaxElemBytes threshold and prevents bucket sizes from exploding for large key/value types. For example, map[[256]byte]int previously used 2128 bytes per bucket (16 header + 256*8 keys + 8*8 values); now it uses 144 bytes per bucket (16 header + 8*8 pointers + 8*8 values). The indirection is fully encapsulated in the runtime via helper functions. Store the computed key and value slot sizes on the hashmap so all runtime and reflect paths use the same bucket layout, including non-indirect keys and values. Add big-key golden coverage and benchmarks. Make the benchmark vary enough key bytes to exercise hashing. --- compiler/testdata/go1.21.ll | 4 +- compiler/testdata/zeromap.ll | 12 +- main_test.go | 6 + src/internal/reflectlite/value.go | 3 + src/runtime/hashmap.go | 207 +++++++++++++++++++++++------- testdata/map_bigkey.go | 63 +++++++++ testdata/map_bigkey.txt | 8 ++ tests/mapbench/mapbench_test.go | 30 +++++ 8 files changed, 277 insertions(+), 56 deletions(-) create mode 100644 testdata/map_bigkey.go create mode 100644 testdata/map_bigkey.txt diff --git a/compiler/testdata/go1.21.ll b/compiler/testdata/go1.21.ll index e8ab03dedb..57c63c90c0 100644 --- a/compiler/testdata/go1.21.ll +++ b/compiler/testdata/go1.21.ll @@ -169,13 +169,13 @@ entry: } ; Function Attrs: nounwind -define hidden void @main.clearMap(ptr dereferenceable_or_null(40) %m, ptr %context) unnamed_addr #2 { +define hidden void @main.clearMap(ptr dereferenceable_or_null(48) %m, ptr %context) unnamed_addr #2 { entry: call void @runtime.hashmapClear(ptr %m, ptr undef) #5 ret void } -declare void @runtime.hashmapClear(ptr dereferenceable_or_null(40), ptr) #1 +declare void @runtime.hashmapClear(ptr dereferenceable_or_null(48), ptr) #1 attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } attributes #1 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } diff --git a/compiler/testdata/zeromap.ll b/compiler/testdata/zeromap.ll index 82c0ee0995..5ce2ebcb8a 100644 --- a/compiler/testdata/zeromap.ll +++ b/compiler/testdata/zeromap.ll @@ -17,7 +17,7 @@ entry: } ; Function Attrs: noinline nounwind -define hidden i32 @main.testZeroGet(ptr dereferenceable_or_null(40) %m, i1 %s.b1, i32 %s.i, i1 %s.b2, ptr %context) unnamed_addr #3 { +define hidden i32 @main.testZeroGet(ptr dereferenceable_or_null(48) %m, i1 %s.b1, i32 %s.i, i1 %s.b2, ptr %context) unnamed_addr #3 { entry: %hashmap.key = alloca %main.hasPadding, align 8 %hashmap.value = alloca i32, align 4 @@ -37,13 +37,13 @@ entry: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) declare void @llvm.lifetime.start.p0(i64 immarg, ptr nocapture) #4 -declare i1 @runtime.hashmapGenericGet(ptr dereferenceable_or_null(40), ptr nocapture, ptr nocapture, i32, ptr) #1 +declare i1 @runtime.hashmapGenericGet(ptr dereferenceable_or_null(48), ptr nocapture, ptr nocapture, i32, ptr) #1 ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) declare void @llvm.lifetime.end.p0(i64 immarg, ptr nocapture) #4 ; Function Attrs: noinline nounwind -define hidden void @main.testZeroSet(ptr dereferenceable_or_null(40) %m, i1 %s.b1, i32 %s.i, i1 %s.b2, ptr %context) unnamed_addr #3 { +define hidden void @main.testZeroSet(ptr dereferenceable_or_null(48) %m, i1 %s.b1, i32 %s.i, i1 %s.b2, ptr %context) unnamed_addr #3 { entry: %hashmap.key = alloca %main.hasPadding, align 8 %hashmap.value = alloca i32, align 4 @@ -60,10 +60,10 @@ entry: ret void } -declare void @runtime.hashmapGenericSet(ptr dereferenceable_or_null(40), ptr nocapture, ptr nocapture, ptr) #1 +declare void @runtime.hashmapGenericSet(ptr dereferenceable_or_null(48), ptr nocapture, ptr nocapture, ptr) #1 ; Function Attrs: noinline nounwind -define hidden i32 @main.testZeroArrayGet(ptr dereferenceable_or_null(40) %m, [2 x %main.hasPadding] %s, ptr %context) unnamed_addr #3 { +define hidden i32 @main.testZeroArrayGet(ptr dereferenceable_or_null(48) %m, [2 x %main.hasPadding] %s, ptr %context) unnamed_addr #3 { entry: %hashmap.key = alloca [2 x %main.hasPadding], align 8 %hashmap.value = alloca i32, align 4 @@ -82,7 +82,7 @@ entry: } ; Function Attrs: noinline nounwind -define hidden void @main.testZeroArraySet(ptr dereferenceable_or_null(40) %m, [2 x %main.hasPadding] %s, ptr %context) unnamed_addr #3 { +define hidden void @main.testZeroArraySet(ptr dereferenceable_or_null(48) %m, [2 x %main.hasPadding] %s, ptr %context) unnamed_addr #3 { entry: %hashmap.key = alloca [2 x %main.hasPadding], align 8 %hashmap.value = alloca i32, align 4 diff --git a/main_test.go b/main_test.go index 6ac0d0f596..aae0720f49 100644 --- a/main_test.go +++ b/main_test.go @@ -77,6 +77,7 @@ func TestBuild(t *testing.T) { "interface.go", "json.go", "map.go", + "map_bigkey.go", "math.go", "oldgo/", "print.go", @@ -296,6 +297,11 @@ func runPlatTests(options compileopts.Options, tests []string, t *testing.T) { // limited amount of memory. continue + case "map_bigkey.go": + // Compiler generates many large stack temporaries for [256]byte + // map keys, overflowing the goroutine stack (384 bytes). + continue + case "gc.go": // Does not pass due to high mark false positive rate. continue diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index fa7c105915..115c640744 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -149,6 +149,9 @@ func TypeAssert[T any](v Value) (T, bool) { // loadSmallValue loads a value of size <= sizeof(uintptr) from ptr into // a pointer-sized value suitable for storing in an interface's data field. func loadSmallValue(ptr unsafe.Pointer, size uintptr) unsafe.Pointer { + if size == unsafe.Sizeof(uintptr(0)) { + return *(*unsafe.Pointer)(ptr) + } var value uintptr for j := size; j != 0; j-- { value = (value << 8) | uintptr(*(*uint8)(unsafe.Add(ptr, j-1))) diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index 5f235300c4..ba864db305 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -13,15 +13,26 @@ import ( // The underlying hashmap structure for Go. type hashmap struct { - buckets unsafe.Pointer // pointer to array of buckets - seed uintptr - count uintptr - keySize uintptr // maybe this can store the key type as well? E.g. keysize == 5 means string? - valueSize uintptr - bucketBits uint8 - keyEqual func(x, y unsafe.Pointer, n uintptr) bool - keyHash func(key unsafe.Pointer, size, seed uintptr) uint32 -} + 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 @@ -40,6 +51,42 @@ type hashmapBucket struct { // 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 @@ -72,20 +119,35 @@ func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) *hashm bucketBits++ } - bucketBufSize := hashmapBucketHeaderSize + keySize*8 + valueSize*8 + 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< 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<> 8) + k[2] = byte(i >> 16) + k[3] = byte(i >> 24) + m[k] = i + } +}