From adc0cae960cec39b172865a5efb522e205bbf2f0 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Tue, 25 Feb 2025 13:04:34 +0100 Subject: [PATCH 1/3] compiler: add //go:linkname support for globals We previously used our own //go:extern but //go:linkname is probably the better choice since it's used in the math/bits package. --- compiler/symbol.go | 9 +++++++-- compiler/testdata/pragma.go | 6 ++++++ compiler/testdata/pragma.ll | 1 + 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/compiler/symbol.go b/compiler/symbol.go index 1de3c6f39d..8360bfc661 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -688,20 +688,25 @@ func (c *compilerContext) getGlobalInfo(g *ssa.Global) globalInfo { // Check for //go: pragmas, which may change the link name (among others). doc := c.astComments[info.linkName] if doc != nil { - info.parsePragmas(doc) + info.parsePragmas(doc, g) } return info } // Parse //go: pragma comments from the source. In particular, it parses the // //go:extern pragma on globals. -func (info *globalInfo) parsePragmas(doc *ast.CommentGroup) { +func (info *globalInfo) parsePragmas(doc *ast.CommentGroup, g *ssa.Global) { for _, comment := range doc.List { if !strings.HasPrefix(comment.Text, "//go:") { continue } parts := strings.Fields(comment.Text) switch parts[0] { + case "//go:linkname": + if len(parts) == 3 && g.Name() == parts[1] { + info.linkName = parts[2] + info.extern = true + } case "//go:extern": info.extern = true if len(parts) == 2 { diff --git a/compiler/testdata/pragma.go b/compiler/testdata/pragma.go index 1e6e967f53..c44f26457a 100644 --- a/compiler/testdata/pragma.go +++ b/compiler/testdata/pragma.go @@ -2,6 +2,12 @@ package main import _ "unsafe" +// Use the go:linkname mechanism to link this global to a different package. +// This is used in math/bits. +// +//go:linkname linknamedGlobal runtime.testLinknamedGlobal +var linknamedGlobal int + // Creates an external global with name extern_global. // //go:extern extern_global diff --git a/compiler/testdata/pragma.ll b/compiler/testdata/pragma.ll index a3cbb72c1d..38d99c3b3a 100644 --- a/compiler/testdata/pragma.ll +++ b/compiler/testdata/pragma.ll @@ -3,6 +3,7 @@ source_filename = "pragma.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.testLinknamedGlobal = external global i32, align 4 @extern_global = external global [0 x i8], align 1 @main.alignedGlobal = hidden global [4 x i32] zeroinitializer, align 32 @main.alignedGlobal16 = hidden global [4 x i32] zeroinitializer, align 16 From 88d273da97de8c390c64b4d357d21c130f2d5391 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Tue, 25 Feb 2025 13:07:21 +0100 Subject: [PATCH 2/3] compiler, runtime: implement recoverable divide-by-zero panic This gets the math/bits and go/constant package tests to pass. Unfortunately, this also has a binary size impact of around 150-200 bytes in many cases. I'm a bit on the edge on whether this is worth it, since it's mostly used for getting package tests to work. But at the same time, having working package tests is very valuable. --- GNUmakefile | 5 +++++ compiler/asserts.go | 26 ++++++++++++++++---------- src/runtime/error.go | 17 +++++++++++++++++ src/runtime/panic.go | 2 +- testdata/recover.go | 13 +++++++++++++ 5 files changed, 52 insertions(+), 11 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 28031ffec5..2b85d610e2 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -362,6 +362,7 @@ TEST_PACKAGES_FAST = \ # debug/plan9obj requires os.ReadAt, which is not yet supported on windows # image requires recover(), which is not yet supported on wasi # io/ioutil requires os.ReadDir, which is not yet supported on windows or wasi +# math/bits: needs panic()/recover() # mime: fail on wasi; neds panic()/recover() # mime/multipart: needs wasip1 syscall.FDFLAG_NONBLOCK # mime/quotedprintable requires syscall.Faccessat @@ -382,8 +383,10 @@ TEST_PACKAGES_LINUX := \ crypto/hmac \ debug/dwarf \ debug/plan9obj \ + go/constant \ image \ io/ioutil \ + math/bits \ mime \ mime/multipart \ mime/quotedprintable \ @@ -403,6 +406,8 @@ TEST_PACKAGES_WINDOWS := \ compress/flate \ crypto/des \ crypto/hmac \ + go/constant \ + math/bits \ strconv \ text/template/parse \ $(nil) diff --git a/compiler/asserts.go b/compiler/asserts.go index f07b73bc26..a3d060ce50 100644 --- a/compiler/asserts.go +++ b/compiler/asserts.go @@ -31,7 +31,7 @@ func (b *builder) createLookupBoundsCheck(arrayLen, index llvm.Value) { // Now do the bounds check: index >= arrayLen outOfBounds := b.CreateICmp(llvm.IntUGE, index, arrayLen, "") - b.createRuntimeAssert(outOfBounds, "lookup", "lookupPanic") + b.createRuntimeAssert(outOfBounds, "lookup", "lookupPanic", false) } // createSliceBoundsCheck emits a bounds check before a slicing operation to make @@ -74,7 +74,7 @@ func (b *builder) createSliceBoundsCheck(capacity, low, high, max llvm.Value, lo outOfBounds3 := b.CreateICmp(llvm.IntUGT, max, capacity, "slice.maxcap") outOfBounds := b.CreateOr(outOfBounds1, outOfBounds2, "slice.lowmax") outOfBounds = b.CreateOr(outOfBounds, outOfBounds3, "slice.lowcap") - b.createRuntimeAssert(outOfBounds, "slice", "slicePanic") + b.createRuntimeAssert(outOfBounds, "slice", "slicePanic", false) } // createSliceToArrayPointerCheck adds a check for slice-to-array pointer @@ -86,7 +86,7 @@ func (b *builder) createSliceToArrayPointerCheck(sliceLen llvm.Value, arrayLen i // > run-time panic occurs. arrayLenValue := llvm.ConstInt(b.uintptrType, uint64(arrayLen), false) isLess := b.CreateICmp(llvm.IntULT, sliceLen, arrayLenValue, "") - b.createRuntimeAssert(isLess, "slicetoarray", "sliceToArrayPointerPanic") + b.createRuntimeAssert(isLess, "slicetoarray", "sliceToArrayPointerPanic", false) } // createUnsafeSliceStringCheck inserts a runtime check used for unsafe.Slice @@ -118,7 +118,7 @@ func (b *builder) createUnsafeSliceStringCheck(name string, ptr, len llvm.Value, lenIsNotZero := b.CreateICmp(llvm.IntNE, len, zero, "") assert := b.CreateAnd(ptrIsNil, lenIsNotZero, "") assert = b.CreateOr(assert, lenOutOfBounds, "") - b.createRuntimeAssert(assert, name, "unsafeSlicePanic") + b.createRuntimeAssert(assert, name, "unsafeSlicePanic", false) } // createChanBoundsCheck creates a bounds check before creating a new channel to @@ -155,7 +155,7 @@ func (b *builder) createChanBoundsCheck(elementSize uint64, bufSize llvm.Value, // Do the check for a too large (or negative) buffer size. bufSizeTooBig := b.CreateICmp(llvm.IntUGE, bufSize, maxBufSize, "") - b.createRuntimeAssert(bufSizeTooBig, "chan", "chanMakePanic") + b.createRuntimeAssert(bufSizeTooBig, "chan", "chanMakePanic", false) } // createNilCheck checks whether the given pointer is nil, and panics if it is. @@ -199,7 +199,7 @@ func (b *builder) createNilCheck(inst ssa.Value, ptr llvm.Value, blockPrefix str isnil := b.CreateICmp(llvm.IntEQ, ptr, nilptr, "") // Emit the nil check in IR. - b.createRuntimeAssert(isnil, blockPrefix, "nilPanic") + b.createRuntimeAssert(isnil, blockPrefix, "nilPanic", false) } // createNegativeShiftCheck creates an assertion that panics if the given shift value is negative. @@ -212,7 +212,7 @@ func (b *builder) createNegativeShiftCheck(shift llvm.Value) { // isNegative = shift < 0 isNegative := b.CreateICmp(llvm.IntSLT, shift, llvm.ConstInt(shift.Type(), 0, false), "") - b.createRuntimeAssert(isNegative, "shift", "negativeShiftPanic") + b.createRuntimeAssert(isNegative, "shift", "negativeShiftPanic", false) } // createDivideByZeroCheck asserts that y is not zero. If it is, a runtime panic @@ -225,12 +225,12 @@ func (b *builder) createDivideByZeroCheck(y llvm.Value) { // isZero = y == 0 isZero := b.CreateICmp(llvm.IntEQ, y, llvm.ConstInt(y.Type(), 0, false), "") - b.createRuntimeAssert(isZero, "divbyzero", "divideByZeroPanic") + b.createRuntimeAssert(isZero, "divbyzero", "divideByZeroPanic", true) } // createRuntimeAssert is a common function to create a new branch on an assert // bool, calling an assert func if the assert value is true (1). -func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc string) { +func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc string, invoke bool) { // Check whether we can resolve this check at compile time. if !assert.IsAConstantInt().IsNil() { val := assert.ZExtValue() @@ -252,7 +252,13 @@ func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc // Fail: the assert triggered so panic. b.SetInsertPointAtEnd(faultBlock) - b.createRuntimeCall(assertFunc, nil, "") + if invoke { + // This runtime panic is recoverable. + b.createRuntimeInvoke(assertFunc, nil, "") + } else { + // This runtime panic is not recoverable. + b.createRuntimeCall(assertFunc, nil, "") + } b.CreateUnreachable() // Ok: assert didn't trigger so continue normally. diff --git a/src/runtime/error.go b/src/runtime/error.go index 3ae5ea3aae..0bca20c0b0 100644 --- a/src/runtime/error.go +++ b/src/runtime/error.go @@ -4,5 +4,22 @@ package runtime type Error interface { error + // Method to indicate this is indeed a runtime error. RuntimeError() } + +type runtimeError struct { + msg string +} + +func (r runtimeError) Error() string { + return r.msg +} + +// Purely here to satisfy the Error interface. +func (r runtimeError) RuntimeError() {} + +var ( + divideError error = runtimeError{"runtime error: integer divide by zero"} + overflowError error = runtimeError{"runtime error: integer overflow"} +) diff --git a/src/runtime/panic.go b/src/runtime/panic.go index 9ae1f982b9..1cabe8a51a 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -220,7 +220,7 @@ func negativeShiftPanic() { // Panic when there is a divide by zero. func divideByZeroPanic() { - runtimePanicAt(returnAddress(0), "divide by zero") + _panic(divideError) } func blockingPanic() { diff --git a/testdata/recover.go b/testdata/recover.go index 6fdf282e7b..0a6a93dd7b 100644 --- a/testdata/recover.go +++ b/testdata/recover.go @@ -30,6 +30,9 @@ func main() { println("\n# defer panic") deferPanic() + println("\n# runtime panics") + runtimePanicDivByZero(1, 0) + println("\n# runtime.Goexit") runtimeGoexit() } @@ -114,6 +117,16 @@ func deferPanic() { println("defer panic") } +func runtimePanicDivByZero(a, b int) int { + defer func() { + if err := recover(); err != nil { + println("recovered:", err) + } + }() + + return a / b +} + func runtimeGoexit() { wg.Add(1) go func() { From 98d55ab070533d1aefe4fdc2e735c4955cb47e11 Mon Sep 17 00:00:00 2001 From: Ayke van Laethem Date: Wed, 26 Feb 2025 12:38:06 +0100 Subject: [PATCH 3/3] compiler, runtime: make slice lookup panics recoverable --- GNUmakefile | 2 ++ compiler/asserts.go | 14 ++++---------- src/runtime/error.go | 1 + src/runtime/panic.go | 2 +- testdata/recover.go | 11 +++++++++++ 5 files changed, 19 insertions(+), 11 deletions(-) diff --git a/GNUmakefile b/GNUmakefile index 2b85d610e2..80d6a6e896 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -383,6 +383,7 @@ TEST_PACKAGES_LINUX := \ crypto/hmac \ debug/dwarf \ debug/plan9obj \ + encoding/binary \ go/constant \ image \ io/ioutil \ @@ -406,6 +407,7 @@ TEST_PACKAGES_WINDOWS := \ compress/flate \ crypto/des \ crypto/hmac \ + encoding/binary \ go/constant \ math/bits \ strconv \ diff --git a/compiler/asserts.go b/compiler/asserts.go index a3d060ce50..2de9a81283 100644 --- a/compiler/asserts.go +++ b/compiler/asserts.go @@ -31,7 +31,7 @@ func (b *builder) createLookupBoundsCheck(arrayLen, index llvm.Value) { // Now do the bounds check: index >= arrayLen outOfBounds := b.CreateICmp(llvm.IntUGE, index, arrayLen, "") - b.createRuntimeAssert(outOfBounds, "lookup", "lookupPanic", false) + b.createRuntimeAssert(outOfBounds, "lookup", "lookupPanic", true) } // createSliceBoundsCheck emits a bounds check before a slicing operation to make @@ -230,7 +230,7 @@ func (b *builder) createDivideByZeroCheck(y llvm.Value) { // createRuntimeAssert is a common function to create a new branch on an assert // bool, calling an assert func if the assert value is true (1). -func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc string, invoke bool) { +func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc string, isInvoke bool) { // Check whether we can resolve this check at compile time. if !assert.IsAConstantInt().IsNil() { val := assert.ZExtValue() @@ -245,23 +245,17 @@ func (b *builder) createRuntimeAssert(assert llvm.Value, blockPrefix, assertFunc // current insert position. faultBlock := b.ctx.AddBasicBlock(b.llvmFn, blockPrefix+".throw") nextBlock := b.insertBasicBlock(blockPrefix + ".next") - b.blockExits[b.currentBlock] = nextBlock // adjust outgoing block for phi nodes // Now branch to the out-of-bounds or the regular block. b.CreateCondBr(assert, faultBlock, nextBlock) // Fail: the assert triggered so panic. b.SetInsertPointAtEnd(faultBlock) - if invoke { - // This runtime panic is recoverable. - b.createRuntimeInvoke(assertFunc, nil, "") - } else { - // This runtime panic is not recoverable. - b.createRuntimeCall(assertFunc, nil, "") - } + b.createRuntimeCallCommon(assertFunc, nil, "", isInvoke) b.CreateUnreachable() // Ok: assert didn't trigger so continue normally. + b.blockExits[b.currentBlock] = nextBlock // adjust outgoing block for phi nodes b.SetInsertPointAtEnd(nextBlock) } diff --git a/src/runtime/error.go b/src/runtime/error.go index 0bca20c0b0..20c8faa426 100644 --- a/src/runtime/error.go +++ b/src/runtime/error.go @@ -21,5 +21,6 @@ func (r runtimeError) RuntimeError() {} var ( divideError error = runtimeError{"runtime error: integer divide by zero"} + lookupError error = runtimeError{"runtime error: index out of range"} overflowError error = runtimeError{"runtime error: integer overflow"} ) diff --git a/src/runtime/panic.go b/src/runtime/panic.go index 1cabe8a51a..fb5cbceab4 100644 --- a/src/runtime/panic.go +++ b/src/runtime/panic.go @@ -187,7 +187,7 @@ func nilMapPanic() { // Panic when trying to access an array or slice out of bounds. func lookupPanic() { - runtimePanicAt(returnAddress(0), "index out of range") + _panic(lookupError) } // Panic when trying to slice a slice out of bounds. diff --git a/testdata/recover.go b/testdata/recover.go index 0a6a93dd7b..7d335fbbfc 100644 --- a/testdata/recover.go +++ b/testdata/recover.go @@ -32,6 +32,7 @@ func main() { println("\n# runtime panics") runtimePanicDivByZero(1, 0) + runtimePanicLookup([]int{1, 2, 3}, 10) println("\n# runtime.Goexit") runtimeGoexit() @@ -127,6 +128,16 @@ func runtimePanicDivByZero(a, b int) int { return a / b } +func runtimePanicLookup(slice []int, index int) int { + defer func() { + if err := recover(); err != nil { + println("recovered:", err) + } + }() + + return slice[index] +} + func runtimeGoexit() { wg.Add(1) go func() {