Skip to content

cmd/compile: internal compiler error on pointer to recursive type alias #74181

Open
@adamlouis

Description

@adamlouis

Go version

go version go1.24.4 darwin/arm64

Output of go env in your module/workspace:

AR='ar'
CC='clang'
CGO_CFLAGS='-O2 -g'
CGO_CPPFLAGS=''
CGO_CXXFLAGS='-O2 -g'
CGO_ENABLED='1'
CGO_FFLAGS='-O2 -g'
CGO_LDFLAGS='-O2 -g'
CXX='clang++'
GCCGO='gccgo'
GO111MODULE=''
GOARCH='arm64'
GOARM64='v8.0'
GOAUTH='netrc'
GOBIN=''
GOCACHE='*************************'
GOCACHEPROG=''
GODEBUG=''
GOENV='*************************'
GOEXE=''
GOEXPERIMENT=''
GOFIPS140='off'
GOFLAGS=''
GOGCCFLAGS='-fPIC -arch arm64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -ffile-prefix-map=/var/folders/34/__4d1gf55nx2m34qjyjc3f1m0000gn/T/go-build4095632605=/tmp/go-build -gno-record-gcc-switches -fno-common'
GOHOSTARCH='arm64'
GOHOSTOS='darwin'
GOINSECURE=''
GOMOD='/dev/null'
GOMODCACHE='*************************'
GONOPROXY=''
GONOSUMDB=''
GOOS='darwin'
GOPATH='*************************'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/local/go'
GOSUMDB='sum.golang.org'
GOTELEMETRY='local'
GOTELEMETRYDIR='*************************'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/local/go/pkg/tool/darwin_arm64'
GOVCS=''
GOVERSION='go1.24.4'
GOWORK=''
PKG_CONFIG='pkg-config'

What did you do?

Link: https://go.dev/play/p/AzuM0eY_vGH

An internal compiler error is emitted when using a pointer to a recursive type alias.

Note: the non-pointer version compiles successfully. Likewise, the pointer version works if moved into the alias definition (type AB = *A[B]).

To reproduce:

go run main.go

package main

type _ interface {
	Value() AB
}

type A[T any] struct{}

type B struct {
	Value *AB
}

type AB = A[B]

func main() {
}

What did you see happen?

% go run main.go
# command-line-arguments
<unknown line number>: internal compiler error: unexpected types2.Invalid

Please file a bug report including a short program that triggers the error.
https://go.dev/issue/new
%

What did you expect to see?

% go run main.go
%

Sorry if this is a duplicate issue! There were some related entires, though I get this on the latest version, 1.24.4.

Activity

added
BugReportIssues describing a possible bug in the Go implementation.
on Jun 14, 2025
thepudds

thepudds commented on Jun 14, 2025

@thepudds
Member

Possibly related to #72887 (or maybe not).

It seems to reproduce with tip on the playground: https://go.dev/play/p/AzuM0eY_vGH?v=gotip

@adamlouis, what do you get if you add -gcflags=-h flag to go run or go build, and does it seem at least superficially similar to the call stacks in #72887? (The -h causes the compiler to panic on the first compile error encountered, which lets you see the call stack).

CC @griesemer, @findleyr

thepudds

thepudds commented on Jun 14, 2025

@thepudds
Member

@adamlouis, also, I should add -- thanks for filing this, and especially thanks for including a nice minimal reproducer, which should be enough for others to investigate further (and no worries of course if you don't have time to try -gcflags=-h or look at this more yourself).

JunyangShao

JunyangShao commented on Jun 16, 2025

@JunyangShao
Contributor

@golang/compiler @mrkfrmn

added
NeedsInvestigationSomeone must examine and confirm this is a valid issue and not a duplicate of an existing one.
on Jun 16, 2025
mrkfrmn

mrkfrmn commented on Jun 18, 2025

@mrkfrmn
Contributor

I've done a bit of poking — somewhat interesting to me is that the below panics:

type AB = A[B]

type _ struct {
	_ AB
}

type B struct {
	f *AB
}

type A[T any] struct{}

While the below does not:

type AB = A[B]

type B struct {
	f *AB
}

type _ struct {
	_ AB
}

type A[T any] struct{}

I added in some one-off logging to get more detail on where the panic is coming from and it looks like the issue is with B — specifically its field f becomes invalid:

type cmd/compile/internal/types2/testdata.A[T any] struct{}
type any = interface{}
type cmd/compile/internal/types2/testdata.AB = cmd/compile/internal/types2/testdata.A[cmd/compile/internal/types2/testdata.B]
type cmd/compile/internal/types2/testdata.B struct{f invalid type}

And a brief cycle trace:

./manual.go:20:6  :  -- checking type _ (white, objPath = )
./manual.go:18:6  :  .  -- checking type AB (white, objPath = _)
./manual.go:28:6  :  .  .  -- checking type A (white, objPath = _->AB)
./manual.go:28:6  :  .  .  => type A[T any] struct{} (black)
./manual.go:24:6  :  .  .  -- checking type B (white, objPath = _->AB)
./manual.go:18:6  :  .  .  .  ## cycle detected: objPath = AB->B->AB (len = 2)
./manual.go:18:6  :  .  .  .  ## cycle contains: 0 values, 1 type definitions
./manual.go:18:6  :  .  .  .  => cycle is valid
./manual.go:24:6  :  .  .  => type B struct{f invalid type} (black)
./manual.go:18:6  :  .  => type AB = A[B] (black)
./manual.go:20:6  :  => type _ struct{_ AB} (black)

While in the second example it resolves fine:

type cmd/compile/internal/types2/testdata.A[T any] struct{}
type any = interface{}
type cmd/compile/internal/types2/testdata.AB =
cmd/compile/internal/types2/testdata.A[cmd/compile/internal/types2/testdata.B]
type cmd/compile/internal/types2/testdata.B struct{f *cmd/compile/internal/types2/testdata.AB}

And a cycle trace:

./manual.go:20:6  :  -- checking type B (white, objPath = )
./manual.go:18:6  :  .  -- checking type AB (white, objPath = B)
./manual.go:28:6  :  .  .  -- checking type A (white, objPath = B->AB)
./manual.go:28:6  :  .  .  => type A[T any] struct{} (black)
./manual.go:20:6  :  .  .  ## cycle detected: objPath = B->AB->B (len = 2)
./manual.go:20:6  :  .  .  ## cycle contains: 0 values, 1 type definitions
./manual.go:20:6  :  .  .  => cycle is valid
./manual.go:18:6  :  .  => type AB = A[B] (black)
./manual.go:20:6  :  => type B struct{f *AB} (black)

./manual.go:24:6  :  -- checking type _ (white, objPath = )
./manual.go:24:6  :  => type _ struct{_ AB} (black)

I took a deeper trace of the first case and got:

./manual.go:20:6  :  -- checking type _ (white, objPath = )
./manual.go:20:8  :  .  -- type struct{_ AB}
./manual.go:21:4  :  .  .  -- type AB
./manual.go:18:6  :  .  .  .  -- checking type AB (white, objPath = _)
./manual.go:18:12 :  .  .  .  .  -- type A[B]
./manual.go:18:11 :  .  .  .  .  .  -- type A
./manual.go:28:6  :  .  .  .  .  .  .  -- checking type A (white, objPath = _->AB)
./manual.go:28:10 :  .  .  .  .  .  .  .  -- type any
./manual.go:28:10 :  .  .  .  .  .  .  .  => any (under = any) // *Alias
./manual.go:28:15 :  .  .  .  .  .  .  .  -- type struct{}
./manual.go:28:15 :  .  .  .  .  .  .  .  => struct{} // *Struct
./manual.go:28:6  :  .  .  .  .  .  .  => type A[T any] struct{} (black)
./manual.go:18:11 :  .  .  .  .  .  => A[T₁ any] (under = struct{}) // *Named
./manual.go:18:13 :  .  .  .  .  .  -- type B
./manual.go:24:6  :  .  .  .  .  .  .  -- checking type B (white, objPath = _->AB)
./manual.go:24:8  :  .  .  .  .  .  .  .  -- type struct{f *AB}
./manual.go:25:4  :  .  .  .  .  .  .  .  .  -- type *AB
./manual.go:25:5  :  .  .  .  .  .  .  .  .  .  -- type AB
./manual.go:18:6  :  .  .  .  .  .  .  .  .  .  .  ## cycle detected: objPath = AB->B->AB (len = 2)
./manual.go:18:6  :  .  .  .  .  .  .  .  .  .  .  ## cycle contains: 0 values, 1 type definitions
./manual.go:18:6  :  .  .  .  .  .  .  .  .  .  .  => cycle is valid
./manual.go:25:5  :  .  .  .  .  .  .  .  .  .  => AB (under = invalid type) // *Alias
./manual.go:25:4  :  .  .  .  .  .  .  .  .  => invalid type // *Basic
./manual.go:24:8  :  .  .  .  .  .  .  .  => struct{f invalid type} // *Struct
./manual.go:24:6  :  .  .  .  .  .  .  => type B struct{f invalid type} (black)
./manual.go:18:13 :  .  .  .  .  .  => B (under = struct{f invalid type}) // *Named
./manual.go:18:12 :  .  .  .  .  => A[B] (under = <nil>) // *Named
./manual.go:18:6  :  .  .  .  => type AB = A[B] (black)
./manual.go:21:4  :  .  .  => AB (under = <nil>) // *Alias
./manual.go:20:8  :  .  => struct{_ AB} // *Struct
./manual.go:20:6  :  => type _ struct{_ AB} (black)

The interesting part to me is that *AB resolves to Basic (due to the invalid type) rather than Pointer. Seems a bit odd.

mrkfrmn

mrkfrmn commented on Jun 18, 2025

@mrkfrmn
Contributor

I suspect the below (from typInternal) is related:

case *syntax.Operation:
  if e.Op == syntax.Mul && e.Y == nil {
    typ := new(Pointer)
    typ.base = Typ[Invalid] // avoid nil base in invalid recursive type declaration
    setDefType(def, typ)
    typ.base = check.varType(e.X)
    // If typ.base is invalid, it's unlikely that *base is particularly
    // useful - even a valid dereferenciation will lead to an invalid
    // type again, and in some cases we get unexpected follow-on errors
    // (e.g., go.dev/issue/49005). Return an invalid type instead.
    if !isValid(typ.base) {
      return Typ[Invalid]
    }
    return typ
}

Specifically the !isValid(typ.base) { ... } introduced by CL 356449. FWIW, reverting that CL locally does seem to resolve this issue (I have not ran expansive tests). Though, I imagine we would be reintroducing #49005 — will think about how we can avoid opening that again.

Also cc @griesemer who worked on the related issue.

findleyr

findleyr commented on Jun 18, 2025

@findleyr
Member

@mrkfrmn I suppose the question then becomes: if !isValid(typ.base), why were there no type checking errors?

added this to the Go1.26 milestone on Jun 18, 2025
mrkfrmn

mrkfrmn commented on Jun 18, 2025

@mrkfrmn
Contributor

@findleyr Oftentimes, when we assign a type to Invalid, we emit an error then and there and continue type checking. However, that does not always happen. In particular, aliases use Invalid to mean that the right hand side of the alias is not yet fully set up — it's a sort of stub.

I think that !isValid(typ.base) is assuming an error reported elsewhere and tries to just eliminate a follow-along error. The alias use case violates this assumption and results in no error at all while leaving behind an Invalid struct field (that will never be set).

I think this is also why altering declaration order "fixes" the above — it's a coincidence that the alias is fully set up when we type the struct field.

2 considerations come to mind:

  • We could consider if Invalid is the optimal way to mark an unfinished alias. I think @adonovan may have thoughts.
  • I assume that at the end of type checking, there is a bug if there exist no errors, yet types are still assigned to Invalid. It's unfortunate that this gets down to noding before panicking.
griesemer

griesemer commented on Jun 18, 2025

@griesemer
Contributor

I agree that using Invalid to mark a type as incomplete is problematic because it implies that an error was reported somewhere when it wasn't. Now that we have an actual Alias type we can probably use a flag. I actually have a TODO (in my personal log) about this: "Use bool to mark Alias as initialized". See also the TODO in Checker.typeDecl (decl.go).

I also agree that upon successful completion of type checking, there shouldn't be any Invalid types anywhere. That's a harder invariant to establish.

mrkfrmn

mrkfrmn commented on Jun 23, 2025

@mrkfrmn
Contributor

It seems that doing this via the Alias type is appropriate. I checked go/types and the issue replicates there as expected. Interestingly, with gotypesalias=0, the check added in CL 379916 is reported:

markfreeman-macbookpro:gotypes markfreeman$ GODEBUG=gotypesalias=0 go run main.go
2025/06/23 14:03:52 error type checking: example.go:10:5: invalid use of type alias AB in recursive type (see go.dev/issue/50729)
exit status 1

It's likely fine to leave the infrastructure without explicit alias types alone. Also considered removing gotypesalias prior to this work, but we must support that until at least 1.27 (thanks @findleyr for the pointer).

I'll look at handling this via flag where gotypesalias=1.

gopherbot

gopherbot commented on Jun 25, 2025

@gopherbot
Contributor

Change https://go.dev/cl/683796 mentions this issue: go/types, types2: use nil to represent incomplete explicit aliases

mrkfrmn

mrkfrmn commented on Jun 27, 2025

@mrkfrmn
Contributor

After some slight adjustment to handle the case of partial type checking, things seem to be in order. I'm going to consider this all but submitted.

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

Metadata

Metadata

Assignees

Labels

BugReportIssues describing a possible bug in the Go implementation.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

Status

Todo

Relationships

None yet

    Development

    No branches or pull requests

      Participants

      @mknyszek@adamlouis@griesemer@gopherbot@thepudds

      Issue actions

        cmd/compile: internal compiler error on pointer to recursive type alias · Issue #74181 · golang/go