Skip to content
Open
Show file tree
Hide file tree
Changes from 11 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
588 changes: 488 additions & 100 deletions compiler/map.go

Large diffs are not rendered by default.

25 changes: 25 additions & 0 deletions compiler/symbol.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions interp/interp.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
8 changes: 8 additions & 0 deletions interp/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Comment on lines +829 to +834
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is true, but I think already a hazard in interp if another value returned 0 because the user wrote it?

}
}
return nil, mem, r.errorAt(bb.instructions[len(bb.instructions)-1], errors.New("interp: reached end of basic block without terminator"))
}
Expand Down
16 changes: 11 additions & 5 deletions interp/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -736,19 +738,23 @@ 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)
}
return buf
}

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:
Expand Down
6 changes: 6 additions & 0 deletions main_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ func TestBuild(t *testing.T) {
"interface.go",
"json.go",
"map.go",
"map_bigkey.go",
"math.go",
"oldgo/",
"print.go",
Expand Down Expand Up @@ -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
Expand Down
135 changes: 81 additions & 54 deletions src/internal/reflectlite/value.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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)
}
Expand Down Expand Up @@ -1085,18 +1091,8 @@ func (v Value) MapKeys() []Value {
k := New(v.typecode.Key())
e := New(v.typecode.Elem())

keyType := v.typecode.key()
keyTypeIsEmptyInterface := keyType.Kind() == Interface && keyType.NumMethod() == 0
shouldUnpackInterface := !keyTypeIsEmptyInterface && keyType.Kind() != String && !keyType.isBinary()
Comment thread
jakebailey marked this conversation as resolved.

for hashmapNext(v.pointer(), it, k.value, e.value) {
if shouldUnpackInterface {
intf := *(*interface{})(k.value)
v := ValueOf(intf)
keys = append(keys, v)
} else {
keys = append(keys, k.Elem())
}
keys = append(keys, k.Elem())
Comment thread
jakebailey marked this conversation as resolved.
k = New(v.typecode.Key())
}

Expand All @@ -1109,9 +1105,40 @@ func hashmapStringGet(m unsafe.Pointer, key string, value unsafe.Pointer, valueS
//go:linkname hashmapBinaryGet runtime.hashmapBinaryGet
func hashmapBinaryGet(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

//go:linkname hashmapGenericGet runtime.hashmapGenericGet
func hashmapGenericGet(m unsafe.Pointer, key, value unsafe.Pointer, valueSize uintptr) bool

// 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.
// 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)
Comment thread
jakebailey marked this conversation as resolved.
}
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()})
Expand Down Expand Up @@ -1140,13 +1167,16 @@ 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 ok := hashmapInterfaceGet(v.pointer(), key.Interface(), elem.value, elemType.Size()); !ok {
// 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 {
Comment thread
jakebailey marked this conversation as resolved.
return Value{}
}
return elem.Elem()
Expand All @@ -1171,21 +1201,14 @@ type MapIter struct {
key Value
val Value

valid bool
unpackKeyInterface bool
valid bool
}

func (it *MapIter) Key() Value {
if !it.valid {
panic("reflect.MapIter.Key called on invalid iterator")
}

Comment thread
jakebailey marked this conversation as resolved.
if it.unpackKeyInterface {
intf := *(*interface{})(it.key.value)
v := ValueOf(intf)
return v
}

return it.key.Elem()
}

Expand Down Expand Up @@ -1218,15 +1241,9 @@ func (iter *MapIter) Reset(v Value) {
panic(&ValueError{Method: "MapRange", Kind: v.Kind()})
}

keyType := v.typecode.key()

keyTypeIsEmptyInterface := keyType.Kind() == Interface && keyType.NumMethod() == 0
shouldUnpackInterface := !keyTypeIsEmptyInterface && keyType.Kind() != String && !keyType.isBinary()

*iter = MapIter{
m: v,
it: hashmapNewIterator(),
unpackKeyInterface: shouldUnpackInterface,
m: v,
it: hashmapNewIterator(),
}
}

Expand Down Expand Up @@ -1969,17 +1986,17 @@ 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 hashmapInterfaceSet runtime.hashmapInterfaceSet
func hashmapInterfaceSet(m unsafe.Pointer, key interface{}, value unsafe.Pointer)
//go:linkname hashmapGenericSet runtime.hashmapGenericSet
func hashmapGenericSet(m unsafe.Pointer, key, value unsafe.Pointer)

//go:linkname hashmapStringDelete runtime.hashmapStringDelete
func hashmapStringDelete(m unsafe.Pointer, key string)

//go:linkname hashmapBinaryDelete runtime.hashmapBinaryDelete
func hashmapBinaryDelete(m unsafe.Pointer, key unsafe.Pointer)

//go:linkname hashmapInterfaceDelete runtime.hashmapInterfaceDelete
func hashmapInterfaceDelete(m unsafe.Pointer, key interface{})
//go:linkname hashmapGenericDelete runtime.hashmapGenericDelete
func hashmapGenericDelete(m unsafe.Pointer, key unsafe.Pointer)

func (v Value) SetMapIndex(key, elem Value) {
v.checkRO()
Expand All @@ -2002,15 +2019,19 @@ func (v Value) SetMapIndex(key, elem Value) {
}

// make elem an interface if it needs to be converted
if v.typecode.elem().Kind() == Interface && elem.typecode.Kind() != Interface {
intf := composeInterface(unsafe.Pointer(elem.typecode), elem.value)
if !del && v.typecode.elem().Kind() == Interface && elem.typecode.Kind() != Interface {
val := elem.value
if elem.isIndirect() && elem.typecode.Size() <= unsafe.Sizeof(uintptr(0)) {
val = loadSmallValue(elem.value, elem.typecode.Size())
}
intf := composeInterface(unsafe.Pointer(elem.typecode), val)
elem = Value{
typecode: v.typecode.elem(),
value: unsafe.Pointer(&intf),
}
}

if key.Kind() == String {
if vkey.Kind() == String {
if del {
hashmapStringDelete(v.pointer(), *(*string)(key.value))
} else {
Expand All @@ -2023,7 +2044,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
Expand All @@ -2043,8 +2064,11 @@ func (v Value) SetMapIndex(key, elem Value) {
hashmapBinarySet(v.pointer(), keyptr, elemptr)
}
} else {
// Compiler-generated hash/equal path.
keyptr := genericKeyPtr(vkey, key)

if del {
hashmapInterfaceDelete(v.pointer(), key.Interface())
hashmapGenericDelete(v.pointer(), keyptr)
} else {
var elemptr unsafe.Pointer
if elem.isIndirect() || elem.typecode.Size() > unsafe.Sizeof(uintptr(0)) {
Expand All @@ -2053,7 +2077,7 @@ func (v Value) SetMapIndex(key, elem Value) {
elemptr = unsafe.Pointer(&elem.value)
}

hashmapInterfaceSet(v.pointer(), key.Interface(), elemptr)
hashmapGenericSet(v.pointer(), keyptr, elemptr)
Comment thread
jakebailey marked this conversation as resolved.
}
}
}
Expand Down Expand Up @@ -2110,6 +2134,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 {
Expand All @@ -2118,7 +2145,6 @@ func MakeMapWithSize(typ Type, n int) Value {
const (
hashmapAlgorithmBinary uint8 = iota
hashmapAlgorithmString
hashmapAlgorithmInterface
)

if typ.Kind() != Map {
Expand All @@ -2132,18 +2158,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,
Expand Down
Loading
Loading