Skip to content
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

cmd/compile: field reference not working for type parameter with structural constraint #50417

Open
akutz opened this issue Jan 3, 2022 · 22 comments

Comments

@akutz
Copy link

@akutz akutz commented Jan 3, 2022

What version of Go are you using (go version)?

$ go version
go version go1.18beta1 darwin/amd64

Does this issue reproduce with the latest release?

na. This is only currently possible in 1.18beta1.

What operating system and processor architecture are you using (go env)?

go env Output
$ go env
GO111MODULE=""
GOARCH="amd64"
GOBIN=""
GOCACHE="/Users/akutz/Library/Caches/go-build"
GOENV="/Users/akutz/Library/Application Support/go/env"
GOEXE=""
GOEXPERIMENT=""
GOFLAGS=""
GOHOSTARCH="amd64"
GOHOSTOS="darwin"
GOINSECURE=""
GOMODCACHE="/Users/akutz/Projects/go/pkg/mod"
GONOPROXY=""
GONOSUMDB=""
GOOS="darwin"
GOPATH="/Users/akutz/Projects/go"
GOPRIVATE=""
GOPROXY="https://proxy.golang.org,direct"
GOROOT="/Users/akutz/.go/active"
GOSUMDB="sum.golang.org"
GOTMPDIR=""
GOTOOLDIR="/Users/akutz/.go/active/pkg/tool/darwin_amd64"
GOVCS=""
GOVERSION="go1.18beta1"
GCCGO="gccgo"
GOAMD64="v1"
AR="ar"
CC="clang"
CXX="clang++"
CGO_ENABLED="1"
GOMOD="/Users/akutz/Projects/controller-runtime/go.mod"
GOWORK=""
CGO_CFLAGS="-g -O2"
CGO_CPPFLAGS=""
CGO_CXXFLAGS="-g -O2"
CGO_FFLAGS="-g -O2"
CGO_LDFLAGS="-g -O2"
PKG_CONFIG="pkg-config"
GOGCCFLAGS="-fPIC -arch x86_64 -m64 -pthread -fno-caret-diagnostics -Qunused-arguments -fmessage-length=0 -fdebug-prefix-map=/var/folders/0c/ffr4wl4d37924s44ctztx_ym00bgms/T/go-build1780269713=/tmp/go-build -gno-record-gcc-switches -fno-common"

What did you do?

Over the holidays I started digging into Golang generics, and after much consideration, I decided I wanted to create a constraint like so:

type Condition[T ~string, S ~string] interface {
	~struct{
		Type               T
		Status             string
		Severity           S
		LastTransitionTime int64
		Reason             string
		Message            string
	}
}

It is my understanding the above constraint should be valid based upon the following example from the type parameter proposal (from the section Composite Types in Constraints):

// structField is a type constraint whose type set consists of some
// struct types that all have a field named x.
type structField interface {
	struct { a int; x int } |
		struct { b int; x float64 } |
		struct { c int; x uint64 }
}

// This function is INVALID.
func IncrementX[T structField](p *T) {
	v := p.x // INVALID: type of p.x is not the same for all types in set
	v++
	p.x = v
}

While the above example shows the function is INVALID, it also implies that such a constraint would be valid if not for the mixed-type of x. However, when I tried to use this in practice, I received the following error:

c.Type undefined (interface Condition has no method Type)

I was able to replicate this in the Go 1.18 playgound with the following example:

package main

import (
	"fmt"
)

type Cat struct {
	Name string
}

type Dog struct {
	Name string
}

type Person struct {
	Name string
}

type Named interface {
	~struct{ Name string }
}

func SayName[T Named](t T) {
	fmt.Println(t.Name)
}

func main() {
	SayName(Person{Name: "Andrew"})
}

The above program produces the following error:

./prog.go:24:16: t.Name undefined (interface Named has no method Name)

Go build failed.

What did you expect to see?

I expected the program to build and print the line Andrew.

What did you see instead?

The aforementioned compile error:

./prog.go:24:16: t.Name undefined (interface Named has no method Name)

Go build failed.

It would be incredibly useful for this constraint to work as I understand it from the proposal. Perhaps:

  • I misunderstood the proposal
  • The proposal is not up-to-date with the implementation
  • The implementation is incomplete in 1.18beta1

Regardless, the reason I would like this to work is as follows:

  • Using the above example, imagine C Condition[T, S]
  • Now imagine a value of c C
  • If I want to set the Type field on c, I can just do c.Type = ...
  • But without the above constraint, I would have to change to something like the following:
    type Condition[T ~string, S ~string] interface {
    	SetType(type3 T)
    }
  • The problem with the above is that if I have a struct that implements the interface Condition[T, S], that means the struct must have a function like func (c *MyCondition) SetType(type3 MyConditionType).
  • The receiver for the setter function is necessarily by address
  • Which means it is not MyCondition that satisfies the constraint Condition[T, S], but *MyCondition
  • This greatly complicates things, and it would be much nicer to constrain based on the ~struct {...} and to use those fields directly

Thanks!

@ianlancetaylor ianlancetaylor changed the title Struct constraints not working in 1.18 as expected per the Type Parameter proposal cmd/compile: field reference not working for type parameter with structural constraint Jan 3, 2022
@ianlancetaylor ianlancetaylor added this to the Go1.18 milestone Jan 3, 2022
@akutz
Copy link
Author

@akutz akutz commented Jan 3, 2022

@ianlancetaylor Thank you for fixing the title 😃 . I should have realized it was the cmd/compile package, doh!

@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jan 3, 2022

CC @griesemer

It does seem to me that this should be permitted. But even if I'm right it's quite possible that this will not be implemented for 1.18, but will have to wait for a later release.

@bitfield
Copy link

@bitfield bitfield commented Jan 4, 2022

A smaller repro: https://go.dev/play/p/bJ1QGu1MU38?v=gotip

I'm working on a book about generics in Go (self-promotion klaxon), and one of my beta readers sent me more or less exactly this as a bug report. When I looked at it, I was confused too. If the constraint is precisely that T is a struct with an int field x, then shouldn't we be able to set the x field on a T parameter? It certainly does seem that we should. Indeed, if we can't reference a struct field in a generic function constrained on that struct, that rather limits the usefulness of such functions! And, by extension, constraints involving struct types.

I need to explain in the book why this apparently intuitive idea doesn't work (yet); how do you think I should frame that explanation? Should I say something like "this doesn't work due to a known compiler bug #50417"? Or is it rather a misunderstanding of the type parameters proposal which I can clarify? (Perhaps the spec should clarify it.) Or is it something like "this definitely should work, but it is difficult to implement, so we haven't done it yet—but we're working on it."?

@akutz
Copy link
Author

@akutz akutz commented Jan 4, 2022

smaller repro

Oh come on, by just a few lines ;)

Anyway, thank you for the follow-up on this as the more traction we get on it, hopefully the more of a chance there will be it is addressed for 1.18 :)

@danscales
Copy link

@danscales danscales commented Jan 4, 2022

See issue #50233 as well . We currently allow struct initialization of interface types with a single struct type, but not reading/writing of individual fields in expressions/assignments, etc. Still to be decided whether we should support both or not support both for Go 1.18, but we will aim to become consistent. We should be deciding soon.

@akutz
Copy link
Author

@akutz akutz commented Jan 4, 2022

Thanks @danscales! I strongly prefer option 3 from #50233 (support setting x.b1) as it can avoid constraints using interfaces with mutating functions (i.e. setters), which necessarily have to be defined on an address, not the struct itself. This results in the situation where new(T) ends up being **T instead of just *T. A structural constraint would also enable var t T to define a new instance of T, where such behavior would not work for constraints expressed with interfaces.

@akutz
Copy link
Author

@akutz akutz commented Jan 4, 2022

Per #50233 (comment), I also verified it is currently possible to read a field from a structural constraint, just not set it. Thanks again Dan!

@danscales
Copy link

@danscales danscales commented Jan 5, 2022

As Ian and I mention in #50233 (comment) and following , reading a field of a type param that has a structural constraint does not work. The example given is not a read of a type param with a structural constraint, but a read of a non-generic type (which has been returned by a generic function call).

@gopherbot
Copy link

@gopherbot gopherbot commented Jan 5, 2022

Change https://golang.org/cl/375795 mentions this issue: cmd/compile/internal/types2: implement field access for struct structural constraints

@gopherbot
Copy link

@gopherbot gopherbot commented Jan 5, 2022

Change https://golang.org/cl/375794 mentions this issue: cmd/compile/internal/types2: minor restructuring of lookup code in call

@gopherbot
Copy link

@gopherbot gopherbot commented Jan 5, 2022

Change https://golang.org/cl/375514 mentions this issue: cmd/compile/internal/types2: remove unused code in lookupFieldOrMethod

gopherbot pushed a commit that referenced this issue Jan 6, 2022
The underlying type of a type parameter is an interface,
so we don't need a special case for type parameters anymore.
Simply share the (identical) code for interfaces.

Adjust code in types.NewMethodSet accordingly.

No functional difference.
Preparation for fix of issues below.

For #50233.
For #50417.

Change-Id: Ib2deadd12f89e6918dec224b4ce35583001c3101
Reviewed-on: https://go-review.googlesource.com/c/go/+/375514
Trust: Robert Griesemer <gri@golang.org>
Run-TryBot: Robert Griesemer <gri@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
gopherbot pushed a commit that referenced this issue Jan 6, 2022
…aints

This change implements field the access p.f where the type of p
is a type parameter with a structural constraint that is a struct
with a field f. This is only the fix for the type checker. The
compiler will need a separate CL.

This makes the behavior consistent with the fact that we can
write struct composite literals for type parameters with a
struct structural type.

For #50417.
For #50233.

Change-Id: I87d07e016f97cbf19c45cde19165eae3ec0bad2b
Reviewed-on: https://go-review.googlesource.com/c/go/+/375795
Trust: Robert Griesemer <gri@golang.org>
Run-TryBot: Robert Griesemer <gri@golang.org>
TryBot-Result: Gopher Robot <gobot@golang.org>
Reviewed-by: Robert Findley <rfindley@google.com>
@griesemer
Copy link
Contributor

@griesemer griesemer commented Jan 6, 2022

This is now implemented on the type-checker side.

@gopherbot
Copy link

@gopherbot gopherbot commented Jan 7, 2022

Change https://golang.org/cl/376174 mentions this issue: test/typeparam: adjust test preamble (fix longtests)

@gopherbot
Copy link

@gopherbot gopherbot commented Jan 7, 2022

Change https://golang.org/cl/376194 mentions this issue: cmd/compile: support field access for typeparam with structural constraint

gopherbot pushed a commit that referenced this issue Jan 7, 2022
For #50417.

Change-Id: Ic55727c454ec342354f7fbffd22aa350e0d392c2
Reviewed-on: https://go-review.googlesource.com/c/go/+/376174
Trust: Robert Griesemer <gri@golang.org>
Run-TryBot: Robert Griesemer <gri@golang.org>
Reviewed-by: Dan Scales <danscales@google.com>
Trust: Dan Scales <danscales@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
@akutz
Copy link
Author

@akutz akutz commented Jan 14, 2022

Should this be working yet in the tip playground? The following example still fails (Golang playground):

package main

import (
	"fmt"
)

func SomeFunc[S ~struct{ Name string }](s S) {
	fmt.Println(s.Name)
}

func main() {
	SomeFunc(struct{ Name string }{Name: "fake"})
}

The failure is:

./prog.go:12:10: internal compiler error: assertion failed

The full stacktrace can be obtained by running the playground example.

@bmizerany
Copy link
Contributor

@bmizerany bmizerany commented Jan 14, 2022

I tried last night and got the same error on tip. I don't see the commit on main yet though.

@akutz
Copy link
Author

@akutz akutz commented Jan 14, 2022

Thanks for verifying. I am working on a repo called Go generics the hard way and I just added a link to this issue to the page on structural constraints.

However, given @griesemer's comment that this was working in the type checker and compiler side (#50233 (comment)), I also thought I would check to see if this was working in the tip playground yet.

Thank you again for verifying it is not @bmizerany, I appreciate it.

@bmizerany
Copy link
Contributor

@bmizerany bmizerany commented Jan 14, 2022

@akutz It is working in the CL most recently posted. I see Andrew when running with gotip download 376194 && gotip run x.go

@akutz
Copy link
Author

@akutz akutz commented Jan 14, 2022

I am sorry, I am confused. Is https://gotipplay.golang.org not using the tip?

@griesemer
Copy link
Contributor

@griesemer griesemer commented Jan 14, 2022

The necessary CL has not been submitted yet as of now. This is not working at tip (only type-checking works).
cc: @danscales

@akutz
Copy link
Author

@akutz akutz commented Jan 14, 2022

I don't see the commit on main yet though.

Ah, I think I understand. It works in that changelist, and you meant the commit has not been merged to main which the "tip" playground uses. I was confused because you then said gotip download, and I just ignored the number after that. It was not until I tried that I realized it was downloading a specific changelist (hence the CL).

My apologies. This is because I just have not used the Go dev builds or CLs in the past. TIL 😃

@akutz
Copy link
Author

@akutz akutz commented Jan 14, 2022

Yep, it works like you said @bmizerany, I just had to actually follow the instructions to use the changelist:

$ gotip download 376194
This will download and execute code from golang.org/cl/376194, continue? [y/n] y
Fetching CL 376194, Patch Set 11...
remote: Finding sources: 100% (489313/489313)
remote: Total 489313 (delta 354730), reused 480761 (delta 354730)
Receiving objects: 100% (489313/489313), 392.70 MiB | 36.45 MiB/s, done.
Resolving deltas: 100% (354730/354730), done.
From https://go.googlesource.com/go
 * branch                  refs/changes/94/376194/11 -> FETCH_HEAD
Previous HEAD position was 3b5eec9370 runtime/race: be less picky about test run time
HEAD is now at f037b818eb cmd/compile: support field access for typeparam with structural constraint
Building Go cmd/dist using /Users/akutz/.go/active. (go1.18beta1 darwin/amd64)
Building Go toolchain1 using /Users/akutz/.go/active.
Building Go bootstrap cmd/go (go_bootstrap) using Go toolchain1.
Building Go toolchain2 using go_bootstrap and Go toolchain1.
Building Go toolchain3 using go_bootstrap and Go toolchain2.
Building packages and commands for darwin/amd64.
---
Installed Go for darwin/amd64 in /Users/akutz/sdk/gotip
Installed commands in /Users/akutz/sdk/gotip/bin
Success. You may now run 'gotip'!
$ cat x.go
package main

import (
	"fmt"
)

func SomeFunc[S ~struct{ Name string }](s S) {
	fmt.Println(s.Name)
}

func main() {
	SomeFunc(struct{ Name string }{Name: "fake"})
}
$ gotip run x.go       
fake

Thanks again to both of you, @bmizerany and @griesemer.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
7 participants