go/types, types2: better error messages for for-range clauses

Provide the exact error cause instead of reporting a missing
core type.

For #70128.

Change-Id: I835698fa1f22382711bd54b974d2c87ee17e9065
Reviewed-on: https://go-review.googlesource.com/c/go/+/651215
Reviewed-by: Robert Griesemer <gri@google.com>
Reviewed-by: Robert Findley <rfindley@google.com>
Auto-Submit: Robert Griesemer <gri@google.com>
TryBot-Bypass: Robert Griesemer <gri@google.com>
This commit is contained in:
Robert Griesemer 2025-02-20 15:01:36 -08:00 committed by Gopher Robot
parent bdcd6d1b65
commit d45d502fbb
8 changed files with 145 additions and 31 deletions

View File

@ -32,7 +32,7 @@ func CoreType(t Type) Type {
// RangeKeyVal returns the key and value types for a range over typ.
// It panics if range over typ is invalid.
func RangeKeyVal(typ Type) (Type, Type) {
key, val, _, ok := rangeKeyVal(typ, nil)
key, val, _, ok := rangeKeyVal(nil, typ, nil)
assert(ok)
return key, val
}

View File

@ -859,8 +859,7 @@ func (check *Checker) rangeStmt(inner stmtContext, s *syntax.ForStmt, rclause *s
// determine key/value types
var key, val Type
if x.mode != invalid {
// Ranging over a type parameter is permitted if it has a core type.
k, v, cause, ok := rangeKeyVal(x.typ, func(v goVersion) bool {
k, v, cause, ok := rangeKeyVal(check, x.typ, func(v goVersion) bool {
return check.allowVersion(v)
})
switch {
@ -992,19 +991,23 @@ func (check *Checker) rangeStmt(inner stmtContext, s *syntax.ForStmt, rclause *s
}
// rangeKeyVal returns the key and value type produced by a range clause
// over an expression of type typ.
// over an expression of type orig.
// If allowVersion != nil, it is used to check the required language version.
// If the range clause is not permitted, rangeKeyVal returns ok = false.
// When ok = false, rangeKeyVal may also return a reason in cause.
func rangeKeyVal(typ Type, allowVersion func(goVersion) bool) (key, val Type, cause string, ok bool) {
// The check parameter is only used in case of an error; it may be nil.
func rangeKeyVal(check *Checker, orig Type, allowVersion func(goVersion) bool) (key, val Type, cause string, ok bool) {
bad := func(cause string) (Type, Type, string, bool) {
return Typ[Invalid], Typ[Invalid], cause, false
}
orig := typ
switch typ := arrayPtrDeref(coreType(typ)).(type) {
case nil:
return bad("no core type")
var cause1 string
rtyp := sharedUnderOrChan(check, orig, &cause1)
if rtyp == nil {
return bad(cause1)
}
switch typ := arrayPtrDeref(rtyp).(type) {
case *Basic:
if isString(typ) {
return Typ[Int], universeRune, "", true // use 'rune' name
@ -1022,9 +1025,7 @@ func rangeKeyVal(typ Type, allowVersion func(goVersion) bool) (key, val Type, ca
case *Map:
return typ.key, typ.elem, "", true
case *Chan:
if typ.dir == SendOnly {
return bad("receive from send-only channel")
}
assert(typ.dir != SendOnly)
return typ.elem, nil, "", true
case *Signature:
if !buildcfg.Experiment.RangeFunc && allowVersion != nil && !allowVersion(go1_23) {

View File

@ -40,6 +40,57 @@ func typeset(t Type, yield func(t, u Type) bool) {
yield(t, under(t))
}
// If t is not a type parameter, sharedUnderOrChan returns the underlying type;
// if that type is a channel type it must permit receive operations.
// If t is a type parameter, sharedUnderOrChan returns the single underlying
// type of all types in its type set if it exists, or, if the type set contains
// only channel types permitting receive operations and with identical element
// types, sharedUnderOrChan returns one of those channel types.
// Otherwise the result is nil, and *cause reports the error if a non-nil cause
// is provided.
// The check parameter is only used if *cause reports an error; it may be nil.
func sharedUnderOrChan(check *Checker, t Type, cause *string) Type {
var s, su Type
var sc *Chan
bad := func(s string) bool {
if cause != nil {
*cause = s
}
su = nil
return false
}
typeset(t, func(t, u Type) bool {
if u == nil {
return bad("no specific type")
}
c, _ := u.(*Chan)
if c != nil && c.dir == SendOnly {
return bad(check.sprintf("receive from send-only channel %s", t))
}
if su == nil {
s, su = t, u
sc = c // possibly nil
return true
}
// su != nil
if sc != nil && c != nil {
if !Identical(sc.elem, c.elem) {
return bad(check.sprintf("channels with different element types %s and %s", sc.elem, c.elem))
}
return true
}
// sc == nil
if !Identical(su, u) {
return bad(check.sprintf("%s and %s have different underlying types", s, t))
}
return true
})
return su
}
// If t is not a type parameter, coreType returns the underlying type.
// If t is a type parameter, coreType returns the single underlying
// type of all types in its type set if it exists, or nil otherwise. If the

View File

@ -877,8 +877,7 @@ func (check *Checker) rangeStmt(inner stmtContext, s *ast.RangeStmt) {
// determine key/value types
var key, val Type
if x.mode != invalid {
// Ranging over a type parameter is permitted if it has a core type.
k, v, cause, ok := rangeKeyVal(x.typ, func(v goVersion) bool {
k, v, cause, ok := rangeKeyVal(check, x.typ, func(v goVersion) bool {
return check.allowVersion(v)
})
switch {
@ -1010,19 +1009,23 @@ func (check *Checker) rangeStmt(inner stmtContext, s *ast.RangeStmt) {
}
// rangeKeyVal returns the key and value type produced by a range clause
// over an expression of type typ.
// over an expression of type orig.
// If allowVersion != nil, it is used to check the required language version.
// If the range clause is not permitted, rangeKeyVal returns ok = false.
// When ok = false, rangeKeyVal may also return a reason in cause.
func rangeKeyVal(typ Type, allowVersion func(goVersion) bool) (key, val Type, cause string, ok bool) {
// The check parameter is only used in case of an error; it may be nil.
func rangeKeyVal(check *Checker, orig Type, allowVersion func(goVersion) bool) (key, val Type, cause string, ok bool) {
bad := func(cause string) (Type, Type, string, bool) {
return Typ[Invalid], Typ[Invalid], cause, false
}
orig := typ
switch typ := arrayPtrDeref(coreType(typ)).(type) {
case nil:
return bad("no core type")
var cause1 string
rtyp := sharedUnderOrChan(check, orig, &cause1)
if rtyp == nil {
return bad(cause1)
}
switch typ := arrayPtrDeref(rtyp).(type) {
case *Basic:
if isString(typ) {
return Typ[Int], universeRune, "", true // use 'rune' name
@ -1040,9 +1043,7 @@ func rangeKeyVal(typ Type, allowVersion func(goVersion) bool) (key, val Type, ca
case *Map:
return typ.key, typ.elem, "", true
case *Chan:
if typ.dir == SendOnly {
return bad("receive from send-only channel")
}
assert(typ.dir != SendOnly)
return typ.elem, nil, "", true
case *Signature:
if !buildcfg.Experiment.RangeFunc && allowVersion != nil && !allowVersion(go1_23) {

View File

@ -43,6 +43,57 @@ func typeset(t Type, yield func(t, u Type) bool) {
yield(t, under(t))
}
// If t is not a type parameter, sharedUnderOrChan returns the underlying type;
// if that type is a channel type it must permit receive operations.
// If t is a type parameter, sharedUnderOrChan returns the single underlying
// type of all types in its type set if it exists, or, if the type set contains
// only channel types permitting receive operations and with identical element
// types, sharedUnderOrChan returns one of those channel types.
// Otherwise the result is nil, and *cause reports the error if a non-nil cause
// is provided.
// The check parameter is only used if *cause reports an error; it may be nil.
func sharedUnderOrChan(check *Checker, t Type, cause *string) Type {
var s, su Type
var sc *Chan
bad := func(s string) bool {
if cause != nil {
*cause = s
}
su = nil
return false
}
typeset(t, func(t, u Type) bool {
if u == nil {
return bad("no specific type")
}
c, _ := u.(*Chan)
if c != nil && c.dir == SendOnly {
return bad(check.sprintf("receive from send-only channel %s", t))
}
if su == nil {
s, su = t, u
sc = c // possibly nil
return true
}
// su != nil
if sc != nil && c != nil {
if !Identical(sc.elem, c.elem) {
return bad(check.sprintf("channels with different element types %s and %s", sc.elem, c.elem))
}
return true
}
// sc == nil
if !Identical(su, u) {
return bad(check.sprintf("%s and %s have different underlying types", s, t))
}
return true
})
return su
}
// If t is not a type parameter, coreType returns the underlying type.
// If t is a type parameter, coreType returns the single underlying
// type of all types in its type set if it exists, or nil otherwise. If the

View File

@ -230,7 +230,7 @@ func _[
for _, _ = range s1 {}
var s2 S2
for range s2 /* ERRORx `cannot range over s2.*no core type` */ {}
for range s2 /* ERRORx `cannot range over s2.*\[\]int and \[10\]int have different underlying types` */ {}
var a0 []int
for range a0 {}
@ -243,7 +243,7 @@ func _[
for _, _ = range a1 {}
var a2 A2
for range a2 /* ERRORx `cannot range over a2.*no core type` */ {}
for range a2 /* ERRORx `cannot range over a2.*\[10\]int and \[\]int have different underlying types` */ {}
var p0 *[10]int
for range p0 {}
@ -256,7 +256,7 @@ func _[
for _, _ = range p1 {}
var p2 P2
for range p2 /* ERRORx `cannot range over p2.*no core type` */ {}
for range p2 /* ERRORx `cannot range over p2.*\*\[10\]int and \*\[\]int have different underlying types` */ {}
var m0 map[string]int
for range m0 {}
@ -269,7 +269,7 @@ func _[
for _, _ = range m1 {}
var m2 M2
for range m2 /* ERRORx `cannot range over m2.*no core type` */ {}
for range m2 /* ERRORx `cannot range over m2.*map\[string\]int and map\[string\]string` */ {}
}
// type inference checks

View File

@ -129,13 +129,23 @@ func test() {
}
}
func _[T any](x T) {
for range x /* ERROR "cannot range over x (variable of type T constrained by any): no specific type" */ {
}
}
func _[T interface{int; string}](x T) {
for range x /* ERROR "cannot range over x (variable of type T constrained by interface{int; string} with empty type set): no specific type" */ {
}
}
func _[T int | string](x T) {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | string): no core type" */ {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | string): int and string have different underlying types" */ {
}
}
func _[T int | int64](x T) {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | int64): no core type" */ {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | int64): int and int64 have different underlying types" */ {
}
}

View File

@ -56,12 +56,12 @@ func _() {
}
func _[T int | string](x T) {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | string): no core type" */ {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | string): int and string have different underlying types" */ {
}
}
func _[T int | int64](x T) {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | int64): no core type" */ {
for range x /* ERROR "cannot range over x (variable of type T constrained by int | int64): int and int64 have different underlying types" */ {
}
}