Repository: go
Issue: proposal: cmd/compile: `go:readonly` comment directive for body-less functions
Labels: Proposal,compiler/runtime

### Proposal Details

Add `go:readonly` comment directive for body-less functions indicating the following arguments and any pointers exclusively accessed through a readonly argument wont be modified:
```go
// writeBlocks will update vector (read-hash-write) and read extra and bytes.
//
//go:nescape
//go:readonly extra, bytes
func writeBlocks(vector *[4]uint64, extra *[32]byte, bytes []byte)
```

The point of this is to hook up with 925d2fb36c8e4c9c0e6e240a1621db36c34e5d31 this would allow `[]byte(str)` conversions to be elided when the byte string is passed as a readonly argument to an assembly optimized subroutine.

[`github.com/cespare/xxhash`](https://pkg.go.dev/github.com/cespare/xxhash) implements `Sum64String(string) uint64` and `(*Digest).WriteString(string)` [using `unsafe`](https://github.com/cespare/xxhash/blob/main/xxhash_unsafe.go) which allows to skip the allocation either by using [`(*strings.Reader).WriteTo`](https://pkg.go.dev/strings#Reader.WriteTo) or [`io.WriteString`](https://pkg.go.dev/io#WriteString).
Other hashing libraries such as `crypto` do not implement this optimization, this could allow to implement this using compiler optimizations rather than duplicating APIs for `[]byte` and `string`. (\*note: this assume you are using a devirtualized `hash.Hash` or a concrete hasher implementation, which is now possible and not unrealistic, particularly for `h := sha256.New(); h.Write([]byte(str))` patterns)

In the future this *could* improve register allocation in some rare edge cases so I don't expect this to be implemented.
Other stronger compilers like GCC and LLVM might already implement such logic in their register allocators, then work in gofrontend could be much easier (as I assume this would amount to setting up metadata flags on the function call nodes passed to gcc or llvm) so who knows.

---

The body-less function is allowed to read from memory, which is arguably a side effect when using `-race` altho I don't think very many body-less functions implement any kind of race tracking.

---

For slices the body-less function would assume to not be allowed to write past capacity.
If some memory is accessible through an unknown and a readonly pointer the function is allowed to write to it:
```go
//go:readonly b
func doSomething(a, b []byte)

func f() {
 var x [64]byte
 doSomething(x[:32:32], x[:])
}
```
Here the compiler *would* be allowed to assume only the last 32 bytes of the `x` array are readonly (I expect compilers to not implement this or to assume `x` maybe completely overwritten).

---

If a pointer to a pointer is readonly then both memory are readonly:
```go
type s struct{
 v *int
}

//go:readonly a
func doSomething(a *s)

func f() {
 var x int // here the compiler is allowed to assume `x` will never be modified by `doSomething`
 doSomething(&S{&x})
}
```
Note: this apply transitively to any amount of level.

---

Combining the two edge cases above, if a pointer to a pointer is accessible both through unknown and readonly paths, the body-less function *may* modify it:
```go
type s struct{
 v *int
}

//go:readonly b
func doSomething(a, b *s)

func f() {
 var x int // x maybe modified through a
 doSomething(&S{&x}, &S{&x})
}
```

---

The body-less function is assumed to maybe write to anything part of the global scope:
```go
var x int

//go:readonly a
func doSomething(a *int)

func f() {
 doSomething(&x) // doSomething is allowed to modify x
}
```