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: spurious promotion of methods from embedded aliases of unnamed types #66540

Open
adonovan opened this issue Mar 26, 2024 · 11 comments
Assignees
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Milestone

Comments

@adonovan
Copy link
Member

adonovan commented Mar 26, 2024

I was surprised to learn yesterday that it is legal to embed an alias type whose underlying type is not a defined type.

type strings = []string
type S struct { strings } // equivalent to 'strings []string'
var _ = new(S).strings

This is legal according to the spec; there is supposed to be no promotion of methods from the alias's underlying type.

And yet promotion does occur:
https://go.dev/play/p/_bJZeiZW-cW

package main

import "io"

type S struct{ A }

var _ = new(S).A

type A = struct{ io.Reader }

func main() {
	if false {
		new(S).Read(nil) // not a legal program
	}
}

I expect go/types has the same issue.

@gopherbot gopherbot added the compiler/runtime Issues related to the Go compiler and/or runtime. label Mar 26, 2024
@thanm thanm added the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Mar 26, 2024
@Merovius
Copy link
Contributor

Personally, I don't find the promotion to be surprising. struct{ io.Reader } has a method Read, so I expect A to have one and I expect that to be promoted. Also, the spec says:

A field or method f of an embedded field in a struct x is called promoted if x.f is a legal selector that denotes that field or method f.

Which IMO does say that the method is promoted.

OTOH it also says two paragraphs later

Given a struct type S and a named type T, promoted methods are included in the method set of the struct as follows:

Which restricts promoted methods in the method set to those of named types.

So, arguably, new(S).Read is a valid selector expression, but *S is not an io.Reader (similar to how you can call pointer-methods on a value, but the value does not implement io.Reader). Or something?

@adonovan
Copy link
Member Author

adonovan commented Mar 27, 2024

Personally, I don't find the promotion to be surprising. struct{ io.Reader } has a method Read, so I expect A to have one and I expect that to be promoted.

I was surprised to learn that embedding a non-named type was legal at all, so my intuition may not be a good guide here. The specified (as opposed to implemented) behavior of promotion makes more sense to me because I have always thought of aliases as being largely melted away. But surprising or not, the spec disallows it (see below).

[spec] A field or method f of an embedded field in a struct x is called promoted if x.f is a legal selector that denotes that field or method f.
[merovius] Which IMO does say that the method is promoted.

That's the definition of promotion, not a description of when it applies. The paragraph that follows explains the mechanism:

[spec] Given a struct type S and a named type T, promoted methods are included in the method set of the struct as follows: [etc]

and the definition of named types is:

[spec] Predeclared types, defined types, and type parameters are called named types. An alias denotes a named type if the type given in the alias declaration is a named type.

So, an alias for (say) a slice type is not a named type, though it is a legal anonymous field, according to:

[spec] A field declared with a type but no explicit field name is called an embedded field. An embedded field must be specified as a type name T or as a pointer to a non-interface type name *T, and T itself may not be a pointer type. The unqualified type name acts as the field name.

@go101
Copy link

go101 commented Mar 27, 2024

Given a struct type S and a named type T,

I believe "named type" should be "type name" here. The "named type` term returned back to spec at Go 1.18.
Maybe, the typo was made at that time.

@Merovius
Copy link
Contributor

Merovius commented Mar 27, 2024

Nope.

Funnily enough, I proposed the "type name" phrasing. But @griesemer opted for "named type" instead. Maybe he has an opinion. IMO if the spec and implementation have been disagreeing for several Go versions, the spec is wrong.

(I also completely forgot that I reported it and I used exactly @adonovan's example to argue for that change)

@go101
Copy link

go101 commented Mar 27, 2024

The embedding-related wording is really subtle. I still think there are some inaccuracies there: #22005

@mknyszek mknyszek added this to the Go1.23 milestone Mar 27, 2024
@griesemer
Copy link
Contributor

Per this phrase, where we now use "named type" (formerly "defined type"), it seems to me that the implementation is not correct. Per the spec, an alias is only a named type type if it aliases a named type, such as a defined type:

Predeclared types, defined types, and type parameters are called named types. An alias denotes a named type if the type given in the alias declaration is a named type.

But in this case A is an alias for a type literal (an unnamed struct). I'd think that the methods of that type shouldn't be promoted (that was the original meaning, before aliases).

Can we change (fix) this? Maybe. It seems that the chance that code such exists in the wild is pretty slim; at the very least it's not going to be a lot of code. But we'd need to do a little investigation first.

@Merovius
Copy link
Contributor

@griesemer I'll point out again that in the issue I linked above I observed this behavior of promoted method and prompted that change. So IMO that change was made specifically to bring the spec in line with this behavior. I don't remember if I filed that issue based on a real world example.

At the time, I suggested "type name", specifically because it includes aliases that are not named types (and thus to address my example). I'm not sure why you chose "named type" instead; neither the issue description, nor the commit message mention.

@go101
Copy link

go101 commented Mar 29, 2024

Per the fact that an interface can embed any types, including struct literals, maybe the current implementation is preferred?

@bjorndm
Copy link

bjorndm commented Mar 30, 2024

I used this in a project for my job, no source available. It is convenient and makes sense.

@griesemer griesemer added the NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made. label Apr 1, 2024
@gopherbot gopherbot removed the NeedsInvestigation Someone must examine and confirm this is a valid issue and not a duplicate of an existing one. label Apr 1, 2024
@griesemer
Copy link
Contributor

@Merovius I see. The commit message says "The types of embedded fields must be named, but they don't need to be defined types" which implies that I meant any type with a name (i.e., with a type name). Yet, the change mentions named type, which excludes aliases denoting type literals, and - which I believe - was my intention as it simply used the more general term (named type) for the original term (defined type). Sigh.

The implementation does something else, the spec says something else in most places, and so maybe this issue is fixed by simply changing "named type" to "type name" in "Given a struct type S and a name type T, ...".

(It's really unfortunate, and a mistake in my mind, that we allowed aliases for type literals. There was no need for it, and it needlessly complicates/confuses things.)

@adonovan
Copy link
Member Author

adonovan commented Apr 1, 2024

The implementation does something else, the spec says something else in most places, and so maybe this issue is fixed by simply changing "named type" to "type name" in "Given a struct type S and a name type T, ...".

That would solve the inconsistency, but it seems like a pragmatic admission of defeat relative to your original intent, which makes more sense to me.

It's ... a mistake in my mind, that we allowed aliases for type literals.

Inessential they may be, but type aliases for unnamed types are quite useful as documentable abbreviations for verbose types that have no need for declared methods, function types in particular.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
compiler/runtime Issues related to the Go compiler and/or runtime. NeedsDecision Feedback is required from experts, contributors, and/or the community before a change can be made.
Projects
Development

No branches or pull requests

8 participants