Skip to content

cmd/compile: regalloc reloads a value from a narrower phi's stack slot, losing its upper bits #81928

Open

Description

@smm-h

Go version

go version go1.27.1 linux/amd64

go1.26.8 and tip (75f2c46, 2026-09-30) fail in the same way. go1.21.13 fails on arm64 only for this program.

Output of go env in your module/workspace:

GOARCH='amd64'
GOOS='linux'
GOAMD64='v1'
GOEXPERIMENT=''

The arm64 results are from cross-compiling on that host (GOARCH=arm64). I ran them on an Android phone (Snapdragon 6 Gen 1) and under qemu-aarch64 10.2.2, with the same results.

What did you do?

package main

import "fmt"

var sink int64

//go:noinline
func g(x int64) { sink += x }

//go:noinline
func f(p []int64, c int) int64 {
	a0, a1, a2, a3, a4, a5, a6 := p[62], p[21], p[45], p[17], p[12], p[44], p[10]
	a7, a8, a9, a10, a11, a12, a13 := p[5], p[48], p[42], p[33], p[7], p[30], p[6]
	a14, a15, a16, a17, a18, a19, a20 := p[49], p[31], p[9], p[50], p[23], p[60], p[24]
	a21, a22, a23, a24, a25 := p[0], p[26], p[28], p[20], p[4]
	var x0, x1, x2 int64
	var y int32
	if c&1 != 0 {
		g(a9)
	} else if c&2 != 0 {
		t := p[44+c&7]
		x1 = p[c&7]
		x2 = t
		y = int32(t)
	} else {
		x0 = a8
		x1 = a0 + int64(c)
		x2 = x1
	}
	h := int64(0)
	h = h*31 + x0
	h = h*31 + x1
	h = h*31 + x2
	h = h*31 + int64(y)
	for _, a := range [...]int64{a0, a1, a2, a3, a4, a5, a6, a7, a8, a10, a11, a12, a13, a14, a15, a16, a17, a18, a19, a20, a21, a22, a23, a24, a25} {
		h = h*41 + a
	}
	return h
}

func main() {
	p := make([]int64, 64)
	for i := range p {
		p[i] = int64(i+1)<<32 | int64(i)
	}
	want := [...]uint64{327951454604667456, 11694440227811961264, 6120305881949198018, 11694440227811961264}
	for c, w := range want {
		if got := uint64(f(p, c)); got != w {
			panic(fmt.Sprintf("f(p, %d) = %#x, want %#x", c, got, w))
		}
	}
	fmt.Println("ok")
}
go run x.go
GOARCH=arm64 go build -o x.arm64 x.go   # run on arm64
go run -gcflags='-N -l' x.go

What did you see happen?

On amd64 and on arm64:

panic: f(p, 2) = 0xdf87d5acda50d6c2, want 0x54efb1c5da50d6c2

The -N -l build prints ok. The low 32 bits of the result are right. The error comes from x2: its upper 32 bits become the sign extension of its low 32 bits.

What did you expect to see?

ok from every build.

Diagnosis

Trunc64to32 lowers to a Copy on amd64 and arm64. After copy elimination, on the edge from the c&2 != 0 block to the merge, the int32 phi y and the int64 phi x2 both take the same int64 value t. With this many values live, y is a stack phi, homed in a 4-byte LocalSlot.

edgeState.processDest stores t into y's slot with the slot's type, on purpose ("Make sure we spill with the size of the slot, not the size of x (which might be wider due to our dropping of narrowing conversions)"). edgeState.set then adds that StoreReg <int32> to e.cache[t] as a full copy of t. Two things follow on the same edge:

  1. t's register no longer holds its only copy (len(e.cache[vid]) == 1 is false, and uniqueRegs drops it), so another destination may overwrite that register.
  2. x2's destination then finds only the 4-byte slot as a source and emits LoadReg <int32> from it: a 32-bit sign-extending load into an int64 phi.

Output of -d=ssa/regalloc/debug=3 for the edge, on amd64 at 2ff5743 (from an equivalent program):

moving v178 to y0[int32]
sources of v178: R15:v178 [use R15]
v154 = StoreReg <int32> v178 : y0[int32]
v178 now available in y0[int32]:v154
...
moving v128 to R15
sources of v128: a11[int64]:v13 [use a11[int64]]
v178 no longer available in R15:v178
v155 = LoadReg <int64> v13 : R15
...
moving v178 to DI
sources of v178: y0[int32]:v154 [use y0[int32]]
v134 = LoadReg <int32> v154 : DI

In machine code (arm64, the original program): MOVW R10, 160(RSP), MOVD R6, R10, then MOVW 160(RSP), R17.

Other programs of the same shape (a fresh 64-bit value flowing into a wide phi and into a narrower stack phi on the same edge) fail on riscv64, s390x, loong64, mips64 and mips64le too. I have not seen it on ppc64le, where the truncation lowers to an explicit MOVWreg/MOVWZreg. Whether a given program fails depends on register pressure, so the program above shows it on amd64 and arm64 only.

A fix that works for every failing program I have: in edgeState.set, when loc is a LocalSlot narrower than the value (e.s.orig[vid].Type.Size()), record the slot in e.contents (the destination is satisfied) but do not add it to e.cache[vid]. I will send a CL.

This looks related to #80577 but is a different path. #80577 is about a value's own spill and restore choosing the extension from its type. Here a wide value is read back through another phi's narrower slot on an edge.

Activity

  1. smm-h commented on Sep 30, 2026

    @smm-h
    ContributorAuthor

    Two corrections to the report above.

    1. The extension depends on the architecture. The 4-byte load is MOVL on amd64, which zero-extends, so there the upper half of x2 becomes zero. On arm64, riscv64, s390x, loong64 and mips64 it is MOVW, which sign-extends. The low halves in the program above are all below 2^31, so both give the same result there. The class is also not 64-bit only: Trunc32to16, Trunc32to8 and Trunc16to8 are Copies on 386, arm and mips too, and every truncation is a Copy on wasm.

    2. The fix suggested at the end (never add a narrower slot to e.cache[vid]) is wrong: it livelocks the edge shuffle. When a value's only destinations on an edge are narrow phi slots, its register then stays protected as its last copy, and the cycle breaker in edgeState.process moves it from register to register forever. Some generated programs make the compiler grow without bound with it. The CL I am preparing instead records, per value, the largest size any destination on the edge needs, and does not count a slot smaller than that as a copy. A narrow slot that holds all the edge needs of the value still counts, as before.

  2. dmitshur commented on Oct 5, 2026

    @dmitshur
    Member

    CC @golang/compiler.

  3. added
    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
    on Oct 5, 2026
  4. added this to the Backlog milestone on Oct 5, 2026
  5. dmitshur commented on Oct 6, 2026

    @dmitshur
    Member

    From triage meeting, this is likely not a regalloc bug but elsewhere. @cherrymui said that @randall77 had a suggested approach for this might work for this.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.compiler/runtimeIssues related to the Go compiler and/or runtime.

    Type

    No type

    Projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions