Skip to content

go/types: can't check interface satisfaction against interfaces defined in package builtin #69717

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
matttproud opened this issue Sep 30, 2024 · 5 comments
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Milestone

Comments

@matttproud
Copy link
Contributor

Go version

go version go1.23-20240626-RC01 cl/646990413 +5a18e79687 X:fieldtrack,boringcrypto linux/amd64

Output of go env in your module/workspace:

GO111MODULE=''
GOARCH='amd64'
GOBIN=''
GOCACHE='/usr/local/google/home/mtp/.cache/go-build'
GOENV='/usr/local/google/home/mtp/.config/go/env'
GOEXE=''
GOEXPERIMENT='fieldtrack,boringcrypto'
GOFLAGS=''
GOHOSTARCH='amd64'
GOHOSTOS='linux'
GOINSECURE=''
GOMODCACHE='/usr/local/google/home/mtp/go/pkg/mod'
GONOPROXY=''
GONOSUMDB=''
GOOS='linux'
GOPATH='/usr/local/google/home/mtp/go'
GOPRIVATE=''
GOPROXY='https://proxy.golang.org,direct'
GOROOT='/usr/lib/google-golang'
GOSUMDB='sum.golang.org'
GOTMPDIR=''
GOTOOLCHAIN='auto'
GOTOOLDIR='/usr/lib/google-golang/pkg/tool/linux_amd64'
GOVCS=''
GOVERSION='go1.23-20240626-RC01 cl/646990413 +5a18e79687 X:fieldtrack,boringcrypto'
GODEBUG=''
GOTELEMETRY='local'
GOTELEMETRYDIR='/usr/local/google/home/mtp/.config/go/telemetry'
GCCGO='gccgo'
GOAMD64='v1'
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 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -Wl,--no-gc-sections -fmessage-length=0 -ffile-prefix-map=/tmp/go-build3848246879=/tmp/go-build -gno-record-gcc-switches'

What did you do?

I built a small program to walk a type-checked AST to determine whether identifiers implement a given interface (for fun interface error from package builtin). I used it to check if io.EOF implements error:

package main

import (
	"fmt"
	"go/types"

	"golang.org/x/tools/go/packages"
)

func main() {
	cfg := &packages.Config{
		Mode: packages.NeedTypes | packages.NeedImports | packages.NeedSyntax | packages.NeedTypesInfo,
	}
	builtinPkg, err := packages.Load(cfg, "builtin")
	if err != nil {
		panic(err)
	}
	errorType := builtinPkg[0].Types.Scope().Lookup("error").Type().Underlying().(*types.Interface)
	ioPkg, err := packages.Load(cfg, "io")
	if err != nil {
		panic(err)
	}
	eofObj := ioPkg[0].Types.Scope().Lookup("EOF")
	fmt.Println(types.Implements(eofObj.Type(), errorType))
}

What did you see happen?

It reports false.

What did you expect to see?

If I hand-compose the interface error using package types it works, but it is rather cumbersome:

package main

import (
	"fmt"
	"go/types"

	"golang.org/x/tools/go/packages"
)

func main() {
	cfg := &packages.Config{
		Mode: packages.NeedTypes | packages.NeedImports | packages.NeedSyntax | packages.NeedTypesInfo,
	}
	ioPkg, err := packages.Load(cfg, "io")
	if err != nil {
		panic(err)
	}
	intermediateType := types.NewInterfaceType([]*types.Func{
		types.NewFunc(0, nil, "Error",
			types.NewSignature(
				nil,
				types.NewTuple(),
				types.NewTuple(types.NewParam(0, nil, "", types.Typ[types.String])),
				false,
			),
		),
	}, nil)
	intermediateType.Complete()
	errorType := types.NewNamed(types.NewTypeName(0, nil, "error", nil), intermediateType, nil).Underlying().(*types.Interface)
	eofObj := ioPkg[0].Types.Scope().Lookup("EOF")
	fmt.Println(types.Implements(eofObj.Type(), errorType))
}

It reports true.

This is somewhat understandable since package builtin is a bit of a stub for things predeclared items in the universe block. Nevertheless, the failure is a bit surprising.

@dominikh
Copy link
Member

I can't speak to why io.EOF doesn't implement builtin.error, but there is a much simpler alternative to your cumbersome solution: looking up error in types.Universe. The same goes for any other identifier in the universe scope.

@matttproud
Copy link
Contributor Author

That certainly works. I appreciate the shortcut.

I think I'll leave the issue open since the bug was about a minimal viable example of working with a typechecked package builtin.

@mknyszek mknyszek added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Sep 30, 2024
@mknyszek mknyszek added this to the Backlog milestone Sep 30, 2024
@mknyszek
Copy link
Contributor

CC @golang/compiler

@griesemer
Copy link
Contributor

As @dominikh points out, using types.Universe.Lookup("error") gives you the correct error type and then your code reports the correct answer.

The package builtin not only declares the error type, but also its own "basic" types, specifically the string type. So the error type declared by builtin has an Error method returning a builtin.string instead of a string. Which is why io.EOF cannot implement it.

The builtin package really is for documentation purposes only, using it as a proper package will produce suprising results.
Closing this as working as expected.

PS: The following code illustrates the problem by printing the error types' underlying types:

package main

import (
	"fmt"
	"go/types"

	"golang.org/x/tools/go/packages"
)

func main() {
	cfg := &packages.Config{
		Mode: packages.NeedTypes | packages.NeedImports | packages.NeedSyntax | packages.NeedTypesInfo,
	}
	builtinPkg, err := packages.Load(cfg, "builtin")
	if err != nil {
		panic(err)
	}

	builtinError := builtinPkg[0].Types.Scope().Lookup("error").Type().Underlying().(*types.Interface)
	universeError := types.Universe.Lookup("error").Type().Underlying().(*types.Interface)
	fmt.Printf("builtinError = %s\n", builtinError)
	fmt.Printf("universeError = %s\n", universeError)

	ioPkg, err := packages.Load(cfg, "io")
	if err != nil {
		panic(err)
	}
	eofObj := ioPkg[0].Types.Scope().Lookup("EOF")

	fmt.Println(types.Implements(eofObj.Type(), builtinError))
	fmt.Println(types.Implements(eofObj.Type(), universeError))
}

The result is:

builtinError = interface{Error() builtin.string}
universeError = interface{Error() string}
false
true

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one.
Projects
None yet
Development

No branches or pull requests

5 participants