From be3c8d6d1bb187a613756933b1dcdef9001ef271 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 6 Jul 2023 12:01:52 +0200 Subject: [PATCH 1/9] all: Go 1.21 support --- .github/workflows/build-macos.yml | 4 ++-- .github/workflows/linux.yml | 10 +++++----- .github/workflows/windows.yml | 8 ++++---- builder/config.go | 2 +- compiler/symbol.go | 2 +- src/internal/bytealg/bytealg.go | 10 ++++++++++ src/runtime/baremetal.go | 2 ++ src/runtime/runtime.go | 24 ++++++++++++++++++++++++ src/runtime/runtime_nintendoswitch.go | 2 +- src/runtime/runtime_wasm_js.go | 4 ++-- src/runtime/symtab.go | 5 ++++- targets/wasm_exec.js | 6 +++++- 12 files changed, 61 insertions(+), 18 deletions(-) diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml index 51c145f337..73dcda07a5 100644 --- a/.github/workflows/build-macos.yml +++ b/.github/workflows/build-macos.yml @@ -27,7 +27,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Restore LLVM source cache uses: actions/cache/restore@v3 @@ -126,7 +126,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Build TinyGo run: go install diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml index 05199e21ec..c77d280efb 100644 --- a/.github/workflows/linux.yml +++ b/.github/workflows/linux.yml @@ -18,7 +18,7 @@ jobs: # statically linked binary. runs-on: ubuntu-latest container: - image: golang:1.20-alpine + image: golang:1.21rc2-alpine steps: - name: Install apk dependencies # tar: needed for actions/cache@v3 @@ -135,7 +135,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Install wasmtime run: | @@ -177,7 +177,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Install Node.js uses: actions/setup-node@v3 @@ -290,7 +290,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Restore LLVM source cache uses: actions/cache/restore@v3 @@ -407,7 +407,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Restore LLVM source cache uses: actions/cache/restore@v3 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 55e752d707..161a4e57f3 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -35,7 +35,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Restore cached LLVM source uses: actions/cache/restore@v3 @@ -143,7 +143,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Download TinyGo build uses: actions/download-artifact@v2 @@ -173,7 +173,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Download TinyGo build uses: actions/download-artifact@v2 @@ -209,7 +209,7 @@ jobs: - name: Install Go uses: actions/setup-go@v3 with: - go-version: '1.20' + go-version: '1.21.0-rc.2' cache: true - name: Download TinyGo build uses: actions/download-artifact@v2 diff --git a/builder/config.go b/builder/config.go index 1ca3aa6414..82e8cb94e6 100644 --- a/builder/config.go +++ b/builder/config.go @@ -27,7 +27,7 @@ func NewConfig(options *compileopts.Options) (*compileopts.Config, error) { if err != nil { return nil, err } - if major != 1 || minor < 18 || minor > 20 { + if major != 1 || minor < 18 || minor > 21 { // Note: when this gets updated, also update the Go compatibility matrix: // https://github.com/tinygo-org/tinygo-site/blob/dev/content/docs/reference/go-compat-matrix.md return nil, fmt.Errorf("requires go version 1.18 through 1.20, got go%d.%d", major, minor) diff --git a/compiler/symbol.go b/compiler/symbol.go index 6426604ec1..cf9d43f7cb 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -367,7 +367,7 @@ func (c *compilerContext) parsePragmas(info *functionInfo, f *ssa.Function) { // The list of allowed types is based on this proposal: // https://github.com/golang/go/issues/59149 func (c *compilerContext) checkWasmImport(f *ssa.Function, pragma string) { - if c.pkg.Path() == "runtime" { + if c.pkg.Path() == "runtime" || c.pkg.Path() == "syscall/js" { // The runtime is a special case. Allow all kinds of parameters // (importantly, including pointers). return diff --git a/src/internal/bytealg/bytealg.go b/src/internal/bytealg/bytealg.go index a6744c4144..72be9ac587 100644 --- a/src/internal/bytealg/bytealg.go +++ b/src/internal/bytealg/bytealg.go @@ -251,3 +251,13 @@ func IndexRabinKarp(s, substr string) int { } return -1 } + +// MakeNoZero makes a slice of length and capacity n without zeroing the bytes. +// It is the caller's responsibility to ensure uninitialized bytes +// do not leak to the end user. +func MakeNoZero(n int) []byte { + // Note: this does zero the buffer even though that's not necessary. + // For performance reasons we might want to change this (similar to the + // malloc function implemented in the runtime). + return make([]byte, n) +} diff --git a/src/runtime/baremetal.go b/src/runtime/baremetal.go index a51191bb95..173d0db25e 100644 --- a/src/runtime/baremetal.go +++ b/src/runtime/baremetal.go @@ -38,6 +38,8 @@ func growHeap() bool { //export malloc func libc_malloc(size uintptr) unsafe.Pointer { + // Note: this zeroes the returned buffer which is not necessary. + // The same goes for bytealg.MakeNoZero. return alloc(size, nil) } diff --git a/src/runtime/runtime.go b/src/runtime/runtime.go index ceb6a2240c..ac7cd25c93 100644 --- a/src/runtime/runtime.go +++ b/src/runtime/runtime.go @@ -100,3 +100,27 @@ func godebug_setUpdate(update func(string, string)) { // variable changes (for example, via os.Setenv). godebugUpdate = update } + +//go:linkname godebug_setNewIncNonDefault internal/godebug.setNewIncNonDefault +func godebug_setNewIncNonDefault(newIncNonDefault func(string) func()) { + // Dummy function necessary in Go 1.21. +} + +// Write to the given file descriptor. +// This is called from internal/godebug starting with Go 1.21, and only seems to +// be called with the stderr file descriptor. +func write(fd uintptr, p unsafe.Pointer, n int32) int32 { + if fd == 2 { // stderr + // Convert to a string, because we know that p won't change during the + // call to printstring. + // TODO: use unsafe.String instead once we require Go 1.20. + s := _string{ + ptr: (*byte)(p), + length: uintptr(n), + } + str := *(*string)(unsafe.Pointer(&s)) + printstring(str) + return n + } + return 0 +} diff --git a/src/runtime/runtime_nintendoswitch.go b/src/runtime/runtime_nintendoswitch.go index f2606023ff..bcfa5151d0 100644 --- a/src/runtime/runtime_nintendoswitch.go +++ b/src/runtime/runtime_nintendoswitch.go @@ -105,7 +105,7 @@ func abort() { } //export write -func write(fd int32, buf *byte, count int) int { +func libc_write(fd int32, buf *byte, count int) int { // TODO: Proper handling write for i := 0; i < count; i++ { putchar(*buf) diff --git a/src/runtime/runtime_wasm_js.go b/src/runtime/runtime_wasm_js.go index c735c76fc1..443ed9e2ea 100644 --- a/src/runtime/runtime_wasm_js.go +++ b/src/runtime/runtime_wasm_js.go @@ -44,8 +44,8 @@ func nanosecondsToTicks(ns int64) timeUnit { // This function is called by the scheduler. // Schedule a call to runtime.scheduler, do not actually sleep. // -//export runtime.sleepTicks +//go:wasmimport gojs runtime.sleepTicks func sleepTicks(d timeUnit) -//export runtime.ticks +//go:wasmimport gojs runtime.ticks func ticks() timeUnit diff --git a/src/runtime/symtab.go b/src/runtime/symtab.go index cfc9885d7f..37c21ee895 100644 --- a/src/runtime/symtab.go +++ b/src/runtime/symtab.go @@ -5,11 +5,14 @@ type Frames struct { } type Frame struct { + PC uintptr + + Func *Func + Function string File string Line int - PC uintptr } func CallersFrames(callers []uintptr) *Frames { diff --git a/targets/wasm_exec.js b/targets/wasm_exec.js index 8e29d6266c..5dfc67c357 100644 --- a/targets/wasm_exec.js +++ b/targets/wasm_exec.js @@ -284,7 +284,7 @@ return 0; }, }, - env: { + gojs: { // func ticks() float64 "runtime.ticks": () => { return timeOrigin + performance.now(); @@ -444,6 +444,10 @@ }, } }; + + // Go 1.20 uses 'env'. Go 1.21 uses 'gojs'. + // For compatibility, we use both as long as Go 1.20 is supported. + this.importObject.env = this.importObject.gojs; } async run(instance) { From 1fe8e61ef39d8b0284c7514313bed0bcf95a7a83 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 6 Jul 2023 22:14:55 +0200 Subject: [PATCH 2/9] compiler: add min and max builtin support --- compiler/compiler.go | 18 +++++ compiler/compiler_test.go | 3 + compiler/testdata/go1.21.go | 53 +++++++++++++ compiler/testdata/go1.21.ll | 152 ++++++++++++++++++++++++++++++++++++ main_test.go | 10 +++ testdata/go1.21.go | 12 +++ testdata/go1.21.txt | 2 + 7 files changed, 250 insertions(+) create mode 100644 compiler/testdata/go1.21.go create mode 100644 compiler/testdata/go1.21.ll create mode 100644 testdata/go1.21.go create mode 100644 testdata/go1.21.txt diff --git a/compiler/compiler.go b/compiler/compiler.go index 09f11e614f..46c3a0672e 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1637,6 +1637,24 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c llvmLen = b.CreateZExt(llvmLen, b.intType, "len.int") } return llvmLen, nil + case "min", "max": + // min and max builtins, added in Go 1.21. + // We can simply reuse the existing binop comparison code, which has all + // the edge cases figured out already. + tok := token.LSS + if callName == "max" { + tok = token.GTR + } + result := argValues[0] + typ := argTypes[0] + for _, arg := range argValues[1:] { + cmp, err := b.createBinOp(tok, typ, typ, result, arg, pos) + if err != nil { + return result, err + } + result = b.CreateSelect(cmp, result, arg, "") + } + return result, nil case "print", "println": for i, value := range argValues { if i >= 1 && callName == "println" { diff --git a/compiler/compiler_test.go b/compiler/compiler_test.go index 9675c40280..92ce31b012 100644 --- a/compiler/compiler_test.go +++ b/compiler/compiler_test.go @@ -54,6 +54,9 @@ func TestCompiler(t *testing.T) { if goMinor >= 20 { tests = append(tests, testCase{"go1.20.go", "", ""}) } + if goMinor >= 21 { + tests = append(tests, testCase{"go1.21.go", "", ""}) + } for _, tc := range tests { name := tc.file diff --git a/compiler/testdata/go1.21.go b/compiler/testdata/go1.21.go new file mode 100644 index 0000000000..5541b489d1 --- /dev/null +++ b/compiler/testdata/go1.21.go @@ -0,0 +1,53 @@ +package main + +func min1(a int) int { + return min(a) +} + +func min2(a, b int) int { + return min(a, b) +} + +func min3(a, b, c int) int { + return min(a, b, c) +} + +func min4(a, b, c, d int) int { + return min(a, b, c, d) +} + +func minUint8(a, b uint8) uint8 { + return min(a, b) +} + +func minUnsigned(a, b uint) uint { + return min(a, b) +} + +func minFloat32(a, b float32) float32 { + return min(a, b) +} + +func minFloat64(a, b float64) float64 { + return min(a, b) +} + +func minString(a, b string) string { + return min(a, b) +} + +func maxInt(a, b int) int { + return max(a, b) +} + +func maxUint(a, b uint) uint { + return max(a, b) +} + +func maxFloat32(a, b float32) float32 { + return max(a, b) +} + +func maxString(a, b string) string { + return max(a, b) +} diff --git a/compiler/testdata/go1.21.ll b/compiler/testdata/go1.21.ll new file mode 100644 index 0000000000..5d4a70197e --- /dev/null +++ b/compiler/testdata/go1.21.ll @@ -0,0 +1,152 @@ +; ModuleID = 'go1.21.go' +source_filename = "go1.21.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 } + +; Function Attrs: allockind("alloc,zeroed") allocsize(0) +declare noalias nonnull ptr @runtime.alloc(i32, ptr, ptr) #0 + +declare void @runtime.trackPointer(ptr nocapture readonly, ptr, ptr) #1 + +; Function Attrs: nounwind +define hidden void @main.init(ptr %context) unnamed_addr #2 { +entry: + ret void +} + +; Function Attrs: nounwind +define hidden i32 @main.min1(i32 %a, ptr %context) unnamed_addr #2 { +entry: + ret i32 %a +} + +; Function Attrs: nounwind +define hidden i32 @main.min2(i32 %a, i32 %b, ptr %context) unnamed_addr #2 { +entry: + %0 = call i32 @llvm.smin.i32(i32 %a, i32 %b) + ret i32 %0 +} + +; Function Attrs: nounwind +define hidden i32 @main.min3(i32 %a, i32 %b, i32 %c, ptr %context) unnamed_addr #2 { +entry: + %0 = call i32 @llvm.smin.i32(i32 %a, i32 %b) + %1 = call i32 @llvm.smin.i32(i32 %0, i32 %c) + ret i32 %1 +} + +; Function Attrs: nounwind +define hidden i32 @main.min4(i32 %a, i32 %b, i32 %c, i32 %d, ptr %context) unnamed_addr #2 { +entry: + %0 = call i32 @llvm.smin.i32(i32 %a, i32 %b) + %1 = call i32 @llvm.smin.i32(i32 %0, i32 %c) + %2 = call i32 @llvm.smin.i32(i32 %1, i32 %d) + ret i32 %2 +} + +; Function Attrs: nounwind +define hidden i8 @main.minUint8(i8 %a, i8 %b, ptr %context) unnamed_addr #2 { +entry: + %0 = call i8 @llvm.umin.i8(i8 %a, i8 %b) + ret i8 %0 +} + +; Function Attrs: nounwind +define hidden i32 @main.minUnsigned(i32 %a, i32 %b, ptr %context) unnamed_addr #2 { +entry: + %0 = call i32 @llvm.umin.i32(i32 %a, i32 %b) + ret i32 %0 +} + +; Function Attrs: nounwind +define hidden float @main.minFloat32(float %a, float %b, ptr %context) unnamed_addr #2 { +entry: + %0 = fcmp olt float %a, %b + %1 = select i1 %0, float %a, float %b + ret float %1 +} + +; Function Attrs: nounwind +define hidden double @main.minFloat64(double %a, double %b, ptr %context) unnamed_addr #2 { +entry: + %0 = fcmp olt double %a, %b + %1 = select i1 %0, double %a, double %b + ret double %1 +} + +; Function Attrs: nounwind +define hidden %runtime._string @main.minString(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr %context) unnamed_addr #2 { +entry: + %0 = insertvalue %runtime._string zeroinitializer, ptr %a.data, 0 + %1 = insertvalue %runtime._string %0, i32 %a.len, 1 + %2 = insertvalue %runtime._string zeroinitializer, ptr %b.data, 0 + %3 = insertvalue %runtime._string %2, i32 %b.len, 1 + %stackalloc = alloca i8, align 1 + %4 = call i1 @runtime.stringLess(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr undef) #4 + %5 = select i1 %4, %runtime._string %1, %runtime._string %3 + %6 = extractvalue %runtime._string %5, 0 + call void @runtime.trackPointer(ptr %6, ptr nonnull %stackalloc, ptr undef) #4 + ret %runtime._string %5 +} + +declare i1 @runtime.stringLess(ptr, i32, ptr, i32, ptr) #1 + +; Function Attrs: nounwind +define hidden i32 @main.maxInt(i32 %a, i32 %b, ptr %context) unnamed_addr #2 { +entry: + %0 = call i32 @llvm.smax.i32(i32 %a, i32 %b) + ret i32 %0 +} + +; Function Attrs: nounwind +define hidden i32 @main.maxUint(i32 %a, i32 %b, ptr %context) unnamed_addr #2 { +entry: + %0 = call i32 @llvm.umax.i32(i32 %a, i32 %b) + ret i32 %0 +} + +; Function Attrs: nounwind +define hidden float @main.maxFloat32(float %a, float %b, ptr %context) unnamed_addr #2 { +entry: + %0 = fcmp ogt float %a, %b + %1 = select i1 %0, float %a, float %b + ret float %1 +} + +; Function Attrs: nounwind +define hidden %runtime._string @main.maxString(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr %context) unnamed_addr #2 { +entry: + %0 = insertvalue %runtime._string zeroinitializer, ptr %a.data, 0 + %1 = insertvalue %runtime._string %0, i32 %a.len, 1 + %2 = insertvalue %runtime._string zeroinitializer, ptr %b.data, 0 + %3 = insertvalue %runtime._string %2, i32 %b.len, 1 + %stackalloc = alloca i8, align 1 + %4 = call i1 @runtime.stringLess(ptr %b.data, i32 %b.len, ptr %a.data, i32 %a.len, ptr undef) #4 + %5 = select i1 %4, %runtime._string %1, %runtime._string %3 + %6 = extractvalue %runtime._string %5, 0 + call void @runtime.trackPointer(ptr %6, ptr nonnull %stackalloc, ptr undef) #4 + ret %runtime._string %5 +} + +; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn +declare i32 @llvm.smin.i32(i32, i32) #3 + +; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn +declare i8 @llvm.umin.i8(i8, i8) #3 + +; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn +declare i32 @llvm.umin.i32(i32, i32) #3 + +; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn +declare i32 @llvm.smax.i32(i32, i32) #3 + +; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn +declare i32 @llvm.umax.i32(i32, i32) #3 + +attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } +attributes #1 = { "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } +attributes #2 = { nounwind "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } +attributes #3 = { nocallback nofree nosync nounwind readnone speculatable willreturn } +attributes #4 = { nounwind } diff --git a/main_test.go b/main_test.go index 59254656ee..32d13f9103 100644 --- a/main_test.go +++ b/main_test.go @@ -71,6 +71,16 @@ func TestBuild(t *testing.T) { "zeroalloc.go", } + // Go 1.21 made some changes to the language, which we can only test when + // we're actually on Go 1.21. + _, minor, err := goenv.GetGorootVersion() + if err != nil { + t.Fatal("could not get version:", minor) + } + if minor >= 21 { + tests = append(tests, "go1.21.go") + } + if *testTarget != "" { // This makes it possible to run one specific test (instead of all), // which is especially useful to quickly check whether some changes diff --git a/testdata/go1.21.go b/testdata/go1.21.go new file mode 100644 index 0000000000..885e588da8 --- /dev/null +++ b/testdata/go1.21.go @@ -0,0 +1,12 @@ +package main + +func main() { + ia := 1 + ib := 5 + ic := -3 + fa := 1.0 + fb := 5.0 + fc := -3.0 + println("min/max:", min(ia, ib, ic), max(ia, ib, ic)) + println("min/max:", min(fa, fb, fc), max(fa, fb, fc)) +} diff --git a/testdata/go1.21.txt b/testdata/go1.21.txt new file mode 100644 index 0000000000..ad81dcfe96 --- /dev/null +++ b/testdata/go1.21.txt @@ -0,0 +1,2 @@ +min/max: -3 5 +min/max: -3.000000e+000 +5.000000e+000 From 0a05e613cdbd3cfa005a232687894df6353e9887 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 7 Jul 2023 15:15:49 +0200 Subject: [PATCH 3/9] compiler: implement clear builtin for slices --- compiler/compiler.go | 35 ++++++++++++++++++++++++++++++++ compiler/intrinsics.go | 24 +++++++++++++--------- compiler/testdata/go1.21.go | 8 ++++++++ compiler/testdata/go1.21.ll | 40 +++++++++++++++++++++++++++---------- testdata/go1.21.go | 6 ++++++ testdata/go1.21.txt | 1 + 6 files changed, 94 insertions(+), 20 deletions(-) diff --git a/compiler/compiler.go b/compiler/compiler.go index 46c3a0672e..6af6debe89 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1600,6 +1600,41 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c cplx = b.CreateInsertValue(cplx, r, 0, "") cplx = b.CreateInsertValue(cplx, i, 1, "") return cplx, nil + case "clear": + value := argValues[0] + switch typ := argTypes[0].Underlying().(type) { + case *types.Slice: + elementType := b.getLLVMType(typ.Elem()) + elementSize := b.targetData.TypeAllocSize(elementType) + elementAlign := b.targetData.ABITypeAlignment(elementType) + + // The pointer to the data to be cleared. + llvmBuf := b.CreateExtractValue(value, 0, "buf") + if llvmBuf.Type() != b.i8ptrType { // compatibility with LLVM 14 + llvmBuf = b.CreateBitCast(llvmBuf, b.i8ptrType, "") + } + + // The length (in bytes) to be cleared. + llvmLen := b.CreateExtractValue(value, 1, "len") + llvmLen = b.CreateMul(llvmLen, llvm.ConstInt(llvmLen.Type(), elementSize, false), "") + + // Do the clear operation using the LLVM memset builtin. + // This is also correct for nil slices: in those cases, len will be + // 0 which means the memset call is a no-op (according to the LLVM + // LangRef). + memset := b.getMemsetFunc() + call := b.createCall(memset.GlobalValueType(), memset, []llvm.Value{ + llvmBuf, // dest + llvm.ConstInt(b.ctx.Int8Type(), 0, false), // val + llvmLen, // len + llvm.ConstInt(b.ctx.Int1Type(), 0, false), // isVolatile + }, "") + call.AddCallSiteAttribute(1, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("align"), uint64(elementAlign))) + + return llvm.Value{}, nil + default: + return llvm.Value{}, b.makeError(pos, "unsupported type in clear builtin: "+typ.String()) + } case "copy": dst := argValues[0] src := argValues[1] diff --git a/compiler/intrinsics.go b/compiler/intrinsics.go index 5761a438f1..af3a57de1b 100644 --- a/compiler/intrinsics.go +++ b/compiler/intrinsics.go @@ -70,15 +70,7 @@ func (b *builder) createMemoryCopyImpl() { // regular libc memset calls if they aren't optimized out in a different way. func (b *builder) createMemoryZeroImpl() { b.createFunctionStart(true) - fnName := "llvm.memset.p0.i" + strconv.Itoa(b.uintptrType.IntTypeWidth()) - if llvmutil.Major() < 15 { // compatibility with LLVM 14 - fnName = "llvm.memset.p0i8.i" + strconv.Itoa(b.uintptrType.IntTypeWidth()) - } - llvmFn := b.mod.NamedFunction(fnName) - if llvmFn.IsNil() { - fnType := llvm.FunctionType(b.ctx.VoidType(), []llvm.Type{b.i8ptrType, b.ctx.Int8Type(), b.uintptrType, b.ctx.Int1Type()}, false) - llvmFn = llvm.AddFunction(b.mod, fnName, fnType) - } + llvmFn := b.getMemsetFunc() params := []llvm.Value{ b.getValue(b.fn.Params[0], getPos(b.fn)), llvm.ConstInt(b.ctx.Int8Type(), 0, false), @@ -89,6 +81,20 @@ func (b *builder) createMemoryZeroImpl() { b.CreateRetVoid() } +// Return the llvm.memset.p0.i8 function declaration. +func (c *compilerContext) getMemsetFunc() llvm.Value { + fnName := "llvm.memset.p0.i" + strconv.Itoa(c.uintptrType.IntTypeWidth()) + if llvmutil.Major() < 15 { // compatibility with LLVM 14 + fnName = "llvm.memset.p0i8.i" + strconv.Itoa(c.uintptrType.IntTypeWidth()) + } + llvmFn := c.mod.NamedFunction(fnName) + if llvmFn.IsNil() { + fnType := llvm.FunctionType(c.ctx.VoidType(), []llvm.Type{c.i8ptrType, c.ctx.Int8Type(), c.uintptrType, c.ctx.Int1Type()}, false) + llvmFn = llvm.AddFunction(c.mod, fnName, fnType) + } + return llvmFn +} + // createKeepAlive creates the runtime.KeepAlive function. It is implemented // using inline assembly. func (b *builder) createKeepAliveImpl() { diff --git a/compiler/testdata/go1.21.go b/compiler/testdata/go1.21.go index 5541b489d1..3d92b69b17 100644 --- a/compiler/testdata/go1.21.go +++ b/compiler/testdata/go1.21.go @@ -51,3 +51,11 @@ func maxFloat32(a, b float32) float32 { func maxString(a, b string) string { return max(a, b) } + +func clearSlice(s []int) { + clear(s) +} + +func clearZeroSizedSlice(s []struct{}) { + clear(s) +} diff --git a/compiler/testdata/go1.21.ll b/compiler/testdata/go1.21.ll index 5d4a70197e..73a3683839 100644 --- a/compiler/testdata/go1.21.ll +++ b/compiler/testdata/go1.21.ll @@ -84,10 +84,10 @@ entry: %2 = insertvalue %runtime._string zeroinitializer, ptr %b.data, 0 %3 = insertvalue %runtime._string %2, i32 %b.len, 1 %stackalloc = alloca i8, align 1 - %4 = call i1 @runtime.stringLess(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr undef) #4 + %4 = call i1 @runtime.stringLess(ptr %a.data, i32 %a.len, ptr %b.data, i32 %b.len, ptr undef) #5 %5 = select i1 %4, %runtime._string %1, %runtime._string %3 %6 = extractvalue %runtime._string %5, 0 - call void @runtime.trackPointer(ptr %6, ptr nonnull %stackalloc, ptr undef) #4 + call void @runtime.trackPointer(ptr %6, ptr nonnull %stackalloc, ptr undef) #5 ret %runtime._string %5 } @@ -123,30 +123,48 @@ entry: %2 = insertvalue %runtime._string zeroinitializer, ptr %b.data, 0 %3 = insertvalue %runtime._string %2, i32 %b.len, 1 %stackalloc = alloca i8, align 1 - %4 = call i1 @runtime.stringLess(ptr %b.data, i32 %b.len, ptr %a.data, i32 %a.len, ptr undef) #4 + %4 = call i1 @runtime.stringLess(ptr %b.data, i32 %b.len, ptr %a.data, i32 %a.len, ptr undef) #5 %5 = select i1 %4, %runtime._string %1, %runtime._string %3 %6 = extractvalue %runtime._string %5, 0 - call void @runtime.trackPointer(ptr %6, ptr nonnull %stackalloc, ptr undef) #4 + call void @runtime.trackPointer(ptr %6, ptr nonnull %stackalloc, ptr undef) #5 ret %runtime._string %5 } +; Function Attrs: nounwind +define hidden void @main.clearSlice(ptr %s.data, i32 %s.len, i32 %s.cap, ptr %context) unnamed_addr #2 { +entry: + %0 = shl i32 %s.len, 2 + call void @llvm.memset.p0.i32(ptr align 4 %s.data, i8 0, i32 %0, i1 false) + ret void +} + +; Function Attrs: argmemonly nocallback nofree nounwind willreturn writeonly +declare void @llvm.memset.p0.i32(ptr nocapture writeonly, i8, i32, i1 immarg) #3 + +; Function Attrs: nounwind +define hidden void @main.clearZeroSizedSlice(ptr %s.data, i32 %s.len, i32 %s.cap, ptr %context) unnamed_addr #2 { +entry: + ret void +} + ; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn -declare i32 @llvm.smin.i32(i32, i32) #3 +declare i32 @llvm.smin.i32(i32, i32) #4 ; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn -declare i8 @llvm.umin.i8(i8, i8) #3 +declare i8 @llvm.umin.i8(i8, i8) #4 ; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn -declare i32 @llvm.umin.i32(i32, i32) #3 +declare i32 @llvm.umin.i32(i32, i32) #4 ; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn -declare i32 @llvm.smax.i32(i32, i32) #3 +declare i32 @llvm.smax.i32(i32, i32) #4 ; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn -declare i32 @llvm.umax.i32(i32, i32) #3 +declare i32 @llvm.umax.i32(i32, i32) #4 attributes #0 = { allockind("alloc,zeroed") allocsize(0) "alloc-family"="runtime.alloc" "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } attributes #1 = { "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } attributes #2 = { nounwind "target-features"="+bulk-memory,+nontrapping-fptoint,+sign-ext" } -attributes #3 = { nocallback nofree nosync nounwind readnone speculatable willreturn } -attributes #4 = { nounwind } +attributes #3 = { argmemonly nocallback nofree nounwind willreturn writeonly } +attributes #4 = { nocallback nofree nosync nounwind readnone speculatable willreturn } +attributes #5 = { nounwind } diff --git a/testdata/go1.21.go b/testdata/go1.21.go index 885e588da8..184bb2d8a7 100644 --- a/testdata/go1.21.go +++ b/testdata/go1.21.go @@ -1,6 +1,7 @@ package main func main() { + // The new min/max builtins. ia := 1 ib := 5 ic := -3 @@ -9,4 +10,9 @@ func main() { fc := -3.0 println("min/max:", min(ia, ib, ic), max(ia, ib, ic)) println("min/max:", min(fa, fb, fc), max(fa, fb, fc)) + + // The clear builtin, for slices. + s := []int{1, 2, 3, 4, 5} + clear(s[:3]) + println("cleared s[:3]:", s[0], s[1], s[2], s[3], s[4]) } diff --git a/testdata/go1.21.txt b/testdata/go1.21.txt index ad81dcfe96..459631a302 100644 --- a/testdata/go1.21.txt +++ b/testdata/go1.21.txt @@ -1,2 +1,3 @@ min/max: -3 5 min/max: -3.000000e+000 +5.000000e+000 +cleared s[:3]: 0 0 0 4 5 From 3f10f2de0eb56dfc0fbe91fce1e176e108244e0e Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 7 Jul 2023 15:47:49 +0200 Subject: [PATCH 4/9] compiler: implement clear builtin for maps --- compiler/compiler.go | 4 ++++ compiler/map.go | 5 +++++ compiler/testdata/go1.21.go | 4 ++++ compiler/testdata/go1.21.ll | 9 +++++++++ src/runtime/hashmap.go | 29 +++++++++++++++++++++++++++++ testdata/go1.21.go | 11 +++++++++++ testdata/go1.21.txt | 2 ++ 7 files changed, 64 insertions(+) diff --git a/compiler/compiler.go b/compiler/compiler.go index 6af6debe89..6dd43935a6 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -1631,6 +1631,10 @@ func (b *builder) createBuiltin(argTypes []types.Type, argValues []llvm.Value, c }, "") call.AddCallSiteAttribute(1, b.ctx.CreateEnumAttribute(llvm.AttributeKindID("align"), uint64(elementAlign))) + return llvm.Value{}, nil + case *types.Map: + m := argValues[0] + b.createMapClear(m) return llvm.Value{}, nil default: return llvm.Value{}, b.makeError(pos, "unsupported type in clear builtin: "+typ.String()) diff --git a/compiler/map.go b/compiler/map.go index c3d4289023..21f0ee4a67 100644 --- a/compiler/map.go +++ b/compiler/map.go @@ -185,6 +185,11 @@ func (b *builder) createMapDelete(keyType types.Type, m, key llvm.Value, pos tok } } +// Clear the given map. +func (b *builder) createMapClear(m llvm.Value) { + b.createRuntimeCall("hashmapClear", []llvm.Value{m}, "") +} + // createMapIteratorNext lowers the *ssa.Next instruction for iterating over a // map. It returns a tuple of {bool, key, value} with the result of the // iteration. diff --git a/compiler/testdata/go1.21.go b/compiler/testdata/go1.21.go index 3d92b69b17..589486d024 100644 --- a/compiler/testdata/go1.21.go +++ b/compiler/testdata/go1.21.go @@ -59,3 +59,7 @@ func clearSlice(s []int) { func clearZeroSizedSlice(s []struct{}) { clear(s) } + +func clearMap(m map[string]int) { + clear(m) +} diff --git a/compiler/testdata/go1.21.ll b/compiler/testdata/go1.21.ll index 73a3683839..d65c75f4f0 100644 --- a/compiler/testdata/go1.21.ll +++ b/compiler/testdata/go1.21.ll @@ -147,6 +147,15 @@ entry: ret void } +; Function Attrs: nounwind +define hidden void @main.clearMap(ptr dereferenceable_or_null(40) %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 + ; Function Attrs: nocallback nofree nosync nounwind readnone speculatable willreturn declare i32 @llvm.smin.i32(i32, i32) #4 diff --git a/src/runtime/hashmap.go b/src/runtime/hashmap.go index 8a902a55d2..dfbec300ed 100644 --- a/src/runtime/hashmap.go +++ b/src/runtime/hashmap.go @@ -91,6 +91,35 @@ func hashmapMakeUnsafePointer(keySize, valueSize uintptr, sizeHint uintptr, alg return (unsafe.Pointer)(hashmapMake(keySize, valueSize, sizeHint, alg)) } +// Remove all entries from the map, without actually deallocating the space for +// it. This is used for the clear builtin, and can be used to reuse a map (to +// avoid extra heap allocations). +func hashmapClear(m *hashmap) { + if m == nil { + // Nothing to do. According to the spec: + // > If the map or slice is nil, clear is a no-op. + return + } + + m.count = 0 + numBuckets := uintptr(1) << m.bucketBits + bucketSize := hashmapBucketSize(m) + for i := uintptr(0); i < numBuckets; i++ { + bucket := hashmapBucketAddr(m, m.buckets, i) + for bucket != nil { + // Clear the tophash, to mark these keys/values as removed. + bucket.tophash = [8]uint8{} + + // Clear the keys and values in the bucket so that the GC won't pin + // these allocations. + memzero(unsafe.Add(unsafe.Pointer(bucket), unsafe.Sizeof(hashmapBucket{})), bucketSize-unsafe.Sizeof(hashmapBucket{})) + + // Move on to the next bucket in the chain. + bucket = bucket.next + } + } +} + func hashmapKeyEqualAlg(alg hashmapAlgorithm) func(x, y unsafe.Pointer, n uintptr) bool { switch alg { case hashmapAlgorithmBinary: diff --git a/testdata/go1.21.go b/testdata/go1.21.go index 184bb2d8a7..603bd06e27 100644 --- a/testdata/go1.21.go +++ b/testdata/go1.21.go @@ -15,4 +15,15 @@ func main() { s := []int{1, 2, 3, 4, 5} clear(s[:3]) println("cleared s[:3]:", s[0], s[1], s[2], s[3], s[4]) + + // The clear builtin, for maps. + m := map[int]string{ + 1: "one", + 2: "two", + 3: "three", + } + clear(m) + println("cleared map:", m[1], m[2], m[3], len(m)) + m[4] = "four" + println("added to cleared map:", m[1], m[2], m[3], m[4], len(m)) } diff --git a/testdata/go1.21.txt b/testdata/go1.21.txt index 459631a302..3edfdb4568 100644 --- a/testdata/go1.21.txt +++ b/testdata/go1.21.txt @@ -1,3 +1,5 @@ min/max: -3 5 min/max: -3.000000e+000 +5.000000e+000 cleared s[:3]: 0 0 0 4 5 +cleared map: 0 +added to cleared map: four 1 From 97fea1cb3a05477c3da08fe08661aeeaed6f09cf Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Fri, 7 Jul 2023 15:21:23 +0200 Subject: [PATCH 5/9] compiler: improve panic message when a runtime call is unavailable This should not happen under normal circumstances. It can still happen when there is a mismatch between TinyGo version and the associated runtime, or while developing the compiler package. --- compiler/calls.go | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/compiler/calls.go b/compiler/calls.go index 65a69fea3c..a110addcf6 100644 --- a/compiler/calls.go +++ b/compiler/calls.go @@ -36,7 +36,11 @@ const ( // createRuntimeCallCommon creates a runtime call. Use createRuntimeCall or // createRuntimeInvoke instead. func (b *builder) createRuntimeCallCommon(fnName string, args []llvm.Value, name string, isInvoke bool) llvm.Value { - fn := b.program.ImportedPackage("runtime").Members[fnName].(*ssa.Function) + member := b.program.ImportedPackage("runtime").Members[fnName] + if member == nil { + panic("unknown runtime call: " + fnName) + } + fn := member.(*ssa.Function) fnType, llvmFn := b.getFunction(fn) if llvmFn.IsNil() { panic("trying to call non-existent function: " + fn.RelString(nil)) From 9ecadddf3d828b2bc4522e1aba08b41f76899902 Mon Sep 17 00:00:00 2001 From: Patricio Whittingslow Date: Sat, 15 Jul 2023 11:24:53 -0300 Subject: [PATCH 6/9] machine.UART refactor (#3832) * add gosched calls to UART * add UART.flush() stubs for all supported architectures * add comment un uart.go on flush functionality * uart.writeByte as base of UART usage * fix NXP having duplicate WriteByte * fix writeByte not returning error on some platforms * add flush method for fe310 device * check for error in WriteByte call to writeByte --- src/machine/machine_atmega.go | 7 +++++-- src/machine/machine_atsamd21.go | 4 +++- src/machine/machine_atsamd51.go | 4 +++- src/machine/machine_esp32.go | 4 +++- src/machine/machine_esp32c3.go | 4 +++- src/machine/machine_esp8266.go | 6 ++++-- src/machine/machine_fe310.go | 5 ++++- src/machine/machine_k210.go | 5 ++++- src/machine/machine_mimxrt1062_uart.go | 4 +++- src/machine/machine_nrf.go | 4 +++- src/machine/machine_nxpmk66f18_uart.go | 4 +++- src/machine/machine_rp2040_uart.go | 9 ++++++++- src/machine/machine_stm32_uart.go | 4 +++- src/machine/uart.go | 22 +++++++++++++++++++--- 14 files changed, 68 insertions(+), 18 deletions(-) diff --git a/src/machine/machine_atmega.go b/src/machine/machine_atmega.go index 90977fb877..81a47c0dfa 100644 --- a/src/machine/machine_atmega.go +++ b/src/machine/machine_atmega.go @@ -97,7 +97,7 @@ func (i2c *I2C) stop() { } // writeByte writes a single byte to the I2C bus. -func (i2c *I2C) writeByte(data byte) { +func (i2c *I2C) writeByte(data byte) error { // Write data to register. avr.TWDR.Set(data) @@ -107,6 +107,7 @@ func (i2c *I2C) writeByte(data byte) { // Wait till data is transmitted. for !avr.TWCR.HasBits(avr.TWCR_TWINT) { } + return nil } // readByte reads a single byte from the I2C bus. @@ -190,7 +191,7 @@ func (uart *UART) handleInterrupt(intr interrupt.Interrupt) { } // WriteByte writes a byte of data to the UART. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { // Wait until UART buffer is not busy. for !uart.statusRegA.HasBits(avr.UCSR0A_UDRE0) { } @@ -198,6 +199,8 @@ func (uart *UART) WriteByte(c byte) error { return nil } +func (uart *UART) flush() {} + // SPIConfig is used to store config info for SPI. type SPIConfig struct { Frequency uint32 diff --git a/src/machine/machine_atsamd21.go b/src/machine/machine_atsamd21.go index 59df853e5c..443c7af56a 100644 --- a/src/machine/machine_atsamd21.go +++ b/src/machine/machine_atsamd21.go @@ -626,7 +626,7 @@ func (uart *UART) SetBaudRate(br uint32) { } // WriteByte writes a byte of data to the UART. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { // wait until ready to receive for !uart.Bus.INTFLAG.HasBits(sam.SERCOM_USART_INTFLAG_DRE) { } @@ -634,6 +634,8 @@ func (uart *UART) WriteByte(c byte) error { return nil } +func (uart *UART) flush() {} + // handleInterrupt should be called from the appropriate interrupt handler for // this UART instance. func (uart *UART) handleInterrupt(interrupt.Interrupt) { diff --git a/src/machine/machine_atsamd51.go b/src/machine/machine_atsamd51.go index de5491c884..7c4c4d07c7 100644 --- a/src/machine/machine_atsamd51.go +++ b/src/machine/machine_atsamd51.go @@ -1114,7 +1114,7 @@ func (uart *UART) SetBaudRate(br uint32) { } // WriteByte writes a byte of data to the UART. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { // wait until ready to receive for !uart.Bus.INTFLAG.HasBits(sam.SERCOM_USART_INT_INTFLAG_DRE) { } @@ -1122,6 +1122,8 @@ func (uart *UART) WriteByte(c byte) error { return nil } +func (uart *UART) flush() {} + func (uart *UART) handleInterrupt(interrupt.Interrupt) { // should reset IRQ uart.Receive(byte((uart.Bus.DATA.Get() & 0xFF))) diff --git a/src/machine/machine_esp32.go b/src/machine/machine_esp32.go index b58cef66ae..4491b3dd95 100644 --- a/src/machine/machine_esp32.go +++ b/src/machine/machine_esp32.go @@ -314,7 +314,7 @@ func (uart *UART) Configure(config UARTConfig) { uart.Bus.CLKDIV.Set(peripheralClock / config.BaudRate) } -func (uart *UART) WriteByte(b byte) error { +func (uart *UART) writeByte(b byte) error { for (uart.Bus.STATUS.Get()>>16)&0xff >= 128 { // Read UART_TXFIFO_CNT from the status register, which indicates how // many bytes there are in the transmit buffer. Wait until there are @@ -324,6 +324,8 @@ func (uart *UART) WriteByte(b byte) error { return nil } +func (uart *UART) flush() {} + // Serial Peripheral Interface on the ESP32. type SPI struct { Bus *esp.SPI_Type diff --git a/src/machine/machine_esp32c3.go b/src/machine/machine_esp32c3.go index f1f646fd5e..e447e4fe0d 100644 --- a/src/machine/machine_esp32c3.go +++ b/src/machine/machine_esp32c3.go @@ -493,7 +493,7 @@ func (uart *UART) enableReceiver() { uart.Bus.SetINT_ENA_RXFIFO_OVF_INT_ENA(1) } -func (uart *UART) WriteByte(b byte) error { +func (uart *UART) writeByte(b byte) error { for (uart.Bus.STATUS.Get()&esp.UART_STATUS_TXFIFO_CNT_Msk)>>esp.UART_STATUS_TXFIFO_CNT_Pos >= 128 { // Read UART_TXFIFO_CNT from the status register, which indicates how // many bytes there are in the transmit buffer. Wait until there are @@ -502,3 +502,5 @@ func (uart *UART) WriteByte(b byte) error { uart.Bus.FIFO.Set(uint32(b)) return nil } + +func (uart *UART) flush() {} diff --git a/src/machine/machine_esp8266.go b/src/machine/machine_esp8266.go index 37ab29a17d..4edd4a535f 100644 --- a/src/machine/machine_esp8266.go +++ b/src/machine/machine_esp8266.go @@ -181,12 +181,14 @@ func (uart *UART) Configure(config UARTConfig) { esp.UART0.UART_CLKDIV.Set(CPUFrequency() / config.BaudRate) } -// WriteByte writes a single byte to the output buffer. Note that the hardware +// writeByte writes a single byte to the output buffer. Note that the hardware // includes a buffer of 128 bytes which will be used first. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { for (esp.UART0.UART_STATUS.Get()>>16)&0xff >= 128 { // Wait until the TX buffer has room. } esp.UART0.UART_FIFO.Set(uint32(c)) return nil } + +func (uart *UART) flush() {} diff --git a/src/machine/machine_fe310.go b/src/machine/machine_fe310.go index 3a0c451349..85a2c5bd37 100644 --- a/src/machine/machine_fe310.go +++ b/src/machine/machine_fe310.go @@ -111,13 +111,16 @@ func (uart *UART) handleInterrupt(interrupt.Interrupt) { uart.Receive(c) } -func (uart *UART) WriteByte(c byte) { +func (uart *UART) writeByte(c byte) error { for sifive.UART0.TXDATA.Get()&sifive.UART_TXDATA_FULL != 0 { } sifive.UART0.TXDATA.Set(uint32(c)) + return nil } +func (uart *UART) flush() {} + // SPI on the FE310. The normal SPI0 is actually a quad-SPI meant for flash, so it is best // to use SPI1 or SPI2 port for most applications. type SPI struct { diff --git a/src/machine/machine_k210.go b/src/machine/machine_k210.go index 9c63a8f766..e8a304c850 100644 --- a/src/machine/machine_k210.go +++ b/src/machine/machine_k210.go @@ -392,13 +392,16 @@ func (uart *UART) handleInterrupt(interrupt.Interrupt) { uart.Receive(c) } -func (uart *UART) WriteByte(c byte) { +func (uart *UART) writeByte(c byte) error { for uart.Bus.TXDATA.Get()&kendryte.UARTHS_TXDATA_FULL != 0 { } uart.Bus.TXDATA.Set(uint32(c)) + return nil } +func (uart *UART) flush() {} + type SPI struct { Bus *kendryte.SPI_Type } diff --git a/src/machine/machine_mimxrt1062_uart.go b/src/machine/machine_mimxrt1062_uart.go index d24206dfb9..6265b85656 100644 --- a/src/machine/machine_mimxrt1062_uart.go +++ b/src/machine/machine_mimxrt1062_uart.go @@ -173,7 +173,7 @@ func (uart *UART) Sync() error { } // WriteByte writes a single byte of data to the UART interface. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { uart.startTransmitting() for !uart.txBuffer.Put(c) { } @@ -181,6 +181,8 @@ func (uart *UART) WriteByte(c byte) error { return nil } +func (uart *UART) flush() {} + // getBaudRateDivisor finds the greatest over-sampling factor (4..32) and // corresponding baud rate divisor (1..8191) that best partition a given baud // rate into equal intervals. diff --git a/src/machine/machine_nrf.go b/src/machine/machine_nrf.go index 83fa57b3e9..346a0bb2e9 100644 --- a/src/machine/machine_nrf.go +++ b/src/machine/machine_nrf.go @@ -186,7 +186,7 @@ func (uart *UART) SetBaudRate(br uint32) { } // WriteByte writes a byte of data to the UART. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { nrf.UART0.EVENTS_TXDRDY.Set(0) nrf.UART0.TXD.Set(uint32(c)) for nrf.UART0.EVENTS_TXDRDY.Get() == 0 { @@ -194,6 +194,8 @@ func (uart *UART) WriteByte(c byte) error { return nil } +func (uart *UART) flush() {} + func (uart *UART) handleInterrupt(interrupt.Interrupt) { if nrf.UART0.EVENTS_RXDRDY.Get() != 0 { uart.Receive(byte(nrf.UART0.RXD.Get())) diff --git a/src/machine/machine_nxpmk66f18_uart.go b/src/machine/machine_nxpmk66f18_uart.go index d62fc64e01..a14d18f5ef 100644 --- a/src/machine/machine_nxpmk66f18_uart.go +++ b/src/machine/machine_nxpmk66f18_uart.go @@ -292,7 +292,7 @@ func (u *UART) handleStatusInterrupt(interrupt.Interrupt) { } // WriteByte writes a byte of data to the UART. -func (u *UART) WriteByte(c byte) error { +func (u *UART) writeByte(c byte) error { if !u.Configured { return ErrNotConfigured } @@ -305,3 +305,5 @@ func (u *UART) WriteByte(c byte) error { u.C2.Set(uartC2TXActive) return nil } + +func (uart *UART) flush() {} diff --git a/src/machine/machine_rp2040_uart.go b/src/machine/machine_rp2040_uart.go index e5e4f77de3..1d927df128 100644 --- a/src/machine/machine_rp2040_uart.go +++ b/src/machine/machine_rp2040_uart.go @@ -86,9 +86,10 @@ func (uart *UART) SetBaudRate(br uint32) { } // WriteByte writes a byte of data to the UART. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { // wait until buffer is not full for uart.Bus.UARTFR.HasBits(rp.UART0_UARTFR_TXFF) { + gosched() } // write data @@ -96,6 +97,12 @@ func (uart *UART) WriteByte(c byte) error { return nil } +func (uart *UART) flush() { + for uart.Bus.UARTFR.HasBits(rp.UART0_UARTFR_BUSY) { + gosched() + } +} + // SetFormat for number of data bits, stop bits, and parity for the UART. func (uart *UART) SetFormat(databits, stopbits uint8, parity UARTParity) error { var pen, pev uint8 diff --git a/src/machine/machine_stm32_uart.go b/src/machine/machine_stm32_uart.go index 6ae6f0f994..6e8806c876 100644 --- a/src/machine/machine_stm32_uart.go +++ b/src/machine/machine_stm32_uart.go @@ -74,10 +74,12 @@ func (uart *UART) SetBaudRate(br uint32) { } // WriteByte writes a byte of data to the UART. -func (uart *UART) WriteByte(c byte) error { +func (uart *UART) writeByte(c byte) error { uart.txReg.Set(uint32(c)) for !uart.statusReg.HasBits(uart.txEmptyFlag) { } return nil } + +func (uart *UART) flush() {} diff --git a/src/machine/uart.go b/src/machine/uart.go index 37d3223324..eeeb7d6a0b 100644 --- a/src/machine/uart.go +++ b/src/machine/uart.go @@ -59,11 +59,27 @@ func (uart *UART) Read(data []byte) (n int, err error) { return size, nil } -// Write data to the UART. +// WriteByte writes a byte of data over the UART's Tx. +// This function blocks until the data is finished being sent. +func (uart *UART) WriteByte(c byte) error { + err := uart.writeByte(c) + if err != nil { + return err + } + uart.flush() // flush() blocks until all data has been transmitted. + return nil +} + +// Write data over the UART's Tx. +// This function blocks until the data is finished being sent. func (uart *UART) Write(data []byte) (n int, err error) { - for _, v := range data { - uart.WriteByte(v) + for i, v := range data { + err = uart.writeByte(v) + if err != nil { + return i, err + } } + uart.flush() // flush() blocks until all data has been transmitted. return len(data), nil } From ccd9ab7741e0da021c1678a5914c7ec0381f9792 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Thu, 13 Jul 2023 15:24:58 +0200 Subject: [PATCH 7/9] nrf: add I2C timeout This commit adds I2C timeouts for nrf51 and nrf52 (but not yet for others like nrf52840). Tested on the PineTime, where I now got a timeout instead of hanging and resetting due to a watchdog reset. --- src/machine/machine_nrf.go | 10 +++++++++- src/machine/machine_nrf5x.go | 17 ++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/machine/machine_nrf.go b/src/machine/machine_nrf.go index 346a0bb2e9..d7b87d9aec 100644 --- a/src/machine/machine_nrf.go +++ b/src/machine/machine_nrf.go @@ -203,6 +203,8 @@ func (uart *UART) handleInterrupt(interrupt.Interrupt) { } } +const i2cTimeout = 0xffff // this is around 29ms on a nrf52 + // I2CConfig is used to store config info for I2C. type I2CConfig struct { Frequency uint32 @@ -261,11 +263,17 @@ func (i2c *I2C) Configure(config I2CConfig) error { } // signalStop sends a stop signal to the I2C peripheral and waits for confirmation. -func (i2c *I2C) signalStop() { +func (i2c *I2C) signalStop() error { + tries := 0 i2c.Bus.TASKS_STOP.Set(1) for i2c.Bus.EVENTS_STOPPED.Get() == 0 { + tries++ + if tries >= i2cTimeout { + return errI2CSignalStopTimeout + } } i2c.Bus.EVENTS_STOPPED.Set(0) + return nil } var rngStarted = false diff --git a/src/machine/machine_nrf5x.go b/src/machine/machine_nrf5x.go index 8c012b49e8..4c731036be 100644 --- a/src/machine/machine_nrf5x.go +++ b/src/machine/machine_nrf5x.go @@ -72,12 +72,17 @@ func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) { if len(r) == 0 { // Stop the I2C transaction after the write. - i2c.signalStop() + err = i2c.signalStop() } else { // The last byte read has already stopped the transaction, via // TWI_SHORTS_BB_STOP. But we still need to wait until we receive the // STOPPED event. + tries := 0 for i2c.Bus.EVENTS_STOPPED.Get() == 0 { + tries++ + if tries >= i2cTimeout { + return errI2CSignalStopTimeout + } } i2c.Bus.EVENTS_STOPPED.Set(0) } @@ -87,12 +92,17 @@ func (i2c *I2C) Tx(addr uint16, w, r []byte) (err error) { // writeByte writes a single byte to the I2C bus and waits for confirmation. func (i2c *I2C) writeByte(data byte) error { + tries := 0 i2c.Bus.TXD.Set(uint32(data)) for i2c.Bus.EVENTS_TXDSENT.Get() == 0 { if e := i2c.Bus.EVENTS_ERROR.Get(); e != 0 { i2c.Bus.EVENTS_ERROR.Set(0) return errI2CBusError } + tries++ + if tries >= i2cTimeout { + return errI2CWriteTimeout + } } i2c.Bus.EVENTS_TXDSENT.Set(0) return nil @@ -100,11 +110,16 @@ func (i2c *I2C) writeByte(data byte) error { // readByte reads a single byte from the I2C bus when it is ready. func (i2c *I2C) readByte() (byte, error) { + tries := 0 for i2c.Bus.EVENTS_RXDREADY.Get() == 0 { if e := i2c.Bus.EVENTS_ERROR.Get(); e != 0 { i2c.Bus.EVENTS_ERROR.Set(0) return 0, errI2CBusError } + tries++ + if tries >= i2cTimeout { + return 0, errI2CReadTimeout + } } i2c.Bus.EVENTS_RXDREADY.Set(0) return byte(i2c.Bus.RXD.Get()), nil From 54bed7629c2b14465579252cd1da44e060152031 Mon Sep 17 00:00:00 2001 From: soypat Date: Sat, 15 Jul 2023 11:07:39 -0300 Subject: [PATCH 8/9] rp2040:add NoPin support --- src/machine/machine_rp2040_gpio.go | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/machine/machine_rp2040_gpio.go b/src/machine/machine_rp2040_gpio.go index fa2051af86..94715d963f 100644 --- a/src/machine/machine_rp2040_gpio.go +++ b/src/machine/machine_rp2040_gpio.go @@ -174,6 +174,9 @@ func (p Pin) init() { // Configure configures the gpio pin as per mode. func (p Pin) Configure(config PinConfig) { + if p == NoPin { + return + } p.init() mask := uint32(1) << p switch config.Mode { @@ -213,6 +216,9 @@ func (p Pin) Configure(config PinConfig) { // Set drives the pin high if value is true else drives it low. func (p Pin) Set(value bool) { + if p == NoPin { + return + } if value { p.set() } else { @@ -251,6 +257,9 @@ var ( // nil func to unset the pin change interrupt. If you do so, the change // parameter is ignored and can be set to any value (such as 0). func (p Pin) SetInterrupt(change PinChange, callback func(Pin)) error { + if p == NoPin { + return nil + } if p > 31 || p < 0 { return ErrInvalidInputPin } From 2a83b1fae53bfaf411f1ccb55d836ee9de1de0fb Mon Sep 17 00:00:00 2001 From: deadprogram Date: Tue, 18 Jul 2023 15:49:05 +0200 Subject: [PATCH 9/9] sync: add implementation from upstream Go for OnceFunc, OnceValue, and OnceValues Signed-off-by: deadprogram --- src/sync/oncefunc.go | 97 +++++++++++++++++++++++ src/sync/oncefunc_test.go | 159 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 256 insertions(+) create mode 100644 src/sync/oncefunc.go create mode 100644 src/sync/oncefunc_test.go diff --git a/src/sync/oncefunc.go b/src/sync/oncefunc.go new file mode 100644 index 0000000000..9ef8344132 --- /dev/null +++ b/src/sync/oncefunc.go @@ -0,0 +1,97 @@ +// Copyright 2022 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. + +package sync + +// OnceFunc returns a function that invokes f only once. The returned function +// may be called concurrently. +// +// If f panics, the returned function will panic with the same value on every call. +func OnceFunc(f func()) func() { + var ( + once Once + valid bool + p any + ) + // Construct the inner closure just once to reduce costs on the fast path. + g := func() { + defer func() { + p = recover() + if !valid { + // Re-panic immediately so on the first call the user gets a + // complete stack trace into f. + panic(p) + } + }() + f() + valid = true // Set only if f does not panic + } + return func() { + once.Do(g) + if !valid { + panic(p) + } + } +} + +// OnceValue returns a function that invokes f only once and returns the value +// returned by f. The returned function may be called concurrently. +// +// If f panics, the returned function will panic with the same value on every call. +func OnceValue[T any](f func() T) func() T { + var ( + once Once + valid bool + p any + result T + ) + g := func() { + defer func() { + p = recover() + if !valid { + panic(p) + } + }() + result = f() + valid = true + } + return func() T { + once.Do(g) + if !valid { + panic(p) + } + return result + } +} + +// OnceValues returns a function that invokes f only once and returns the values +// returned by f. The returned function may be called concurrently. +// +// If f panics, the returned function will panic with the same value on every call. +func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2) { + var ( + once Once + valid bool + p any + r1 T1 + r2 T2 + ) + g := func() { + defer func() { + p = recover() + if !valid { + panic(p) + } + }() + r1, r2 = f() + valid = true + } + return func() (T1, T2) { + once.Do(g) + if !valid { + panic(p) + } + return r1, r2 + } +} diff --git a/src/sync/oncefunc_test.go b/src/sync/oncefunc_test.go new file mode 100644 index 0000000000..0273b894a9 --- /dev/null +++ b/src/sync/oncefunc_test.go @@ -0,0 +1,159 @@ +// Copyright 2022 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. + +package sync_test + +import ( + "sync" + "testing" +) + +// We assume that the Once.Do tests have already covered parallelism. + +func TestOnceFunc(t *testing.T) { + calls := 0 + f := sync.OnceFunc(func() { calls++ }) + allocs := testing.AllocsPerRun(10, f) + if calls != 1 { + t.Errorf("want calls==1, got %d", calls) + } + if allocs != 0 { + t.Errorf("want 0 allocations per call, got %v", allocs) + } +} + +func TestOnceValue(t *testing.T) { + calls := 0 + f := sync.OnceValue(func() int { + calls++ + return calls + }) + allocs := testing.AllocsPerRun(10, func() { f() }) + value := f() + if calls != 1 { + t.Errorf("want calls==1, got %d", calls) + } + if value != 1 { + t.Errorf("want value==1, got %d", value) + } + if allocs != 0 { + t.Errorf("want 0 allocations per call, got %v", allocs) + } +} + +func TestOnceValues(t *testing.T) { + calls := 0 + f := sync.OnceValues(func() (int, int) { + calls++ + return calls, calls + 1 + }) + allocs := testing.AllocsPerRun(10, func() { f() }) + v1, v2 := f() + if calls != 1 { + t.Errorf("want calls==1, got %d", calls) + } + if v1 != 1 || v2 != 2 { + t.Errorf("want v1==1 and v2==2, got %d and %d", v1, v2) + } + if allocs != 0 { + t.Errorf("want 0 allocations per call, got %v", allocs) + } +} + +// TODO: need to implement more complete panic handling for these tests. +// func testOncePanicX(t *testing.T, calls *int, f func()) { +// testOncePanicWith(t, calls, f, func(label string, p any) { +// if p != "x" { +// t.Fatalf("%s: want panic %v, got %v", label, "x", p) +// } +// }) +// } + +// func testOncePanicWith(t *testing.T, calls *int, f func(), check func(label string, p any)) { +// // Check that the each call to f panics with the same value, but the +// // underlying function is only called once. +// for _, label := range []string{"first time", "second time"} { +// var p any +// panicked := true +// func() { +// defer func() { +// p = recover() +// }() +// f() +// panicked = false +// }() +// if !panicked { +// t.Fatalf("%s: f did not panic", label) +// } +// check(label, p) +// } +// if *calls != 1 { +// t.Errorf("want calls==1, got %d", *calls) +// } +// } + +// func TestOnceFuncPanic(t *testing.T) { +// calls := 0 +// f := sync.OnceFunc(func() { +// calls++ +// panic("x") +// }) +// testOncePanicX(t, &calls, f) +// } + +// func TestOnceValuePanic(t *testing.T) { +// calls := 0 +// f := sync.OnceValue(func() int { +// calls++ +// panic("x") +// }) +// testOncePanicX(t, &calls, func() { f() }) +// } + +// func TestOnceValuesPanic(t *testing.T) { +// calls := 0 +// f := sync.OnceValues(func() (int, int) { +// calls++ +// panic("x") +// }) +// testOncePanicX(t, &calls, func() { f() }) +// } +// +// func TestOnceFuncPanicNil(t *testing.T) { +// calls := 0 +// f := sync.OnceFunc(func() { +// calls++ +// panic(nil) +// }) +// testOncePanicWith(t, &calls, f, func(label string, p any) { +// switch p.(type) { +// case nil, *runtime.PanicNilError: +// return +// } +// t.Fatalf("%s: want nil panic, got %v", label, p) +// }) +// } +// +// func TestOnceFuncGoexit(t *testing.T) { +// // If f calls Goexit, the results are unspecified. But check that f doesn't +// // get called twice. +// calls := 0 +// f := sync.OnceFunc(func() { +// calls++ +// runtime.Goexit() +// }) +// var wg sync.WaitGroup +// for i := 0; i < 2; i++ { +// wg.Add(1) +// go func() { +// defer wg.Done() +// defer func() { recover() }() +// f() +// }() +// wg.Wait() +// } +// if calls != 1 { +// t.Errorf("want calls==1, got %d", calls) +// } +// }