From 51527776364412d8cc9cf40e8de373ed848427a3 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 13 Jan 2023 17:52:18 +0000 Subject: [PATCH 1/5] compiler: add support for new unsafe slice/string functions This adds support for unsafe.SliceData, unsafe.String, and unsafe.SringData that were introduced in Go 1.20. --- compiler/asserts.go | 11 +++---- compiler/compiler.go | 40 ++++++++++++++++--------- compiler/compiler_test.go | 10 +++++++ compiler/testdata/go1.20.go | 15 ++++++++++ compiler/testdata/go1.20.ll | 58 +++++++++++++++++++++++++++++++++++++ src/runtime/panic.go | 7 +++-- 6 files changed, 120 insertions(+), 21 deletions(-) create mode 100644 compiler/testdata/go1.20.go create mode 100644 compiler/testdata/go1.20.ll diff --git a/compiler/asserts.go b/compiler/asserts.go index ba4824943e..0fb112e0bc 100644 --- a/compiler/asserts.go +++ b/compiler/asserts.go @@ -89,10 +89,11 @@ func (b *builder) createSliceToArrayPointerCheck(sliceLen llvm.Value, arrayLen i b.createRuntimeAssert(isLess, "slicetoarray", "sliceToArrayPointerPanic") } -// createUnsafeSliceCheck inserts a runtime check used for unsafe.Slice. This -// function must panic if the ptr/len parameters are invalid. -func (b *builder) createUnsafeSliceCheck(ptr, len llvm.Value, elementType llvm.Type, lenType *types.Basic) { - // From the documentation of unsafe.Slice: +// createUnsafeSliceStringCheck inserts a runtime check used for unsafe.Slice +// and unsafe.String. This function must panic if the ptr/len parameters are +// invalid. +func (b *builder) createUnsafeSliceStringCheck(name string, ptr, len llvm.Value, elementType llvm.Type, lenType *types.Basic) { + // From the documentation of unsafe.Slice and unsafe.String: // > At run time, if len is negative, or if ptr is nil and len is not // > zero, a run-time panic occurs. // However, in practice, it is also necessary to check that the length is @@ -117,7 +118,7 @@ func (b *builder) createUnsafeSliceCheck(ptr, len llvm.Value, elementType llvm.T lenIsNotZero := b.CreateICmp(llvm.IntNE, len, zero, "") assert := b.CreateAnd(ptrIsNil, lenIsNotZero, "") assert = b.CreateOr(assert, lenOutOfBounds, "") - b.createRuntimeAssert(assert, "unsafe.Slice", "unsafeSlicePanic") + b.createRuntimeAssert(assert, name, "unsafeSlicePanic") } // createChanBoundsCheck creates a bounds check before creating a new channel to diff --git a/compiler/compiler.go b/compiler/compiler.go index 4abfdd5595..aad5e0d9a2 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1646,20 +1646,20 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c case "Sizeof": // unsafe.Sizeof size := b.targetData.TypeAllocSize(argValues[0].Type()) return llvm.ConstInt(b.uintptrType, size, false), nil - case "Slice": // unsafe.Slice - // This creates a slice from a pointer and a length. + case "Slice", "String": // unsafe.Slice, unsafe.String + // This creates a slice or string from a pointer and a length. // Note that the exception mentioned in the documentation (if the // pointer and length are nil, the slice is also nil) is trivially // already the case. ptr := argValues[0] len := argValues[1] - slice := llvm.Undef(b.ctx.StructType([]llvm.Type{ - ptr.Type(), - b.uintptrType, - b.uintptrType, - }, false)) - elementType := b.getLLVMType(argTypes[0].Underlying().(*types.Pointer).Elem()) - b.createUnsafeSliceCheck(ptr, len, elementType, argTypes[1].Underlying().(*types.Basic)) + var elementType llvm.Type + if callName == "Slice" { + elementType = b.getLLVMType(argTypes[0].Underlying().(*types.Pointer).Elem()) + } else { + elementType = b.ctx.Int8Type() + } + b.createUnsafeSliceStringCheck("unsafe."+callName, ptr, len, elementType, argTypes[1].Underlying().(*types.Basic)) if len.Type().IntTypeWidth() < b.uintptrType.IntTypeWidth() { // Too small, zero-extend len. len = b.CreateZExt(len, b.uintptrType, "") @@ -1667,10 +1667,24 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c // Too big, truncate len. len = b.CreateTrunc(len, b.uintptrType, "") } - slice = b.CreateInsertValue(slice, ptr, 0, "") - slice = b.CreateInsertValue(slice, len, 1, "") - slice = b.CreateInsertValue(slice, len, 2, "") - return slice, nil + if callName == "Slice" { + slice := llvm.Undef(b.ctx.StructType([]llvm.Type{ + ptr.Type(), + b.uintptrType, + b.uintptrType, + }, false)) + slice = b.CreateInsertValue(slice, ptr, 0, "") + slice = b.CreateInsertValue(slice, len, 1, "") + slice = b.CreateInsertValue(slice, len, 2, "") + return slice, nil + } else { + str := llvm.Undef(b.getLLVMRuntimeType("_string")) + str = b.CreateInsertValue(str, argValues[0], 0, "") + str = b.CreateInsertValue(str, len, 1, "") + return str, nil + } + case "SliceData", "StringData": // unsafe.SliceData, unsafe.StringData + return b.CreateExtractValue(argValues[0], 0, "slice.data"), nil default: return llvm.Value{}, b.makeError(pos, "todo: builtin: "+callName) } diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index a5cd9c6bf2..f8221a08e7 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -9,6 +9,7 @@ import ( "testing" "github.com/tinygo-org/tinygo/compileopts" + "github.com/tinygo-org/tinygo/goenv" "github.com/tinygo-org/tinygo/loader" "tinygo.org/x/go-llvm" ) @@ -27,6 +28,12 @@ type testCase struct { func TestCompiler(t *testing.T) { t.Parallel() + // Determine Go minor version (e.g. 16 in go1.16.3). + _, goMinor, err := goenv.GetGorootVersion(goenv.Get("GOROOT")) + if err != nil { + t.Fatal("could not read Go version:", err) + } + // Determine which tests to run, depending on the Go and LLVM versions. tests := []testCase{ {"basic.go", "", ""}, @@ -43,6 +50,9 @@ func TestCompiler(t *testing.T) { {"channel.go", "", ""}, {"gc.go", "", ""}, } + if goMinor >= 20 { + tests = append(tests, testCase{"go1.20.go", "", ""}) + } for _, tc := range tests { name := tc.file diff --git a/compiler/testdata/go1.20.go b/compiler/testdata/go1.20.go new file mode 100644 index 0000000000..be65363ecc --- /dev/null +++ b/compiler/testdata/go1.20.go @@ -0,0 +1,15 @@ +package main + +import "unsafe" + +func unsafeSliceData(s []int) *int { + return unsafe.SliceData(s) +} + +func unsafeString(ptr *byte, len int16) string { + return unsafe.String(ptr, len) +} + +func unsafeStringData(s string) *byte { + return unsafe.StringData(s) +} diff --git a/compiler/testdata/go1.20.ll b/compiler/testdata/go1.20.ll new file mode 100644 index 0000000000..fc66b47c58 --- /dev/null +++ b/compiler/testdata/go1.20.ll @@ -0,0 +1,58 @@ +; ModuleID = 'go1.20.go' +source_filename = "go1.20.go" +target datalayout = "e-m:e-p:32:32-p10:8:8-p20:8:8-i64:64-n32:64-S128-ni:1:10:20" +target triple = "wasm32-unknown-wasi" + +%runtime._string = type { ptr, i32 } + +declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #0 + +declare void @runtime.trackPointer(ptr nocapture readonly, ptr) #0 + +; Function Attrs: nounwind +define hidden void @main.init(ptr %context) unnamed_addr #1 { +entry: + ret void +} + +; Function Attrs: nounwind +define hidden ptr @main.unsafeSliceData(ptr %s.data, i32 %s.len, i32 %s.cap, ptr %context) unnamed_addr #1 { +entry: + call void @runtime.trackPointer(ptr %s.data, ptr undef) #2 + ret ptr %s.data +} + +; Function Attrs: nounwind +define hidden %runtime._string @main.unsafeString(ptr dereferenceable_or_null(1) %ptr, i16 %len, ptr %context) unnamed_addr #1 { +entry: + %0 = icmp slt i16 %len, 0 + %1 = icmp eq ptr %ptr, null + %2 = icmp ne i16 %len, 0 + %3 = and i1 %1, %2 + %4 = or i1 %3, %0 + br i1 %4, label %unsafe.String.throw, label %unsafe.String.next + +unsafe.String.next: ; preds = %entry + %5 = zext i16 %len to i32 + %6 = insertvalue %runtime._string undef, ptr %ptr, 0 + %7 = insertvalue %runtime._string %6, i32 %5, 1 + call void @runtime.trackPointer(ptr %ptr, ptr undef) #2 + ret %runtime._string %7 + +unsafe.String.throw: ; preds = %entry + call void @runtime.unsafeSlicePanic(ptr undef) #2 + unreachable +} + +declare void @runtime.unsafeSlicePanic(ptr) #0 + +; Function Attrs: nounwind +define hidden ptr @main.unsafeStringData(ptr %s.data, i32 %s.len, ptr %context) unnamed_addr #1 { +entry: + call void @runtime.trackPointer(ptr %s.data, ptr undef) #2 + ret ptr %s.data +} + +attributes #0 = { "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } +attributes #1 = { nounwind "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } +attributes #2 = { nounwind } diff --git a/src/runtime/panic.go b/src/runtime/panic.go index d7ba9ded8b..c9c69363bd 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -143,10 +143,11 @@ func sliceToArrayPointerPanic() { runtimePanic("slice smaller than array") } -// Panic when calling unsafe.Slice() (Go 1.17+) with a len that's too large -// (which includes if the ptr is nil and len is nonzero). +// Panic when calling unsafe.Slice() (Go 1.17+) or unsafe.String() (Go 1.20+) +// with a len that's too large (which includes if the ptr is nil and len is +// nonzero). func unsafeSlicePanic() { - runtimePanic("unsafe.Slice: len out of range") + runtimePanic("unsafe.Slice/String: len out of range") } // Panic when trying to create a new channel that is too big. From 3a5068f26e18b182e8361bb705d62c23a0b68419 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 13 Jan 2023 19:54:05 +0100 Subject: [PATCH 2/5] runtime: implement math/rand.fastrand64 to fix linker error This is needed for Go 1.20 support. --- src/runtime/algorithm.go | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/runtime/algorithm.go b/src/runtime/algorithm.go index 35e2e26013..11b39200f3 100644 --- a/src/runtime/algorithm.go +++ b/src/runtime/algorithm.go @@ -7,6 +7,14 @@ import ( "unsafe" ) +// This function is needed by math/rand since Go 1.20. +// See: https://github.com/golang/go/issues/54880 +// +//go:linkname rand_fastrand64 math/rand.fastrand64 +func rand_fastrand64() uint64 { + return fastrand64() +} + // This function is used by hash/maphash. func fastrand() uint32 { xorshift32State = xorshift32(xorshift32State) From 75897e7923ac56e5fdc6f26d7c8469fc75b794d4 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 13 Jan 2023 20:02:54 +0100 Subject: [PATCH 3/5] runtime: implement internal/godebug.setUpdate as a stub This function provides a mechanism to watch for changes to the GODEBUG environment variable. For now, we'll not implement it. It might be useful in the future, when it can always be added. --- src/runtime/runtime.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index eecedee22a..1a2a0495e4 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -95,3 +95,9 @@ func KeepAlive(x interface{}) { func SetFinalizer(obj interface{}, finalizer interface{}) { // Unimplemented. } + +//go:linkname godebug_setUpdate internal/godebug.setUpdate +func godebug_setUpdate(update func(string, string)) { + // Unimplemented. The 'update' function needs to be called whenever the + // GODEBUG environment variable changes (for example, via os.Setenv). +} From b67829532744309c35824f55b85745cde2f6d725 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Sat, 14 Jan 2023 23:10:44 +0100 Subject: [PATCH 4/5] syscall: implement setenv/unsetenv in the runtime This is expected starting with Go 1.20. I've also applied the same modification to syscall_libc.go so that setenv is only called in a single place. --- src/runtime/{env_linux.go => env_unix.go} | 32 ++++++++++++++++------- src/syscall/syscall_libc.go | 27 +++++-------------- 2 files changed, 28 insertions(+), 31 deletions(-) rename src/runtime/{env_linux.go => env_unix.go} (67%) diff --git a/src/runtime/env_linux.go b/src/runtime/env_unix.go similarity index 67% rename from src/runtime/env_linux.go rename to src/runtime/env_unix.go index 2581ab8e29..707162048c 100644 --- a/src/runtime/env_linux.go +++ b/src/runtime/env_unix.go @@ -1,28 +1,40 @@ -//go:build linux +//go:build linux || darwin package runtime // Update the C environment if cgo is loaded. -// Called from syscall.Setenv. +// Called from Go 1.20 and above. // -//go:linkname syscall_setenv_c syscall.setenv_c -func syscall_setenv_c(key string, val string) { +//go:linkname syscallSetenv syscall.runtimeSetenv +func syscallSetenv(key, value string) { keydata := cstring(key) - valdata := cstring(val) + valdata := cstring(value) // ignore any errors libc_setenv(&keydata[0], &valdata[0], 1) - return } // Update the C environment if cgo is loaded. -// Called from syscall.Unsetenv. +// Called from Go 1.20 and above. // -//go:linkname syscall_unsetenv_c syscall.unsetenv_c -func syscall_unsetenv_c(key string) { +//go:linkname syscallUnsetenv syscall.runtimeUnsetenv +func syscallUnsetenv(key string) { keydata := cstring(key) // ignore any errors libc_unsetenv(&keydata[0]) - return +} + +// Compatibility with Go 1.19 and below. +// +//go:linkname syscall_setenv_c syscall.setenv_c +func syscall_setenv_c(key string, val string) { + syscallSetenv(key, val) +} + +// Compatibility with Go 1.19 and below. +// +//go:linkname syscall_unsetenv_c syscall.unsetenv_c +func syscall_unsetenv_c(key string) { + syscallUnsetenv(key) } // cstring converts a Go string to a C string. diff --git a/src/syscall/syscall_libc.go b/src/syscall/syscall_libc.go index 002c9dc3f8..5c70171014 100644 --- a/src/syscall/syscall_libc.go +++ b/src/syscall/syscall_libc.go @@ -197,21 +197,12 @@ func Setenv(key, val string) (err error) { return EINVAL } } - keydata := cstring(key) - valdata := cstring(val) - errCode := libc_setenv(&keydata[0], &valdata[0], 1) - if errCode != 0 { - err = getErrno() - } + runtimeSetenv(key, val) return } func Unsetenv(key string) (err error) { - keydata := cstring(key) - errCode := libc_unsetenv(&keydata[0]) - if errCode != 0 { - err = getErrno() - } + runtimeUnsetenv(key) return } @@ -312,6 +303,10 @@ func splitSlice(p []byte) (buf *byte, len uintptr) { return slice.buf, slice.len } +// These two functions are provided by the runtime. +func runtimeSetenv(key, value string) +func runtimeUnsetenv(key string) + //export strlen func libc_strlen(ptr unsafe.Pointer) uintptr @@ -325,16 +320,6 @@ func libc_write(fd int32, buf *byte, count uint) int //export getenv func libc_getenv(name *byte) *byte -// int setenv(const char *name, const char *val, int replace); -// -//export setenv -func libc_setenv(name *byte, val *byte, replace int32) int32 - -// int unsetenv(const char *name); -// -//export unsetenv -func libc_unsetenv(name *byte) int32 - // ssize_t read(int fd, void *buf, size_t count); // //export read From 2fba94dec154f38d4b9436d74143c22792570047 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 13 Jan 2023 17:53:55 +0000 Subject: [PATCH 5/5] builder: add support for Go 1.20 Not all features work yet, but allow it to compile with this Go version. --- .circleci/config.yml | 8 ++++++++ Makefile | 1 - builder/config.go | 4 ++-- cgo/cgo_test.go | 15 +++++++++++---- cgo/testdata/errors.out.go | 4 ++-- testdata/stdlib.go | 2 +- 6 files changed, 24 insertions(+), 10 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 7a7e17d5a9..95e2a491b3 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -105,6 +105,13 @@ jobs: - test-linux: llvm: "14" resource_class: large + test-llvm15-go120: + docker: + - image: golang:1.20-rc-buster + steps: + - test-linux: + llvm: "15" + resource_class: large workflows: test-all: @@ -112,3 +119,4 @@ workflows: # This tests our lowest supported versions of Go and LLVM, to make sure at # least the smoke tests still pass. - test-llvm14-go118 + - test-llvm15-go120 diff --git a/Makefile b/Makefile index cc359d569d..97ba293cb6 100644 --- a/Makefile +++ b/Makefile @@ -286,7 +286,6 @@ TEST_PACKAGES_FAST = \ container/list \ container/ring \ crypto/des \ - crypto/internal/subtle \ crypto/md5 \ crypto/rc4 \ crypto/sha1 \ diff --git a/builder/config.go b/builder/config.go index c55ce1a21e..a7df83f18e 100644 --- a/builder/config.go +++ b/builder/config.go @@ -33,8 +33,8 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) { if err != nil { return nil, fmt.Errorf("could not read version from GOROOT (%v): %v", goroot, err) } - if major != 1 || minor < 18 || minor > 19 { - return nil, fmt.Errorf("requires go version 1.18 through 1.19, got go%d.%d", major, minor) + if major != 1 || minor < 18 || minor > 20 { + return nil, fmt.Errorf("requires go version 1.18 through 1.20, got go%d.%d", major, minor) } clangHeaderPath := getClangHeaderPath(goenv.Get("TINYGOROOT")) diff --git a/cgo/cgo_test.go b/cgo/cgo_test.go index e25da7a1dc..a44dd8e18b 100644 --- a/cgo/cgo_test.go +++ b/cgo/cgo_test.go @@ -11,6 +11,7 @@ import ( "go/types" "os" "path/filepath" + "regexp" "runtime" "strings" "testing" @@ -21,9 +22,15 @@ var flagUpdate = flag.Bool("update", false, "Update images based on test output. // normalizeResult normalizes Go source code that comes out of tests across // platforms and Go versions. -func normalizeResult(result string) string { - actual := strings.ReplaceAll(result, "\r\n", "\n") - return actual +func normalizeResult(t *testing.T, result string) string { + result = strings.ReplaceAll(result, "\r\n", "\n") + + // This changed to 'undefined:', in Go 1.20. + result = strings.ReplaceAll(result, ": undeclared name:", ": undefined:") + // Go 1.20 added a bit more detail + result = regexp.MustCompile(`(unknown field z in struct literal).*`).ReplaceAllString(result, "$1") + + return result } func TestCGo(t *testing.T) { @@ -88,7 +95,7 @@ func TestCGo(t *testing.T) { if err != nil { t.Errorf("could not write out CGo AST: %v", err) } - actual := normalizeResult(buf.String()) + actual := normalizeResult(t, buf.String()) // Read the file with the expected output, to compare against. outfile := filepath.Join("testdata", name+".out.go") diff --git a/cgo/testdata/errors.out.go b/cgo/testdata/errors.out.go index b15a26b668..716fd771bb 100644 --- a/cgo/testdata/errors.out.go +++ b/cgo/testdata/errors.out.go @@ -8,9 +8,9 @@ // Type checking errors after CGo processing: // testdata/errors.go:102: cannot use 2 << 10 (untyped int constant 2048) as C.char value in variable declaration (overflows) // testdata/errors.go:105: unknown field z in struct literal -// testdata/errors.go:108: undeclared name: C.SOME_CONST_1 +// testdata/errors.go:108: undefined: C.SOME_CONST_1 // testdata/errors.go:110: cannot use C.SOME_CONST_3 (untyped int constant 1234) as byte value in variable declaration (overflows) -// testdata/errors.go:112: undeclared name: C.SOME_CONST_4 +// testdata/errors.go:112: undefined: C.SOME_CONST_4 package main diff --git a/testdata/stdlib.go b/testdata/stdlib.go index bc59d20c0a..f051eaf028 100644 --- a/testdata/stdlib.go +++ b/testdata/stdlib.go @@ -24,7 +24,7 @@ func main() { syscall.Getppid() // package math/rand - fmt.Println("pseudorandom number:", rand.Int31()) + fmt.Println("pseudorandom number:", rand.New(rand.NewSource(1)).Int31()) // package strings fmt.Println("strings.IndexByte:", strings.IndexByte("asdf", 'd'))