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

go/types: API changes to support explicit Alias nodes #63223

Closed
griesemer opened this issue Sep 25, 2023 · 28 comments
Closed

go/types: API changes to support explicit Alias nodes #63223

griesemer opened this issue Sep 25, 2023 · 28 comments

Comments

@griesemer
Copy link
Contributor

griesemer commented Sep 25, 2023

Background

Currently, when a type alias declaration

type A = some_type

is type-checked, go/types creates a TypeName Object for the type alias A but the type of the TypeName is the type of some_type. When A is used as identifier in source code, the denoted type is some_type, which if it's a defined type will have a different name than A; and if it's not a defined type, it will be a type literal, which is also not called A. As a consequence, an error message related to the type A will not report A but some_type or whatever type A is a alias for. This is a long-standing issue that we'd like to fix.

Furthermore, with proposal #46477 accepted (parameterized type aliases), currently go/types doesn't have a way to represent a parameterized alias type.

Both these problems can be addressed with an explicit Alias type node. This proposal is about introducing such a new type node.

Proposal

We introduce a new go/types node called Alias:

// An Alias represents an alias type.
type Alias struct {
	obj     *TypeName // corresponding declared alias object
	fromRHS Type      // RHS of type alias declaration; may be an alias
	actual  Type      // actual (aliased) type; never an alias

	// additional fields for type parameters
}

// Obj returns the type name for the declaration defining the alias a.
func (a *Alias) Obj() *TypeName { return a.obj }

// Methods supported by all types
func (*Alias) Underlying() Type
func (*Alias) String() string 

An Alias node stands for a type alias. An Alias node is created with a factory function:

func NewAlias(obj *TypeName, rhs Type) *Alias

When an Alias node is encountered, go/types and related tool clients may need to indirect to the actual type the Alias node is a type alias for. This can be done through a new function

// Unalias returns t if it is not an alias type;
// otherwise it follows t's alias chain until it
// reaches a non-alias type which is then returned.
// Consequently, the result is never an alias type.
func Unalias(t Type) Type

Tools that care about the actual un-aliased type will need to call Unalias everywhere in the tools code where there is a type assertion or type switch and the type may be an alias type.

This is a non-backward compatible change to go/types and it may require pervasive changes to tools, with a potentially long bug tail. For that reason we also propose to introduce a new types.Config flag:

// If EnableAlias is set, alias declarations produce an Alias type.
// Otherwise the alias information is only in the type name, which
// points directly to the actual (aliased) type.
EnableAlias bool

Unless EnableAlias is set, go/types behaves exactly as before. If EnableAlias is set, Alias nodes appear for type alias declarations, and clients of go/types will need to be aware of that.

Discussion

This proposal only introduces the Alias node functionality, without support for type parameters. We believe this is a significant change which should be done separately from support for parameterized alias types, ideally for Go 1.22. Once the new API is settled and clients have adjusted, we would then extend the Alias node with support for type parameters and instantiation, presumably for Go 1.23. As that step doesn't introduce a new type node, we expect it to be easier to adjust clients.

Unfortunately we don't see a way around introducing an explicit Alias node if we want to support parameterization, and therefore we don't see a way to support parameterized alias types in a backward-compatible way. The EnableAlias configuration flag should help ease the transition: tools will continue to work as before if this proposal is accepted. Only when they set the configuration flag will they need to be aware of the new type node.

Implementation

An initial implementation can be found here: CL 521956 with unexported API changes.
The compiler doesn't set EnableAlias yet. Also, changes to the compiler (calls to Unalias) and exporters/importers are missing.

@griesemer griesemer added this to the Go1.22 milestone Sep 25, 2023
@griesemer griesemer self-assigned this Sep 25, 2023
@griesemer
Copy link
Contributor Author

cc: @findleyr @ianlancetaylor @mdempsky

@mvdan
Copy link
Member

mvdan commented Sep 25, 2023

I would love this for tools - I have struggled with not being able to distinguish aliases before. The approach with the option sounds good to me.

Is _Alias a typo in the godoc? Not sure what the underscore is for.

@griesemer
Copy link
Contributor Author

@mvdan The CL implements this proposal but the proposal needs to be discussed and accepted first. Once we're confident that the CL is ready we may submit it, with the API changes unexported (with a leading _). We plan to adjust (as needed) and export once the proposal is accepted.

@mvdan
Copy link
Member

mvdan commented Sep 26, 2023

To be clear, I only mean in this issue body - the type is declared as Alias, but referred to as _Alias in a godoc.

@griesemer
Copy link
Contributor Author

@mvdan I fixed one typo. Not sure if there's others.

@findleyr
Copy link
Contributor

This proposal looks good, and I'm excited for the simplifications this will enable in gopls.

I have some questions about the details:

  • In NewAlias, should we pass in the RHS, or the actual type? For NewNamed, we pass in the underlying and panic if the underlying is a Named type. Should we do the same for Alias, accepting an actual type, and panicking if actual is an alias? This matters for import/export: we have no way to access the RHS type and therefore couldn't preserve it in export data.
  • provocative speculation: Could we have -lang=go1.23 imply EnableAlias, and avoid supporting the mode where we only produce alias nodes if they are generic? Any tools supporting 1.23 will have to deal with Alias anyway, and it avoids the strange partial representation. Unfortunately, I don't think we can avoid the EnableAlias flag entirely because we can't break tools that currently work find for older Go versions.

@mvdan
Copy link
Member

mvdan commented Sep 27, 2023

I have struggled with not being able to distinguish aliases before.

If you want more specifics, and fancy getting horrified by my workaround, see: https://github.com/burrowers/garble/blob/82834ace20257dcbb4a9bc9493cce6d69418f4b7/main.go#L1383-L1389

That entire chunk of code could be replaced by this new option and Alias type, as far as I can tell!

@findleyr
Copy link
Contributor

Oh, are we sharing workarounds :)? This entire function could probably be replaced by types.TypeString, if go/types handles aliases: https://cs.opensource.google/go/x/tools/+/master:gopls/internal/lsp/source/types_format.go;l=270;drc=5fc00b44cd9b4227deece2f4611a2e3d4ce47475

@timothy-king
Copy link
Contributor

Should there also be a helper function that combines Unalias() and Underlying() until a non-named type is reached? My guess is most [but not all] uses of Underlying() will need to be replaced by this helper. (Similar to the CoreType replacements in x/tools.) We can always delay introducing this to go/types seeing if this is indeed the case.

@mdempsky
Copy link
Member

Alternative idea: what if Object.Type() and Type.Underlying() continue to behave the same as today, but we add a method Alias() *Alias to the Type interface, which points back to the Alias that the type was originally accessed through, if any.

So like type A = *int would create a TypeName for A, whose Type() would be a copy of the type.Pointer that represents *int, but calling Alias() on it would return the Alias representing A. (Caveat: for GODEBUG below, we would define that it may instead return nil if alias information isn't available.)

Then existing alias-unaware code continues working the same, while alias-aware code can always find the Alias type when it wants it, even if the Type value came from alias-unaware code.

The only tricky thing would be aliases to Basic and Named. I know within the Go compiler (types1), we assume that defined types always have a unique type instance, so that we can do direct pointer comparisons on them (though we special case any, byte, and rune). I don't see it formally documented in the go/types APIs, so users shouldn't be relying on it, but that doesn't mean they aren't. (Actually, go/types special cases byte and rune basically the same way, by giving them a separate Basic instance.)

So potentially some go/types code would break if we duplicate Basic and Named types so they can store the Alias pointer. We could offer a GODEBUG knob to disable this behavior though. That would allow the problematic alias-unaware code to continue working, while alias-aware code would gracefully degrade to work without Alias information for Named and/or Basic and/or everything.

@dominikh
Copy link
Member

dominikh commented Sep 28, 2023

Should there also be a helper function that combines Unalias() and Underlying() until a non-named type is reached? My guess is most [but not all] uses of Underlying() will need to be replaced by this helper. (Similar to the CoreType replacements in x/tools.) We can always delay introducing this to go/types seeing if this is indeed the case.

@timothy-king can you elaborate on that? Wouldn't a.Underlying() already be the same as Unalias(a).Underlying()?

@findleyr
Copy link
Contributor

@mdempsky, that's an interesting idea: we'd still need an Alias node to represent aliases, but it wouldn't occur in info.Types. This would also align more closely with the specification of the type system. A couple comments:

  • I think the types.Type interface is intentionally open, meaning there were at least intended to be use-cases where external packages implement types.Type. I'm not sure why, but this is my recollection. If indeed this is the case, we can't add an Alias method (though of course we can add a package-level function that can extract alias information).
  • Poking around, there appear to be a number of places that compare directly with types.Typ[...]. With that said, there were also a number of places that assumed Named types were canonical, and it wasn't that much work when we broke this assumption with generics.

Pragmatically, I think it is a simpler change to introduce an Alias node, because so much logic already operates on underlying types. It also doesn't affect the memory footprint of non-alias types, which is a small but nontrivial consideration.

@rsc
Copy link
Contributor

rsc commented Nov 8, 2023

It sounds like the exchange between @mdempsky and @rfindley concluded that the "implicit alias info" approach is not the right one – the breakage there seems subtler than the breakage of being explicit. Please correct me if I'm mistaken about that.

Discussed with @cherrymui, @adonovan, and @griesemer. Inspired by @mdempsky's mention of GODEBUG, we think it makes sense to use a GODEBUG rather than the EnableAlias bool. The GODEBUG infrastructure provides a transition path after which, in a few years, analysis programs just use the current setup instead of every program in the Go ecosystem having to remember to set EnableAlias = true.

Concretely, we would add to go/types:

type Alias struct { ... unexported ... }
func (*Alias) Obj() *TypeName
func (*Alias) Underlying() Type
func (*Alias) String() string 
func NewAlias(obj *TypeName, rhs Type) *Alias
func Unalias(t Type) Type

And then also a new GODEBUG named gotypesalias. gotypesalias=0 means to not generate aliases, gotypesalias=1 means to generate aliases. For Go 1.22, the new API would be added and gotypesalias=0 would remain the default. For Go 1.23, the default would change to gotypesalias=1 (as usual, only in programs that are in a main module with "go 1.23" in the go.mod).

This two-step transition means that programs supporting Go 1.21 and Go 1.22 ignore the new API, and programs supporting Go 1.22 and Go 1.23 use the new API, without needing build tags.

There is one subtle case to consider about the interaction between go/types and compiler export data. In Go 1.23, if gotypesalias=1 is set, we want programs reading compiler export data to see aliases, same as if they type-checked themselves. That means the compiler must export alias information unconditionally. Either the gcimporter or go/types itself would need to de-alias the types as it reads them in.

So the proposal is the new API above and the new GODEBUG gotypesalias defaulting to 0 in Go 1.22. If gotypesalias=1 then the Go 1.22 go/types package would create aliases. @griesemer wasn't sure whether the compiler would be able to create aliases in time for Go 1.22, so perhaps the export data won't have them in Go 1.22, in which case setting gotypesalias=1 but reading compiler export data would find no aliases. That's obviously not ideal but it's also fine, since for the purposes of Go 1.22, gotypesalias=1 can be considered an experimental preview of Go 1.23.

@rsc
Copy link
Contributor

rsc commented Nov 8, 2023

FWIW, one potential way to implement the de-aliasing would be to make NewAlias return a Type instead of a *Alias, and then when gotypesalias=0, NewAlias(obj, rhs) returns rhs directly. I don't know whether that's a good idea but it seemed worth noting.

@findleyr
Copy link
Contributor

findleyr commented Nov 8, 2023

Thanks. This sounds like a good step forward, and GODEBUG avoids the confusing definition for EnableAlias.

FWIW, one potential way to implement the de-aliasing would be to make NewAlias return a Type instead of a *Alias, and then when gotypesalias=0, NewAlias(obj, rhs) returns rhs directly.

I'm unfamiliar with using GODEBUG, but presumably it should be possible to write a wrapper for NewAlias that does this. Therefore we should keep the more specific signature for types.NewAlias.

Overall, the only objection I can think of for this proposal is that an Alias node is not a desirable way to represent aliases. While that may be true if we could start over, I think it is the cleanest way to represent aliases in the current API. My rationale for this is as follows:

  • We have already chosen to store type parameters on the Type, not Object (that ship has sailed)
  • ...therefore we need alias information on the Type
  • ...which means we need either (1) a new Type node, or (2) a new attribute on all types that does not affect their identity
  • ...of the two options, (1) seems like less work to adopt, and less error prone, because it doesn't lead to subtle bugs in logic checking e.g. typ == types.Typ[types.Int]. I think most code working on types starts by taking typ.Underlying(), and that code never needs to be considered. This was also demonstrated by the relative ease with which the new node was added to the type checker. @griesemer would know first-hand.

So, following this logic, I think adding an Alias node is the natural path forward. And once we have chosen this path the rest of the proposal mostly writes itself.

@rsc
Copy link
Contributor

rsc commented Nov 8, 2023

I'm unfamiliar with using GODEBUG, but presumably it should be possible to write a wrapper for NewAlias that does this. Therefore we should keep the more specific signature for types.NewAlias.

Definitely. I was just observing that if the check happened in NewAlias than any code that "created" aliases would be dealiased properly, so that all the potential creators of aliases (gcimporter, gccgoimporter, and go/types itself) wouldn't have to duplicate the logic. I'm happy not to do that, of course.

@rsc
Copy link
Contributor

rsc commented Nov 10, 2023

Based on the discussion above, this proposal seems like a likely accept.
— rsc for the proposal review group

Some new API is added to go/types to represent aliases.

type Alias struct { ... unexported ... }
func (*Alias) Obj() *TypeName
func (*Alias) Underlying() Type
func (*Alias) String() string 
func NewAlias(obj *TypeName, rhs Type) *Alias
func Unalias(t Type) Type

Because returning Alias information may break existing type switches that do not know to check for the Alias type, this functionality will be controlled by a GODEBUG named “gotypesalias”.

When gotypesalias=0, everything behaves as before, and aliases are never generated in the returned types. When gotypesalias=1, aliases are generated and code must expect them.

The gotypesalias setting will also control whether aliases are returned by gcimporter and gccgoimporter. When the compilers start recording aliases in the export information, the importers will have to strip those aliases out when gotypeslias=0.

For Go 1.22, gotypesalias=0 will be the default, and gotypesalias=1 may or may not be fully implemented. (It will probably produce alias info in go/types type-checking, but it may or may not produce aliases in gcimporter type-checking.) Code that needs to work with Go 1.21 and Go 1.22 can ignore gotypealias and these new types entirely.

For Go 1.23, gotypealias=1 will become the default. Code that needs to work with Go 1.22 and Go 1.23 will have the types available for use in type switches, even though they won’t be generated in Go 1.22. This should avoid needing //go:build tags.

@gopherbot
Copy link

Change https://go.dev/cl/541737 mentions this issue: go/types, types2: implement Alias proposal (export API)

gopherbot pushed a commit that referenced this issue Nov 13, 2023
This CL exports the previously unexported Alias type and
corresponding functions and methods per issue #63223.

Whether Alias types are used or not is controlled by
the gotypesalias setting with the GODEBUG environment
variable. Setting gotypesalias to "1" enables the Alias
types:

	GODEBUG=gotypesalias=1

By default, gotypesalias is not set.

Adjust test cases that enable/disable the use of Alias
types to use -gotypesalias=1 or -gotypesalias=0 rather
than -alias and -alias=false for consistency and to
avoid confusion.

For #63223.

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

rsc commented Nov 16, 2023

No change in consensus, so accepted. 🎉
This issue now tracks the work of implementing the proposal.
— rsc for the proposal review group

Some new API is added to go/types to represent aliases.

type Alias struct { ... unexported ... }
func (*Alias) Obj() *TypeName
func (*Alias) Underlying() Type
func (*Alias) String() string 
func NewAlias(obj *TypeName, rhs Type) *Alias
func Unalias(t Type) Type

Because returning Alias information may break existing type switches that do not know to check for the Alias type, this functionality will be controlled by a GODEBUG named “gotypesalias”.

When gotypesalias=0, everything behaves as before, and aliases are never generated in the returned types. When gotypesalias=1, aliases are generated and code must expect them.

The gotypesalias setting will also control whether aliases are returned by gcimporter and gccgoimporter. When the compilers start recording aliases in the export information, the importers will have to strip those aliases out when gotypeslias=0.

For Go 1.22, gotypesalias=0 will be the default, and gotypesalias=1 may or may not be fully implemented. (It will probably produce alias info in go/types type-checking, but it may or may not produce aliases in gcimporter type-checking.) Code that needs to work with Go 1.21 and Go 1.22 can ignore gotypealias and these new types entirely.

For Go 1.23, gotypealias=1 will become the default. Code that needs to work with Go 1.22 and Go 1.23 will have the types available for use in type switches, even though they won’t be generated in Go 1.22. This should avoid needing //go:build tags.

@rsc rsc changed the title proposal: go/types: API changes to support explicit Alias nodes go/types: API changes to support explicit Alias nodes Nov 16, 2023
@mvdan
Copy link
Member

mvdan commented Nov 16, 2023

For Go 1.22, gotypesalias=0 will be the default, and gotypesalias=1 may or may not be fully implemented. (It will probably produce alias info in go/types type-checking, but it may or may not produce aliases in gcimporter type-checking.)

Would love to hear what is the thinking for gcimporter in Go 1.22 when the development is a bit further ahead, since this will affect whether gotypesalias=1 is useful for Go tools using go/packages.

@findleyr
Copy link
Contributor

findleyr commented Nov 16, 2023

@mvdan go/packages uses a fork of the importer in x/tools, so that support is independent of the Go release cycle.
EDIT: ah, but go/packages reads compiler export data (I forgot), so this still depends on the compiler.

gopls uses yet another fork (based on the index export format), which will need to be updated. Using this in gopls will at least give us some experience.

@mvdan
Copy link
Member

mvdan commented Nov 16, 2023

EDIT: ah, but go/packages reads compiler export data (I forgot), so this still depends on the compiler.

Yes, I could have been clearer that that's what I meant. I think the bare minimum for go/packages to support explicit alias nodes in Go 1.22 would be go/types and the export data in the build cache from cmd/go.

@findleyr
Copy link
Contributor

So it won't be usable with go/packages until 1.23 (at least not when using go/package's type checking). Perhaps we can update go/packages to type check from source if gotypesalias=1, if that would help tool authors.

@griesemer
Copy link
Contributor Author

The go/types and types2 changes have been implemented and submitted.
Still missing are the changes to the export data format and importers. This is now tracked through the separate issue #64208.
Closing this one as completed.

@mvdan
Copy link
Member

mvdan commented Nov 16, 2023

Understood, thank you @findleyr. And thanks @griesemer for opening the new issue - following :)

@mvdan
Copy link
Member

mvdan commented Dec 4, 2023

The fact that we had https://pkg.go.dev/go/types@master#TypeName.IsAlias already, and now we've gained https://pkg.go.dev/go/types@master#Alias, makes me slightly uneasy. Looking at the docs with fresh eyes, I honestly couldn't even say when I should use one API over the other.

Should we eventually deprecate the IsAlias method in favor of the Alias type? If not, should we perhaps improve the docs a bit to explain the difference between the two?

@griesemer
Copy link
Contributor Author

I'm sure that the docs can be improved. But IsAlias applies to a TypeName which is an Object. The Alias node represents the Alias type, which is not the same. I don't see a problem here.

@gopherbot
Copy link

Change https://go.dev/cl/546358 mentions this issue: doc: add release note for go/types.Alias type and Unalias function

gopherbot pushed a commit that referenced this issue Dec 5, 2023
Also, add some missing <code></code> tags.

For #63223.

Change-Id: I570b82be830b3c124420c5715ab1165ca53725f9
Reviewed-on: https://go-review.googlesource.com/c/go/+/546358
Auto-Submit: Robert Griesemer <gri@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Griesemer <gri@google.com>
TryBot-Bypass: Robert Griesemer <gri@google.com>
ezz-no pushed a commit to ezz-no/go-ezzno that referenced this issue Feb 18, 2024
Also, add some missing <code></code> tags.

For golang#63223.

Change-Id: I570b82be830b3c124420c5715ab1165ca53725f9
Reviewed-on: https://go-review.googlesource.com/c/go/+/546358
Auto-Submit: Robert Griesemer <gri@google.com>
Reviewed-by: Alan Donovan <adonovan@google.com>
Reviewed-by: Robert Griesemer <gri@google.com>
TryBot-Bypass: Robert Griesemer <gri@google.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
Status: Accepted
Development

No branches or pull requests

8 participants