cmd/compile: "panic: unification reached recursion depth limit" with recursive type constraint
Go version
go version go1.22.3 darwin/arm64
Output of go env in your module/workspace:
GO111MODULE=''
GOARCH='arm64'
GOBIN=''
GOCACHE='/Users/cameronmartin/Library/Caches/go-build'
GOENV='/Users/cameronmartin/Library/Application Support/go/env'
GOEXE=''
GOEXPERIMENT=''
GOFLAGS=''
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMODCACHE='/Users/cameronmartin/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='/Users/cameronmartin/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/Users/cameronmartin/go/pkg/mod/golang.org/[email protected]'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='go1.22.3+auto'
GOTOOLDIR='/Users/cameronmartin/go/pkg/mod/golang.org/[email protected]/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.22.3'
GCCGO='gccgo'
AR='ar'
CC='clang'
CXX='clang++'
CGO_ENABLED='1'
GOMOD='/dev/null'
GOWORK=''
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
PKG_CONFIG='pkg-config'
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/k3/7h82b52d2gz972yl3nppjdtw0000gn/T/go-build29573479=/tmp/go-build -gno-record-gcc-switches -fno-common'
What did you do?
https://go.dev/play/p/_dEZsvc-bV-
What did you see happen?
<unknown line number>: internal compiler error: panic: unification reached recursion depth limit
What did you expect to see?
I expected a non-panic compilation error.
(But maybe I shouldn't have; with a couple of tweaks, a similar program is valid: https://go.dev/play/p/jJ8fTc7Uhie)
Slightly simplified reproducer:
package p
type S *S
func f[R, T *R](T) {}
func _() {
var s S
f(s)
}
panics as described. But adding a ~ (or swapping the type parameters) in f avoids the crash:
package p
type S *S
func f[R, T ~*R](T) {} // <<< no crash with `~`
func _() {
var s S
f(s)
}
The handling of type element unification may not be quite right (see unify.go:566 ff.)
Too late to fix for 1.23 as this is tricky and may introduce subtle bugs. Moving to 1.24 since there's a work-around, and this is not a regression.
@taking and I looked at this some more. At the core of the issue we have the following situation.
- Type inference correctly infers that
T ➞ Sfrom the parameter assignment to the functionf. - Type inference then proceeds by trying to extract missing information for
R. - The constraint for
Ris*R, the core type of the constraint is*Rand*Ris the only type in the typeset of the constraint, so (constraint) type inference infersR ➞ *R - Now inference looks at the constraint
[T *R]. We know thatTmust beSso inference matchesSagainst*R:S ≡ *R. This is the start of the cycle. - Because
Sis a defined type and*Ris type literal, inference comparesunder(S) ≡ *Rwhich is*S ≡ *R. - Both types are pointer types, so
S ≡ R. - The inferred type for
Ris*R(step 3), so we arrive atS ≡ *R, which is where we were at step 4. (In the inference trace, the cycle starts a bit later because the LHS and RHS are swapped in the first round, but this doesn't change the behavior - the inference is symmetric with respect to LHS and RHS.)
From the inference trace:
== infer : [R₃, T₄](T₄) ➞ [<nil>, <nil>]
== function parameters: (T₄)
-- function arguments : [s (variable of type p.S)]
. T₄ ≡ p.S // assign
. T₄ ➞ p.S
=> [R₃, T₄] ➞ [<nil>, p.S]
== type parameters: [R₃, T₄]
-- iteration 0
-- type parameter R₃ = <nil>: core(R₃) = *R, single = true
R₃ ➞ *R₃
-- type parameter T₄ = p.S: core(T₄) = *R, single = true
. p.S ≡ *R₃ // inexact
. *R₃ ≡ p.S // swap
. *R₃ ≡ under p.S
. . R₃ ≡ p.S // inexact
. . . *R₃ ≡ p.S // inexact
. . . *R₃ ≡ under p.S
. . . . R₃ ≡ p.S // inexact
. . . . . *R₃ ≡ p.S // inexact
. . . . . etc.
It's not obvious what the correct "fix" is in a case like this. We end up in a cycle because of the cyclic definition of S, but if we start with comparing T against *R instead of R against *R we avoid the endless recursion.
One approach might be to start with type parameters for which we already have a type argument inferred when doing the constraint type inference step.
This issue is currently labeled as early-in-cycle for Go 1.24. That time is now, so a friendly reminder to look at it again.
Moving to 1.25; too late for 1.24.
This issue is currently labeled as early-in-cycle for Go 1.25. That time is now, so a friendly reminder to look at it again.