Skip to content

Commit d28b27c

Browse files
author
Mark Freeman
committed
go/types, types2: use nil to represent incomplete explicit aliases
Using Invalid to represent an incomplete alias is problematic since it implies that an error has been reported somewhere. This causes confusion for observers of invalid aliases trying not to emit follow-on errors. This change uses nil instead to represent an incomplete alias. This has a mild benefit of making alias memoization more convenient. We additionally can now memoize Invalid aliases. This necessitates a minor change to our cycle error reporting for aliases. Care is taken to separate logic according to gotypesalias. Otherwise, a cycle as simple as "type T = T" panics. A test is also added which uses go/types to inspect for Invalid types. Currently, the problematic Invalid does not cause an error in type checking, but rather a panic in noding. Thus, we cannot use the familiar test facilities relying on error reporting. Fixes #74181 Change-Id: Iea5ebce567a2805f5647de0fb7ded4a96f6c5f8d Reviewed-on: https://go-review.googlesource.com/c/go/+/683796 Reviewed-by: Robert Griesemer <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]>
1 parent 7b53d8d commit d28b27c

File tree

5 files changed

+143
-78
lines changed

5 files changed

+143
-78
lines changed

src/cmd/compile/internal/types2/alias.go

Lines changed: 8 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ package types2
66

77
import (
88
"cmd/compile/internal/syntax"
9-
"fmt"
109
)
1110

1211
// An Alias represents an alias type.
@@ -50,7 +49,7 @@ type Alias struct {
5049
}
5150

5251
// NewAlias creates a new Alias type with the given type name and rhs.
53-
// rhs must not be nil.
52+
// If rhs is nil, the alias is incomplete.
5453
func NewAlias(obj *TypeName, rhs Type) *Alias {
5554
alias := (*Checker)(nil).newAlias(obj, rhs)
5655
// Ensure that alias.actual is set (#65455).
@@ -98,6 +97,7 @@ func (a *Alias) Rhs() Type { return a.fromRHS }
9897
// otherwise it follows t's alias chain until it
9998
// reaches a non-alias type which is then returned.
10099
// Consequently, the result is never an alias type.
100+
// Returns nil if the alias is incomplete.
101101
func Unalias(t Type) Type {
102102
if a0, _ := t.(*Alias); a0 != nil {
103103
return unalias(a0)
@@ -113,19 +113,10 @@ func unalias(a0 *Alias) Type {
113113
for a := a0; a != nil; a, _ = t.(*Alias) {
114114
t = a.fromRHS
115115
}
116-
if t == nil {
117-
panic(fmt.Sprintf("non-terminated alias %s", a0.obj.name))
118-
}
119-
120-
// Memoize the type only if valid.
121-
// In the presence of unfinished cyclic declarations, Unalias
122-
// would otherwise latch the invalid value (#66704).
123-
// TODO(adonovan): rethink, along with checker.typeDecl's use
124-
// of Invalid to mark unfinished aliases.
125-
if t != Typ[Invalid] {
126-
a0.actual = t
127-
}
128116

117+
// It's fine to memoize nil types since it's the zero value for actual.
118+
// It accomplishes nothing.
119+
a0.actual = t
129120
return t
130121
}
131122

@@ -137,9 +128,8 @@ func asNamed(t Type) *Named {
137128
}
138129

139130
// newAlias creates a new Alias type with the given type name and rhs.
140-
// rhs must not be nil.
131+
// If rhs is nil, the alias is incomplete.
141132
func (check *Checker) newAlias(obj *TypeName, rhs Type) *Alias {
142-
assert(rhs != nil)
143133
a := new(Alias)
144134
a.obj = obj
145135
a.orig = a
@@ -172,12 +162,6 @@ func (check *Checker) newAliasInstance(pos syntax.Pos, orig *Alias, targs []Type
172162

173163
func (a *Alias) cleanup() {
174164
// Ensure a.actual is set before types are published,
175-
// so Unalias is a pure "getter", not a "setter".
176-
actual := Unalias(a)
177-
178-
if actual == Typ[Invalid] {
179-
// We don't set a.actual to Typ[Invalid] during type checking,
180-
// as it may indicate that the RHS is not fully set up.
181-
a.actual = actual
182-
}
165+
// so unalias is a pure "getter", not a "setter".
166+
unalias(a)
183167
}

src/cmd/compile/internal/types2/decl.go

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -320,11 +320,15 @@ func (check *Checker) cycleError(cycle []Object, start int) {
320320
// If obj is a type alias, mark it as valid (not broken) in order to avoid follow-on errors.
321321
obj := cycle[start]
322322
tname, _ := obj.(*TypeName)
323-
if tname != nil && tname.IsAlias() {
324-
// If we use Alias nodes, it is initialized with Typ[Invalid].
325-
// TODO(gri) Adjust this code if we initialize with nil.
326-
if !check.conf.EnableAlias {
327-
check.validAlias(tname, Typ[Invalid])
323+
if tname != nil {
324+
if check.conf.EnableAlias {
325+
if a, ok := tname.Type().(*Alias); ok {
326+
a.fromRHS = Typ[Invalid]
327+
}
328+
} else {
329+
if tname.IsAlias() {
330+
check.validAlias(tname, Typ[Invalid])
331+
}
328332
}
329333
}
330334

@@ -507,17 +511,18 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *syntax.TypeDecl, def *TypeN
507511
}
508512

509513
if check.conf.EnableAlias {
510-
// TODO(gri) Should be able to use nil instead of Typ[Invalid] to mark
511-
// the alias as incomplete. Currently this causes problems
512-
// with certain cycles. Investigate.
513-
//
514-
// NOTE(adonovan): to avoid the Invalid being prematurely observed
515-
// by (e.g.) a var whose type is an unfinished cycle,
516-
// Unalias does not memoize if Invalid. Perhaps we should use a
517-
// special sentinel distinct from Invalid.
518-
alias := check.newAlias(obj, Typ[Invalid])
514+
alias := check.newAlias(obj, nil)
519515
setDefType(def, alias)
520516

517+
// If we could not type the RHS, set it to invalid. This should
518+
// only ever happen if we panic before setting.
519+
defer func() {
520+
if alias.fromRHS == nil {
521+
alias.fromRHS = Typ[Invalid]
522+
unalias(alias)
523+
}
524+
}()
525+
521526
// handle type parameters even if not allowed (Alias type is supported)
522527
if tparam0 != nil {
523528
if !versionErr && !buildcfg.Experiment.AliasTypeParams {
@@ -531,8 +536,9 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *syntax.TypeDecl, def *TypeN
531536

532537
rhs = check.definedType(tdecl.Type, obj)
533538
assert(rhs != nil)
539+
534540
alias.fromRHS = rhs
535-
Unalias(alias) // resolve alias.actual
541+
unalias(alias) // resolve alias.actual
536542
} else {
537543
if !versionErr && tparam0 != nil {
538544
check.error(tdecl, UnsupportedFeature, "generic type alias requires GODEBUG=gotypesalias=1 or unset")

src/go/types/alias.go

Lines changed: 8 additions & 24 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/go/types/alias_test.go

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
// Copyright 2025 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package types_test
6+
7+
import (
8+
"go/ast"
9+
"go/parser"
10+
"go/token"
11+
"go/types"
12+
"testing"
13+
)
14+
15+
func TestIssue74181(t *testing.T) {
16+
t.Setenv("GODEBUG", "gotypesalias=1")
17+
18+
src := `package p
19+
20+
type AB = A[B]
21+
22+
type _ struct {
23+
_ AB
24+
}
25+
26+
type B struct {
27+
f *AB
28+
}
29+
30+
type A[T any] struct{}`
31+
32+
fset := token.NewFileSet()
33+
file, err := parser.ParseFile(fset, "p.go", src, parser.ParseComments)
34+
if err != nil {
35+
t.Fatalf("could not parse: %v", err)
36+
}
37+
38+
conf := types.Config{}
39+
pkg, err := conf.Check(file.Name.Name, fset, []*ast.File{file}, &types.Info{})
40+
if err != nil {
41+
t.Fatalf("could not type check: %v", err)
42+
}
43+
44+
b := pkg.Scope().Lookup("B").Type()
45+
if n, ok := b.(*types.Named); ok {
46+
if s, ok := n.Underlying().(*types.Struct); ok {
47+
got := s.Field(0).Type()
48+
want := types.NewPointer(pkg.Scope().Lookup("AB").Type())
49+
if !types.Identical(got, want) {
50+
t.Errorf("wrong type for f: got %v, want %v", got, want)
51+
}
52+
return
53+
}
54+
}
55+
t.Errorf("unexpected type for B: %v", b)
56+
}
57+
58+
func TestPartialTypeCheckUndeclaredAliasPanic(t *testing.T) {
59+
t.Setenv("GODEBUG", "gotypesalias=1")
60+
61+
src := `package p
62+
63+
type A = B // undeclared`
64+
65+
fset := token.NewFileSet()
66+
file, err := parser.ParseFile(fset, "p.go", src, parser.ParseComments)
67+
if err != nil {
68+
t.Fatalf("could not parse: %v", err)
69+
}
70+
71+
conf := types.Config{} // no error handler, panic
72+
pkg, _ := conf.Check(file.Name.Name, fset, []*ast.File{file}, &types.Info{})
73+
a := pkg.Scope().Lookup("A").Type()
74+
75+
if alias, ok := a.(*types.Alias); ok {
76+
got := alias.Rhs()
77+
want := types.Typ[types.Invalid]
78+
79+
if !types.Identical(got, want) {
80+
t.Errorf("wrong type for B: got %v, want %v", got, want)
81+
}
82+
return
83+
}
84+
t.Errorf("unexpected type for A: %v", a)
85+
}

src/go/types/decl.go

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -321,11 +321,15 @@ func (check *Checker) cycleError(cycle []Object, start int) {
321321
// If obj is a type alias, mark it as valid (not broken) in order to avoid follow-on errors.
322322
obj := cycle[start]
323323
tname, _ := obj.(*TypeName)
324-
if tname != nil && tname.IsAlias() {
325-
// If we use Alias nodes, it is initialized with Typ[Invalid].
326-
// TODO(gri) Adjust this code if we initialize with nil.
327-
if !check.conf._EnableAlias {
328-
check.validAlias(tname, Typ[Invalid])
324+
if tname != nil {
325+
if check.conf._EnableAlias {
326+
if a, ok := tname.Type().(*Alias); ok {
327+
a.fromRHS = Typ[Invalid]
328+
}
329+
} else {
330+
if tname.IsAlias() {
331+
check.validAlias(tname, Typ[Invalid])
332+
}
329333
}
330334
}
331335

@@ -582,17 +586,18 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *ast.TypeSpec, def *TypeName
582586
}
583587

584588
if check.conf._EnableAlias {
585-
// TODO(gri) Should be able to use nil instead of Typ[Invalid] to mark
586-
// the alias as incomplete. Currently this causes problems
587-
// with certain cycles. Investigate.
588-
//
589-
// NOTE(adonovan): to avoid the Invalid being prematurely observed
590-
// by (e.g.) a var whose type is an unfinished cycle,
591-
// Unalias does not memoize if Invalid. Perhaps we should use a
592-
// special sentinel distinct from Invalid.
593-
alias := check.newAlias(obj, Typ[Invalid])
589+
alias := check.newAlias(obj, nil)
594590
setDefType(def, alias)
595591

592+
// If we could not type the RHS, set it to invalid. This should
593+
// only ever happen if we panic before setting.
594+
defer func() {
595+
if alias.fromRHS == nil {
596+
alias.fromRHS = Typ[Invalid]
597+
unalias(alias)
598+
}
599+
}()
600+
596601
// handle type parameters even if not allowed (Alias type is supported)
597602
if tparam0 != nil {
598603
if !versionErr && !buildcfg.Experiment.AliasTypeParams {
@@ -606,8 +611,9 @@ func (check *Checker) typeDecl(obj *TypeName, tdecl *ast.TypeSpec, def *TypeName
606611

607612
rhs = check.definedType(tdecl.Type, obj)
608613
assert(rhs != nil)
614+
609615
alias.fromRHS = rhs
610-
Unalias(alias) // resolve alias.actual
616+
unalias(alias) // resolve alias.actual
611617
} else {
612618
// With Go1.23, the default behavior is to use Alias nodes,
613619
// reflected by check.enableAlias. Signal non-default behavior.

0 commit comments

Comments
 (0)