diff --git a/GNUmakefile b/GNUmakefile index aaaf130d9..1fe2a5a6c 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -344,6 +344,7 @@ TEST_PACKAGES_FAST = \ unicode \ unicode/utf16 \ unicode/utf8 \ + unique \ $(nil) # Assume this will go away before Go2, so only check minor version. diff --git a/loader/goroot.go b/loader/goroot.go index 739819bce..7325db5b4 100644 --- a/loader/goroot.go +++ b/loader/goroot.go @@ -251,6 +251,7 @@ func pathsToOverride(goMinor int, needsSyscallPackage bool) map[string]bool { "runtime/": false, "sync/": true, "testing/": true, + "unique/": false, } if goMinor >= 19 { diff --git a/src/unique/handle.go b/src/unique/handle.go new file mode 100644 index 000000000..67c925d2d --- /dev/null +++ b/src/unique/handle.go @@ -0,0 +1,74 @@ +// Package unique implements the upstream Go unique package for TinyGo. +// +// It is not a full implementation: while it should behave the same way, it +// doesn't free unreferenced uniqued objects. +package unique + +import ( + "sync" + "unsafe" +) + +var ( + // We use a two-level map because that way it's easier to store and retrieve + // values. + globalMap map[unsafe.Pointer]any // map value type is always map[T]Handle[T] + + globalMapMutex sync.Mutex +) + +// Unique handle for the given value. Comparing two handles is cheap. +type Handle[T comparable] struct { + value *T +} + +// Value returns a shallow copy of the T value that produced the Handle. +func (h Handle[T]) Value() T { + return *h.value +} + +// Make a new unqique handle for the given value. +func Make[T comparable](value T) Handle[T] { + // Very simple implementation of the unique package. This is much, *much* + // simpler than the upstream implementation. Sadly it's not possible to + // reuse the upstream version because it relies on implementation details of + // the upstream runtime. + // It probably isn't as efficient as the upstream version, but the first + // goal here is compatibility. If the performance is a problem, it can be + // optimized later. + + globalMapMutex.Lock() + + // The map isn't initialized at program startup (and after a test run), so + // create it. + if globalMap == nil { + globalMap = make(map[unsafe.Pointer]any) + } + + // Retrieve the type-specific map, creating it if not yet present. + typeptr, _ := decomposeInterface(value) + var typeSpecificMap map[T]Handle[T] + if typeSpecificMapValue, ok := globalMap[typeptr]; !ok { + typeSpecificMap = make(map[T]Handle[T]) + globalMap[typeptr] = typeSpecificMap + } else { + typeSpecificMap = typeSpecificMapValue.(map[T]Handle[T]) + } + + // Retrieve the handle for the value, creating it if it isn't created yet. + var handle Handle[T] + if h, ok := typeSpecificMap[value]; !ok { + var clone T = value + handle.value = &clone + typeSpecificMap[value] = handle + } else { + handle = h + } + + globalMapMutex.Unlock() + + return handle +} + +//go:linkname decomposeInterface runtime.decomposeInterface +func decomposeInterface(i interface{}) (unsafe.Pointer, unsafe.Pointer) diff --git a/src/unique/handle_test.go b/src/unique/handle_test.go new file mode 100644 index 000000000..864b11f23 --- /dev/null +++ b/src/unique/handle_test.go @@ -0,0 +1,76 @@ +// Copyright 2024 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +// This file is a copy of src/unique/handle_test.go in upstream Go, but with +// some parts removed that rely on Go runtime implementation details. + +package unique + +import ( + "fmt" + "reflect" + "testing" +) + +// Set up special types. Because the internal maps are sharded by type, +// this will ensure that we're not overlapping with other tests. +type testString string +type testIntArray [4]int +type testEface any +type testStringArray [3]string +type testStringStruct struct { + a string +} +type testStringStructArrayStruct struct { + s [2]testStringStruct +} +type testStruct struct { + z float64 + b string +} + +func TestHandle(t *testing.T) { + testHandle[testString](t, "foo") + testHandle[testString](t, "bar") + testHandle[testString](t, "") + testHandle[testIntArray](t, [4]int{7, 77, 777, 7777}) + //testHandle[testEface](t, nil) // requires Go 1.20 + testHandle[testStringArray](t, [3]string{"a", "b", "c"}) + testHandle[testStringStruct](t, testStringStruct{"x"}) + testHandle[testStringStructArrayStruct](t, testStringStructArrayStruct{ + s: [2]testStringStruct{testStringStruct{"y"}, testStringStruct{"z"}}, + }) + testHandle[testStruct](t, testStruct{0.5, "184"}) +} + +func testHandle[T comparable](t *testing.T, value T) { + name := reflect.TypeFor[T]().Name() + t.Run(fmt.Sprintf("%s/%#v", name, value), func(t *testing.T) { + t.Parallel() + + v0 := Make(value) + v1 := Make(value) + + if v0.Value() != v1.Value() { + t.Error("v0.Value != v1.Value") + } + if v0.Value() != value { + t.Errorf("v0.Value not %#v", value) + } + if v0 != v1 { + t.Error("v0 != v1") + } + + drainMaps(t) + }) +} + +// drainMaps ensures that the internal maps are drained. +func drainMaps(t *testing.T) { + t.Helper() + + globalMapMutex.Lock() + globalMap = nil + globalMapMutex.Unlock() +}