From 975e60558944a1c7dfae5838fe6601b63b6257ab Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Mon, 27 Apr 2026 22:40:55 -0700 Subject: [PATCH 01/17] compiler, runtime, reflect: generate type-specific hash/equal for composite map keys MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 all comparable types: strings, floats, complex numbers, interfaces, channels, and composites (structs/arrays) containing any mix of these. Previously, a map like map[struct{s string; x int}]int would: 1. Convert the key to interface{} via createMakeInterface (heap alloc) 2. Hash via hashmapInterfaceHash using reflection (slow, more allocs) 3. Compare via interface equality (reflection) Now, the compiler: 1. Generates a hash function that walks struct fields, dispatching to the appropriate runtime helper per field type (hashmapStringPtrHash for strings, hashmapFloat32Hash/hashmapFloat64Hash for floats, hash32 for integers/pointers/channels, hashmapInterfacePtrHash for interfaces) 2. Generates an equal function that compares fields directly (stringEqual for strings, fcmp oeq for floats, memequal for binary types, hashmapInterfaceEqual for interfaces) 3. Stores the key at its actual type (no interface conversion) 4. Passes the generated functions to hashmapMakeGeneric Additionally, nocapture LLVM attributes are added to the key and value pointer parameters of hashmapSet, hashmapGet, hashmapDelete, and the new hashmapGeneric* functions. The indirect calls through m.keyHash and m.keyEqual function pointers prevent LLVM from inferring nocapture automatically; adding it explicitly allows the escape analysis to move key allocations from heap to stack. The reflect package is updated to match: MapIndex, SetMapIndex, MapKeys, and MapIter now use hashmapGenericGet/Set/Delete for non-string, non-binary keys that use the generated hash/equal path, and fall back to hashmapInterfaceGet/Set/Delete only for types still using the legacy interface storage (currently only unsafe.Pointer). The shouldUnpackInterface logic is similarly narrowed. This also fixes #3794, where reflect.MapIter.Key() returned a value with the concrete key kind (e.g., Int) instead of Interface for map[interface{}] keys. The old shouldUnpackInterface logic treated all non-string, non-binary key types as interface-wrapped; it would read the raw key bytes as an interface{} and call ValueOf to extract the concrete type. For map[interface{}], the key type IS interface{}, so this unwrapping was incorrect. The new code only applies interface unpacking for types that actually use the legacy interface storage path. Benchmarks on wasip1 (wasmtime), map with 100 entries: │ old.txt │ new.txt │ │ sec/op │ sec/op vs base │ StringShortGet 397.0n ± 9% 53.3n ± 12% -86.56% StringLongGet 458.0n ± 2% 96.8n ± 2% -78.86% CompositeGet 2214.5n ± 1% 71.4n ± 1% -96.78% IntGet 55.1n ± 2% 55.7n ± 3% ~ │ old.txt │ new.txt │ │ B/op │ B/op vs base │ StringShort 8.00 ± 0% 0.00 ± 0% -100.00% StringLong 8.00 ± 0% 0.00 ± 0% -100.00% CompositeGet 32.00 ± 0% 0.00 ± 0% -100.00% IntGet 0.00 ± 0% 0.00 ± 0% ~ Fixes #3354. Fixes #3794. --- compiler/map.go | 399 +++++++++++++++++++++++++++--- compiler/symbol.go | 25 ++ src/internal/reflectlite/value.go | 124 +++++++++- src/runtime/hashmap.go | 70 +++++- testdata/map.go | 21 ++ testdata/map.txt | 1 + tests/mapbench/mapbench_test.go | 68 +++++ 7 files changed, 657 insertions(+), 51 deletions(-) create mode 100644 tests/mapbench/mapbench_test.go diff --git a/compiler/map.go b/compiler/map.go index 6aaf8c76c6..fc8a6c9ec0 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,18 @@ 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 +143,17 @@ 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 +178,17 @@ 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 +221,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 } @@ -271,8 +293,41 @@ 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: + // 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 !hashmapCanGenerateHashEqual(fieldType) { + return false + } + } + return true + case *types.Array: + 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. + // Zero all undefined (padding) bytes in the key data before storing it in + // a map bucket. This ensures that padding bytes don't affect binary + // comparisons or hash values. // 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 @@ -336,3 +391,273 @@ func (b *builder) zeroUndefBytes(llvmType llvm.Type, ptr llvm.Value) error { return nil } + +// 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, + ) +} + +// 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, + ) +} + +// hashmapKeyFuncName returns a unique name for a generated hash or equal +// function based on the Go type. +func hashmapKeyFuncName(prefix string, keyType types.Type) string { + return prefix + "." + keyType.String() +} + +// 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 + } + + // 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) + + return fn +} + +// 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 + } + + // 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) + + 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 via hashmapStringPtrHash. + 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++ { + 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)) + } +} + +// 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++ { + 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/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< Date: Tue, 28 Apr 2026 23:14:54 -0700 Subject: [PATCH 02/17] 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 (which converted keys to interface{} and used reflection for hashing) 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 +++--------------------------- testdata/map.go | 48 ++++++++----- testdata/map.txt | 7 +- testdata/reflect.go | 18 +++++ testdata/reflect.txt | 1 + 9 files changed, 98 insertions(+), 215 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index fc8a6c9ec0..eeba1744bc 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -34,11 +34,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()) @@ -49,18 +51,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 @@ -87,13 +77,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") @@ -106,15 +95,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 @@ -137,13 +117,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) @@ -191,17 +160,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 } } @@ -221,38 +179,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) @@ -272,10 +207,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.Struct: @@ -300,9 +232,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 30b0872485..88854137ac 100644 --- a/interp/interp.go +++ b/interp/interp.go @@ -35,6 +35,7 @@ type runner struct { start time.Time timeout time.Duration callsExecuted uint64 + interpErr error // set by Uint/Int when they encounter pointer data } func newRunner(mod llvm.Module, timeout time.Duration, debug bool) *runner { diff --git a/interp/interpreter.go b/interp/interpreter.go index e8f5545d5d..feb16a10e6 100644 --- a/interp/interpreter.go +++ b/interp/interpreter.go @@ -825,6 +825,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 77983ae17a..d10c25f929 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -1082,46 +1082,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 @@ -1131,34 +1102,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 @@ -1182,7 +1125,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()}) @@ -1211,12 +1153,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. @@ -1225,12 +1166,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() } } @@ -1252,8 +1187,7 @@ type MapIter struct { key Value val Value - valid bool - unpackKeyInterface bool + valid bool } func (it *MapIter) Key() Value { @@ -1261,12 +1195,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() } @@ -1287,7 +1215,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) @@ -1299,15 +1227,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(), } } @@ -2053,9 +1975,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) @@ -2065,9 +1984,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 { @@ -2129,7 +2045,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) @@ -2145,20 +2061,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/testdata/map.go b/testdata/map.go index 0bef18bb09..cd695093cc 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 @@ -131,7 +138,7 @@ func main() { mapgrow() - reflectMapIterfaceKey() + ptrmaps() interfacerehash() } @@ -312,20 +319,25 @@ func interfacerehash() { } } -// 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") +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)]) } diff --git a/testdata/map.txt b/testdata/map.txt index cc6077f42f..a74d3ee150 100644 --- a/testdata/map.txt +++ b/testdata/map.txt @@ -80,5 +80,10 @@ tested growing of a map 2 2 done -reflect map interface key ok +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 no interface lookup failures diff --git a/testdata/reflect.go b/testdata/reflect.go index 873d60f787..2bcf00a3c2 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 3024568c3d..fc869f8c58 100644 --- a/testdata/reflect.txt +++ b/testdata/reflect.txt @@ -512,3 +512,4 @@ blue gopher v.Interface() method kind: interface int 5 +reflect map interface key ok From d5d60d37d4c3838aff42323df2c5f9d59061a564 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 1 May 2026 14:35:27 -0700 Subject: [PATCH 03/17] compiler: clarify hashmapStringPtrHash size parameter The size parameter passed to hashmapStringPtrHash is unused; the function dereferences the string header to get the actual length. Add a comment explaining this, since the call site makes it look like size matters. --- compiler/map.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/compiler/map.go b/compiler/map.go index eeba1744bc..0291e99f75 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -435,7 +435,10 @@ func (b *builder) generateKeyHash(keyType types.Type, llvmKeyType llvm.Type, key switch keyType := keyType.Underlying().(type) { case *types.Basic: if keyType.Info()&types.IsString != 0 { - // Hash the string contents via hashmapStringPtrHash. + // 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") } From 8dd043d2d6b84a3ce08c6309b8dc2b0a3cd0784d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 1 May 2026 14:37:24 -0700 Subject: [PATCH 04/17] 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. --- compiler/map.go | 91 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 74 insertions(+), 17 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 0291e99f75..fb87f87479 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -497,15 +497,45 @@ 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) + } + // Non-binary elements: generate a loop to avoid code explosion + // for large arrays. + 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") + + idx := b.CreateTrunc(phiI, b.ctx.Int32Type(), "") + elemPtr := b.CreateInBoundsGEP(llvmKeyType, keyPtr, []llvm.Value{zero, idx}, "") + 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) + + phiI.AddIncoming([]llvm.Value{llvm.ConstInt(b.uintptrType, 0, false), nextI}, + []llvm.BasicBlock{loopEntry, loopBody}) + phiHash.AddIncoming([]llvm.Value{initHash, newHash}, + []llvm.BasicBlock{loopEntry, loopBody}) + + b.SetInsertPointAtEnd(loopDone) + return newHash default: panic(fmt.Sprintf("unhandled key type for hash generation: %T", keyType)) } @@ -580,16 +610,43 @@ 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) + } + // Non-binary elements: generate a loop with short-circuit exit. + 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") + + idx := b.CreateTrunc(phiI, b.ctx.Int32Type(), "") + 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) + + 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)) } From fcfd8ca02fbf696f75b476fa662b4eca46666d0d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 1 May 2026 14:39:33 -0700 Subject: [PATCH 05/17] 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. --- compiler/map.go | 45 ++++++++++++++++++++++++++---------------- transform/maps.go | 50 ++++++++++++++++++++++++++--------------------- 2 files changed, 56 insertions(+), 39 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index fb87f87479..7dc99f82a4 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" ) @@ -34,28 +32,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) { 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 0646500e0936d6d718f4f1e6030e7f6b7cc423e9 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 1 May 2026 14:42:41 -0700 Subject: [PATCH 06/17] compiler: use canonical underlying types for hash/equal function names Generated hash/equal function names now use the underlying type structure instead of the Go type string. This means structurally identical types with different named fields (e.g., struct{i1; str1} and struct{i2; str2} where both resolve to struct{int; string}) share a single generated function instead of getting duplicates. --- compiler/map.go | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 7dc99f82a4..83832cc7a0 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -365,10 +365,52 @@ func hashmapKeyEqualSignature() *types.Signature { ) } -// hashmapKeyFuncName returns a unique name for a generated hash or equal -// function based on the Go type. +// 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 + "." + keyType.String() + return prefix + "." + hashmapCanonicalTypeName(keyType) +} + +// 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() } // getOrGenerateKeyHashFunc returns an LLVM function that computes the hash From f46da5a67ecf63cb344e9d239cf6b97de2238cf7 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 1 May 2026 15:07:54 -0700 Subject: [PATCH 07/17] 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 (hashmapKeySlotSize, hashmapValueSlotSize, hashmapSlotKeyData, hashmapSlotValueData, hashmapStoreKey, hashmapStoreValue). No compiler or reflect changes are needed. --- main_test.go | 6 ++ src/runtime/hashmap.go | 163 +++++++++++++++++++++++++++----- testdata/map_bigkey.go | 63 ++++++++++++ testdata/map_bigkey.txt | 8 ++ tests/mapbench/mapbench_test.go | 28 ++++++ 5 files changed, 242 insertions(+), 26 deletions(-) create mode 100644 testdata/map_bigkey.go create mode 100644 testdata/map_bigkey.txt diff --git a/main_test.go b/main_test.go index 55eb678910..66d18f4aad 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", @@ -283,6 +284,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/runtime/hashmap.go b/src/runtime/hashmap.go index f316185eb1..a51d9c6f3a 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -16,13 +16,22 @@ 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? + keySize uintptr valueSize uintptr 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 // ordering is to make sure the keys and values are well aligned when one of @@ -40,6 +49,48 @@ 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 { + if m.flags&hashmapFlagIndirectKey != 0 { + return unsafe.Sizeof(unsafe.Pointer(nil)) + } + return m.keySize +} + +// hashmapValueSlotSize returns the size of a value slot in the bucket. +// +//go:inline +func hashmapValueSlotSize(m *hashmap) uintptr { + if m.flags&hashmapFlagIndirectValue != 0 { + return unsafe.Sizeof(unsafe.Pointer(nil)) + } + return m.valueSize +} + +// 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,7 +123,19 @@ 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) + m[k] = i + } +} From b864663aad67e430a5477cd1a2d14e3802b5522a Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 1 May 2026 15:16:09 -0700 Subject: [PATCH 08/17] compiler: unroll small array hash/equal instead of looping --- compiler/map.go | 28 +++++++++++++++++++++++++--- 1 file changed, 25 insertions(+), 3 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 83832cc7a0..597f38618a 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -10,6 +10,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) { @@ -559,8 +561,17 @@ func (b *builder) generateKeyHash(keyType types.Type, llvmKeyType llvm.Type, key if arrayLen == 0 { return llvm.ConstInt(b.ctx.Int32Type(), 0, false) } - // Non-binary elements: generate a loop to avoid code explosion - // for large arrays. + 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) @@ -672,7 +683,18 @@ func (b *builder) generateKeyEqual(keyType types.Type, llvmKeyType llvm.Type, xP if arrayLen == 0 { return llvm.ConstInt(b.ctx.Int1Type(), 1, false) } - // Non-binary elements: generate a loop with short-circuit exit. + 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() From 75f44e8417127faabd7743a04c547e5a8fd956a2 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 5 May 2026 11:47:08 -0700 Subject: [PATCH 09/17] reflect: fix at-runtime map issues from review, and more found locally --- src/internal/reflectlite/value.go | 21 ++++---- src/runtime/hashmap.go | 63 ++++++++++++------------ src/tinygo/runtime.go | 1 - testdata/reflect.go | 79 +++++++++++++++++++++++++++++++ testdata/reflect.txt | 14 ++++++ 5 files changed, 139 insertions(+), 39 deletions(-) diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index d10c25f929..e53ac23021 100644 --- a/src/internal/reflectlite/value.go +++ b/src/internal/reflectlite/value.go @@ -2013,7 +2013,7 @@ func (v Value) SetMapIndex(key, elem Value) { } } - if key.Kind() == String { + if vkey.Kind() == String { if del { hashmapStringDelete(v.pointer(), *(*string)(key.value)) } else { @@ -2026,7 +2026,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 @@ -2116,6 +2116,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 { @@ -2124,7 +2127,6 @@ func MakeMapWithSize(typ Type, n int) Value { const ( hashmapAlgorithmBinary uint8 = iota hashmapAlgorithmString - hashmapAlgorithmInterface ) if typ.Kind() != Map { @@ -2138,18 +2140,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 a51d9c6f3a..1e7f25e403 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -188,8 +188,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 @@ -202,8 +200,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 @@ -664,6 +660,40 @@ 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)) { + var raw uintptr + memcpy(unsafe.Pointer(&raw), ptr, t.Size()) + val = unsafe.Pointer(raw) + } else { + val = ptr + } + i := composeInterface(unsafe.Pointer(t), val) + return *(*interface{})(unsafe.Pointer(&i)) +} + // Hashmap with string keys (a common case). func hashmapStringEqual(x, y unsafe.Pointer, n uintptr) bool { @@ -799,28 +829,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/reflect.go b/testdata/reflect.go index 2bcf00a3c2..2cc522f165 100644 --- a/testdata/reflect.go +++ b/testdata/reflect.go @@ -773,6 +773,8 @@ 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() } type IfaceNode interface { @@ -825,3 +827,80 @@ 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"]) +} diff --git a/testdata/reflect.txt b/testdata/reflect.txt index fc869f8c58..e0c3143b49 100644 --- a/testdata/reflect.txt +++ b/testdata/reflect.txt @@ -503,6 +503,20 @@ 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 + alignment / offset: struct{[0]func(); byte}: true From fbf8be4d86a18093f98175dc4b5ba314029ca8fd Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 5 May 2026 12:24:25 -0700 Subject: [PATCH 10/17] reflect: further fixes --- src/internal/reflectlite/value.go | 34 +++++++++++++++++++++++-------- src/runtime/hashmap.go | 7 ++++--- testdata/reflect.go | 11 ++++++++++ testdata/reflect.txt | 1 + 4 files changed, 42 insertions(+), 11 deletions(-) diff --git a/src/internal/reflectlite/value.go b/src/internal/reflectlite/value.go index e53ac23021..fa7c105915 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) } @@ -1117,7 +1123,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)) { @@ -2005,8 +2019,12 @@ 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), diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index 1e7f25e403..a84177df93 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -684,9 +684,7 @@ func hashmapMakeReflect(keySize, valueSize, sizeHint uintptr, keyType unsafe.Poi func rawToInterface(t *reflectlite.RawType, ptr unsafe.Pointer) interface{} { var val unsafe.Pointer if t.Size() <= unsafe.Sizeof(uintptr(0)) { - var raw uintptr - memcpy(unsafe.Pointer(&raw), ptr, t.Size()) - val = unsafe.Pointer(raw) + val = reflectliteLoadSmallValue(ptr, t.Size()) } else { val = ptr } @@ -694,6 +692,9 @@ func rawToInterface(t *reflectlite.RawType, ptr unsafe.Pointer) interface{} { 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 2cc522f165..36b6524dbb 100644 --- a/testdata/reflect.go +++ b/testdata/reflect.go @@ -903,4 +903,15 @@ func testMakeMapInterfaceKey() { 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") + } } diff --git a/testdata/reflect.txt b/testdata/reflect.txt index e0c3143b49..a56389ac13 100644 --- a/testdata/reflect.txt +++ b/testdata/reflect.txt @@ -516,6 +516,7 @@ len: 2 hello: 200 compiled 42: 100 compiled hello: 200 +addressable 99: 300 alignment / offset: struct{[0]func(); byte}: true From 44edb16161556be47627da10e139708f06357a0e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 5 May 2026 13:23:19 -0700 Subject: [PATCH 11/17] compiler, reflect: make structs not binary, ignore blank named fields --- compiler/map.go | 14 ++++++-------- testdata/map.go | 30 ++++++++++++++++++++++++++++++ testdata/map.txt | 4 ++++ testdata/reflect.go | 29 +++++++++++++++++++++++++++++ testdata/reflect.txt | 3 +++ 5 files changed, 72 insertions(+), 8 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 597f38618a..18929f73ba 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -223,14 +223,6 @@ func hashmapIsBinaryKey(keyType types.Type) bool { return keyType.Info()&(types.IsBoolean|types.IsInteger) != 0 || keyType.Kind() == types.UnsafePointer case *types.Pointer: return true - case *types.Struct: - for i := 0; i < keyType.NumFields(); i++ { - fieldType := keyType.Field(i).Type().Underlying() - if !hashmapIsBinaryKey(fieldType) { - return false - } - } - return true case *types.Array: return hashmapIsBinaryKey(keyType.Elem()) default: @@ -538,6 +530,9 @@ func (b *builder) generateKeyHash(keyType types.Type, llvmKeyType llvm.Type, key 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 { @@ -659,6 +654,9 @@ func (b *builder) generateKeyEqual(keyType types.Type, llvmKeyType llvm.Type, xP 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 { diff --git a/testdata/map.go b/testdata/map.go index cd695093cc..e7a641c799 100644 --- a/testdata/map.go +++ b/testdata/map.go @@ -340,4 +340,34 @@ func ptrmaps() { 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. + type paddedKey struct { + A int8 + B int32 + } + 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 bytes. + (*[8]byte)(unsafe.Pointer(&pk2))[1] = 0xFF + pm[pk1] = 100 + println("padded key lookup:", pm[pk2]) // 100 + println("padded key equal:", pk1 == pk2) // 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 } diff --git a/testdata/map.txt b/testdata/map.txt index a74d3ee150..7a2f587455 100644 --- a/testdata/map.txt +++ b/testdata/map.txt @@ -86,4 +86,8 @@ 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 36b6524dbb..74616f6c43 100644 --- a/testdata/reflect.go +++ b/testdata/reflect.go @@ -775,6 +775,7 @@ func testImplements() { testValueSetInterface() testMakeMapCompositeKey() testMakeMapInterfaceKey() + testMakeMapPaddedKey() } type IfaceNode interface { @@ -915,3 +916,31 @@ func testMakeMapInterfaceKey() { 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 + // Poison pk2's padding via unsafe. + (*[8]byte)(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 a56389ac13..dbd992e870 100644 --- a/testdata/reflect.txt +++ b/testdata/reflect.txt @@ -518,6 +518,9 @@ compiled 42: 100 compiled hello: 200 addressable 99: 300 +reflect.MakeMap padded key: +padded lookup: 100 + alignment / offset: struct{[0]func(); byte}: true From 0f0500f2df3ed21ed0b8d1c5880bf7d87097a826 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 5 May 2026 13:37:19 -0700 Subject: [PATCH 12/17] tests: fix benchmark hashing --- tests/mapbench/mapbench_test.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/mapbench/mapbench_test.go b/tests/mapbench/mapbench_test.go index a67bb6add0..57c287876b 100644 --- a/tests/mapbench/mapbench_test.go +++ b/tests/mapbench/mapbench_test.go @@ -89,8 +89,10 @@ func BenchmarkMapBigKeySet(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { var k bigKey - k[0] = byte(i & 0xff) + k[0] = byte(i) k[1] = byte(i >> 8) + k[2] = byte(i >> 16) + k[3] = byte(i >> 24) m[k] = i } } From 5e38b70cfaa3d8299fa9cf648a35f03829cbdc72 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 5 May 2026 16:51:45 -0700 Subject: [PATCH 13/17] reflect: fix regression for non-indirect keys --- src/runtime/hashmap.go | 66 +++++++++++++++++++++--------------------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index a84177df93..ba864db305 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -13,15 +13,17 @@ import ( // The underlying hashmap structure for Go. type hashmap struct { - buckets unsafe.Pointer // pointer to array of buckets - seed uintptr - count uintptr - keySize uintptr - valueSize uintptr - bucketBits uint8 - flags 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 ( @@ -54,20 +56,14 @@ const hashmapBucketHeaderSize = (unsafe.Sizeof(hashmapBucket{}) + 7) &^ 7 // //go:inline func hashmapKeySlotSize(m *hashmap) uintptr { - if m.flags&hashmapFlagIndirectKey != 0 { - return unsafe.Sizeof(unsafe.Pointer(nil)) - } - return m.keySize + return m.keySlotSize } // hashmapValueSlotSize returns the size of a value slot in the bucket. // //go:inline func hashmapValueSlotSize(m *hashmap) uintptr { - if m.flags&hashmapFlagIndirectValue != 0 { - return unsafe.Sizeof(unsafe.Pointer(nil)) - } - return m.valueSize + return m.valueSlotSize } // hashmapSlotKeyData returns a pointer to the actual key data for a given slot. @@ -142,14 +138,16 @@ func hashmapMake(keySize, valueSize uintptr, sizeHint uintptr, alg uint8) *hashm keyEqual := hashmapKeyEqualAlg(tinygo.HashmapAlgorithm(alg)) return &hashmap{ - buckets: buckets, - seed: uintptr(fastrand()), - keySize: keySize, - valueSize: valueSize, - bucketBits: bucketBits, - flags: flags, - keyEqual: keyEqual, - keyHash: keyHash, + buckets: buckets, + seed: uintptr(fastrand()), + keySize: keySize, + valueSize: valueSize, + keySlotSize: keySlotSize, + valueSlotSize: valueSlotSize, + bucketBits: bucketBits, + flags: flags, + keyEqual: keyEqual, + keyHash: keyHash, } } @@ -649,14 +647,16 @@ func hashmapMakeGeneric(keySize, valueSize uintptr, sizeHint uintptr, buckets := alloc(bucketBufSize*(1< Date: Tue, 5 May 2026 16:55:20 -0700 Subject: [PATCH 14/17] Fix padding tests --- testdata/map.go | 27 +++++++++++++++++---------- testdata/reflect.go | 7 +++++-- 2 files changed, 22 insertions(+), 12 deletions(-) diff --git a/testdata/map.go b/testdata/map.go index e7a641c799..66b99ef2ae 100644 --- a/testdata/map.go +++ b/testdata/map.go @@ -342,20 +342,27 @@ func ptrmaps() { println("unsafe ptr map:", m2[unsafe.Pointer(&a)]) // Struct keys with padding: the hash/equal must operate per-field - // and not include padding bytes. + // and not include padding bytes. Only test when padding actually + // exists (e.g., not on AVR where alignment is 1). type paddedKey struct { A int8 B int32 } - 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 bytes. - (*[8]byte)(unsafe.Pointer(&pk2))[1] = 0xFF - pm[pk1] = 100 - println("padded key lookup:", pm[pk2]) // 100 - println("padded key equal:", pk1 == pk2) // true + if unsafe.Offsetof(paddedKey{}.B) > 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 { diff --git a/testdata/reflect.go b/testdata/reflect.go index 74616f6c43..476ccd1148 100644 --- a/testdata/reflect.go +++ b/testdata/reflect.go @@ -931,8 +931,11 @@ func testMakeMapPaddedKey() { pk1.B = 42 pk2.A = 1 pk2.B = 42 - // Poison pk2's padding via unsafe. - (*[8]byte)(unsafe.Pointer(&pk2))[1] = 0xFF + + 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{})) From cfce6bcda18d22ec075602ff9f9859a6984eb83c Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Tue, 5 May 2026 17:56:48 -0700 Subject: [PATCH 15/17] Fix tests again --- compiler/testdata/go1.21.ll | 4 ++-- compiler/testdata/zeromap.ll | 20 ++++++++++---------- src/internal/reflectlite/value.go | 3 +++ 3 files changed, 15 insertions(+), 12 deletions(-) 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 058c14fb32..b8ce4bf521 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 @@ -31,7 +31,7 @@ entry: 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 + %5 = 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 call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %hashmap.value) @@ -43,13 +43,13 @@ 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(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 @@ -64,16 +64,16 @@ entry: 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(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 @@ -92,7 +92,7 @@ entry: 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 + %4 = 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 call void @llvm.lifetime.end.p0(i64 4, ptr nonnull %hashmap.value) @@ -100,7 +100,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 @@ -120,7 +120,7 @@ entry: 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 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))) From 45074882cd90375f4bc2f49f4aff1e22fca79a76 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 6 May 2026 12:05:32 -0700 Subject: [PATCH 16/17] Remove zeroUndefBytes as structs are no longer binary --- compiler/map.go | 71 ------------------------------------ compiler/testdata/zeromap.ll | 38 +++---------------- 2 files changed, 6 insertions(+), 103 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 18929f73ba..2d3c33583f 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -100,7 +100,6 @@ func (b *builder) createMapLookup(keyType, valueType types.Type, m, key llvm.Val // compiler-generated hash/equal. mapKeyAlloca, mapKeySize := b.createTemporaryAlloca(key.Type(), "hashmap.key") b.CreateStore(key, mapKeyAlloca) - b.zeroUndefBytes(b.getLLVMType(keyType), mapKeyAlloca) params := []llvm.Value{m, mapKeyAlloca, mapValueAlloca, mapValueSize} fnName := "hashmapBinaryGet" if !hashmapIsBinaryKey(keyType) { @@ -139,7 +138,6 @@ func (b *builder) createMapUpdate(keyType types.Type, m, key, value llvm.Value, // 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" @@ -164,7 +162,6 @@ func (b *builder) createMapDelete(keyType types.Type, m, key llvm.Value, pos tok // 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" @@ -259,74 +256,6 @@ func hashmapCanGenerateHashEqual(keyType types.Type) bool { } } -func (b *builder) zeroUndefBytes(llvmType llvm.Type, ptr llvm.Value) error { - // Zero all undefined (padding) bytes in the key data before storing it in - // a map bucket. This ensures that padding bytes don't affect binary - // comparisons or hash values. - // 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) - - 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() - - for i := 0; i < llvmArrayType.ArrayLength(); i++ { - idx := llvm.ConstInt(b.uintptrType, uint64(i), false) - elemPtr := b.CreateInBoundsGEP(llvmArrayType, ptr, []llvm.Value{zero, idx}, "") - - // zero any padding bytes in this element - b.zeroUndefBytes(llvmElemType, elemPtr) - } - - case llvm.StructTypeKind: - llvmStructType := llvmType - numFields := llvmStructType.StructElementTypesCount() - llvmElementTypes := llvmStructType.StructElementTypes() - - for i := 0; i < numFields; i++ { - idx := llvm.ConstInt(b.ctx.Int32Type(), uint64(i), false) - elemPtr := b.CreateInBoundsGEP(llvmStructType, ptr, []llvm.Value{zero, idx}, "") - - // zero any padding bytes in this field - llvmElemType := llvmElementTypes[i] - b.zeroUndefBytes(llvmElemType, elemPtr) - - // zero any padding bytes before the next field, if any - offset := b.targetData.ElementOffset(llvmStructType, i) - storeSize := b.targetData.TypeStoreSize(llvmElemType) - fieldEndOffset := offset + storeSize - - 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) - } - - 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 nil -} - // 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/compiler/testdata/zeromap.ll b/compiler/testdata/zeromap.ll index b8ce4bf521..5ce2ebcb8a 100644 --- a/compiler/testdata/zeromap.ll +++ b/compiler/testdata/zeromap.ll @@ -27,22 +27,16 @@ 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.hashmapGenericGet(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.hashmapGenericGet(ptr dereferenceable_or_null(48), ptr nocapture, ptr nocapture, i32, ptr) #1 ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) @@ -60,10 +54,6 @@ 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.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) @@ -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.hashmapGenericGet(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,14 +94,6 @@ 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.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) From 9de2ed227ab3ae39f3b2d3b04ee5b56f2e2cda2e Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Wed, 6 May 2026 12:08:54 -0700 Subject: [PATCH 17/17] Remove unused hashmapCanGenerateHashEqual too --- compiler/map.go | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/compiler/map.go b/compiler/map.go index 2d3c33583f..9e380209a3 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -227,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 {