re-bundle the latest version of typeutil.Hasher

And document how we do this and why.
This commit is contained in:
Daniel Martí
2026-02-27 22:35:04 +00:00
committed by Paul
parent 799a451ce7
commit 6c956e00e5
3 changed files with 75 additions and 34 deletions
+26
View File
@@ -105,6 +105,32 @@ the hash of the operation's inputs, and the last component is the *content ID*,
the hash of the operation's output. For more, read the hash of the operation's output. For more, read
[the docs in buildid.go](https://github.com/golang/go/blob/master/src/cmd/go/internal/work/buildid.go) [the docs in buildid.go](https://github.com/golang/go/blob/master/src/cmd/go/internal/work/buildid.go)
### Bundled x/tools packages
`bundled_typeutil.go` and `bundled_typeparams.go` are modified copies of x/tools packages.
We need to modify [typeutil.Hasher](https://pkg.go.dev/golang.org/x/tools/go/types/typeutil#Hasher)
so that it doesn't include struct field tags in the hash algorithm,
given that struct field tags do not affect type identity.
The typeutil package imports the internal `typeparams` package, so we must bundle that too,
and replace the import with references to the bundled typeparams file.
Our patches are marked via `// NOTE(garble)` and are done by hand.
We also remove the `//go:generate` lines added by `bundle`
because re-running the tool will undo the manual edits.
Finally, we also remove any bundled API we don't need, that is,
anything which is not reachable from the bundled `typeutil_Hasher` type.
We also ensure that `staticcheck` does not report any unused code.
To update these files to a newer x/tools version to gain upstream fixes:
* `go get golang.org/x/tools@latest`
* `go tool bundle -o bundled_typeutil.go golang.org/x/tools/go/types/typeutil`
* `go tool bundle -o bundled_typeparams.go golang.org/x/tools/internal/typeparams`
* re-apply the changes mentioned above
* inspect the diff to ensure the changes look reasonable and we haven't lost any of our edits
### Benchmarking ### Benchmarking
A build benchmark is available, to be able to measure the cost of builing a A build benchmark is available, to be able to measure the cost of builing a
+34 -11
View File
@@ -1,16 +1,20 @@
// Originally bundled from golang.org/x/tools/internal/typeparams@v0.29.0, // NOTE(garble): bundled as of golang.org/x/tools v0.42.0; see CONTRIBUTING.md.
// as it is used by x/tools/go/types/typeutil and is an internal package.
//lint:file-ignore ST1012 NOTE(garble): err var names get a new prefix
package main package main
import ( import (
"bytes"
"errors" "errors"
"fmt" "fmt"
"go/types" "go/types"
"os"
"strings"
) )
var errEmptyTypeSet = errors.New("empty type set") const typeparams_debug = false
var typeparams_ErrEmptyTypeSet = errors.New("empty type set")
// InterfaceTermSet computes the normalized terms for a constraint interface, // InterfaceTermSet computes the normalized terms for a constraint interface,
// returning an error if the term set cannot be computed or is empty. In the // returning an error if the term set cannot be computed or is empty. In the
@@ -38,7 +42,7 @@ func typeparams_computeTermSet(typ types.Type) ([]*types.Term, error) {
return nil, err return nil, err
} }
if tset.terms.isEmpty() { if tset.terms.isEmpty() {
return nil, errEmptyTypeSet return nil, typeparams_ErrEmptyTypeSet
} }
if tset.terms.isAll() { if tset.terms.isAll() {
return nil, nil return nil, nil
@@ -60,11 +64,26 @@ type typeparams_termSet struct {
terms typeparams_termlist terms typeparams_termlist
} }
func typeparams_indentf(depth int, format string, args ...any) {
fmt.Fprintf(os.Stderr, strings.Repeat(".", depth)+format+"\n", args...)
}
func typeparams_computeTermSetInternal(t types.Type, seen map[types.Type]*typeparams_termSet, depth int) (res *typeparams_termSet, err error) { func typeparams_computeTermSetInternal(t types.Type, seen map[types.Type]*typeparams_termSet, depth int) (res *typeparams_termSet, err error) {
if t == nil { if t == nil {
panic("nil type") panic("nil type")
} }
if typeparams_debug {
typeparams_indentf(depth, "%s", t.String())
defer func() {
if err != nil {
typeparams_indentf(depth, "=> %s", err)
} else {
typeparams_indentf(depth, "=> %s", res.terms.String())
}
}()
}
const maxTermCount = 100 const maxTermCount = 100
if tset, ok := seen[t]; ok { if tset, ok := seen[t]; ok {
if !tset.complete { if !tset.complete {
@@ -85,8 +104,7 @@ func typeparams_computeTermSetInternal(t types.Type, seen map[types.Type]*typepa
// The term set of an interface is the intersection of the term sets of its // The term set of an interface is the intersection of the term sets of its
// embedded types. // embedded types.
tset.terms = typeparams_allTermlist tset.terms = typeparams_allTermlist
for i := range u.NumEmbeddeds() { for embedded := range u.EmbeddedTypes() {
embedded := u.EmbeddedType(i)
if _, ok := embedded.Underlying().(*types.TypeParam); ok { if _, ok := embedded.Underlying().(*types.TypeParam); ok {
return nil, fmt.Errorf("invalid embedded type %T", embedded) return nil, fmt.Errorf("invalid embedded type %T", embedded)
} }
@@ -99,8 +117,7 @@ func typeparams_computeTermSetInternal(t types.Type, seen map[types.Type]*typepa
case *types.Union: case *types.Union:
// The term set of a union is the union of term sets of its terms. // The term set of a union is the union of term sets of its terms.
tset.terms = nil tset.terms = nil
for i := range u.Len() { for t := range u.Terms() {
t := u.Term(i)
var terms typeparams_termlist var terms typeparams_termlist
switch t.Type().Underlying().(type) { switch t.Type().Underlying().(type) {
case *types.Interface: case *types.Interface:
@@ -156,15 +173,18 @@ type typeparams_termlist []*typeparams_term
// It is in normal form. // It is in normal form.
var typeparams_allTermlist = typeparams_termlist{new(typeparams_term)} var typeparams_allTermlist = typeparams_termlist{new(typeparams_term)}
// termSep is the separator used between individual terms.
const typeparams_termSep = " | "
// String prints the termlist exactly (without normalization). // String prints the termlist exactly (without normalization).
func (xl typeparams_termlist) String() string { func (xl typeparams_termlist) String() string {
if len(xl) == 0 { if len(xl) == 0 {
return "∅" return "∅"
} }
var buf bytes.Buffer var buf strings.Builder
for i, x := range xl { for i, x := range xl {
if i > 0 { if i > 0 {
buf.WriteString(" | ") buf.WriteString(typeparams_termSep)
} }
buf.WriteString(x.String()) buf.WriteString(x.String())
} }
@@ -342,6 +362,9 @@ func (x *typeparams_term) intersect(y *typeparams_term) *typeparams_term {
// disjoint reports whether x ∩ y == ∅. // disjoint reports whether x ∩ y == ∅.
// x.typ and y.typ must not be nil. // x.typ and y.typ must not be nil.
func (x *typeparams_term) disjoint(y *typeparams_term) bool { func (x *typeparams_term) disjoint(y *typeparams_term) bool {
if typeparams_debug && (x.typ == nil || y.typ == nil) {
panic("invalid argument(s)")
}
ux := x.typ ux := x.typ
if y.tilde { if y.tilde {
ux = typeparams_under(ux) ux = typeparams_under(ux)
+15 -23
View File
@@ -1,12 +1,11 @@
// Originally bundled from golang.org/x/tools/go/types/typeutil@v0.29.0. // NOTE(garble): bundled as of golang.org/x/tools v0.42.0; see CONTRIBUTING.md.
// Edited to just keep the hasher API in place, removing the use of internal/typeparams,
// and removed the inclusion of struct field tags in the hasher.
package main package main
import ( import (
"fmt" "fmt"
"go/types" "go/types"
_ "unsafe"
) )
// -- Hasher -- // -- Hasher --
@@ -37,7 +36,7 @@ type typeutil_hasher struct{ inGenericSig bool }
// hashString computes the FowlerNollVo hash of s. // hashString computes the FowlerNollVo hash of s.
func typeutil_hashString(s string) uint32 { func typeutil_hashString(s string) uint32 {
var h uint32 var h uint32
for i := range len(s) { for i := 0; i < len(s); i++ {
h ^= uint32(s[i]) h ^= uint32(s[i])
h *= 16777619 h *= 16777619
} }
@@ -67,7 +66,7 @@ func (h typeutil_hasher) hash(t types.Type) uint32 {
if f.Anonymous() { if f.Anonymous() {
hash += 8861 hash += 8861
} }
// NOTE: we must not hash struct field tags, as they do not affect type identity. // NOTE(garble): we must not hash struct field tags, as they do not affect type identity.
// hash += typeutil_hashString(t.Tag(i)) // hash += typeutil_hashString(t.Tag(i))
hash += typeutil_hashString(f.Name()) // (ignore f.Pkg) hash += typeutil_hashString(f.Name()) // (ignore f.Pkg)
hash += h.hash(f.Type()) hash += h.hash(f.Type())
@@ -84,10 +83,13 @@ func (h typeutil_hasher) hash(t types.Type) uint32 {
} }
tparams := t.TypeParams() tparams := t.TypeParams()
for i := range tparams.Len() { if n := tparams.Len(); n > 0 {
h.inGenericSig = true h.inGenericSig = true // affects constraints, params, and results
tparam := tparams.At(i)
hash += 7 * h.hash(tparam.Constraint()) for i := range n {
tparam := tparams.At(i)
hash += 7 * h.hash(tparam.Constraint())
}
} }
return hash + 3*h.hashTuple(t.Params()) + 5*h.hashTuple(t.Results()) return hash + 3*h.hashTuple(t.Params()) + 5*h.hashTuple(t.Results())
@@ -129,8 +131,7 @@ func (h typeutil_hasher) hash(t types.Type) uint32 {
case *types.Named: case *types.Named:
hash := h.hashTypeName(t.Obj()) hash := h.hashTypeName(t.Obj())
targs := t.TypeArgs() targs := t.TypeArgs()
for i := range targs.Len() { for targ := range targs.Types() {
targ := targs.At(i)
hash += 2 * h.hash(targ) hash += 2 * h.hash(targ)
} }
return hash return hash
@@ -200,21 +201,12 @@ func (h typeutil_hasher) hashTypeParam(t *types.TypeParam) uint32 {
// hashTypeName hashes the pointer of tname. // hashTypeName hashes the pointer of tname.
func (typeutil_hasher) hashTypeName(tname *types.TypeName) uint32 { func (typeutil_hasher) hashTypeName(tname *types.TypeName) uint32 {
// NOTE: we must not hash any pointers, as garble is a toolexec tool // NOTE(garble): we must not hash any pointers,
// so by nature it uses multiple processes. // as garble is a toolexec tool so by nature it uses multiple processes.
return typeutil_hashString(tname.Name()) return typeutil_hashString(tname.Name())
// Since types.Identical uses == to compare TypeNames, // Since types.Identical uses == to compare TypeNames,
// the Hash function uses maphash.Comparable. // the Hash function uses maphash.Comparable.
// TODO(adonovan): or will, when it becomes available in go1.24. // hash := maphash.Comparable(typeutil_theSeed, tname)
// In the meantime we use the pointer's numeric value.
//
// hash := maphash.Comparable(theSeed, tname)
//
// (Another approach would be to hash the name and package
// path, and whether or not it is a package-level typename. It
// is rare for a package to define multiple local types with
// the same name.)
// hash := uintptr(unsafe.Pointer(tname))
// return uint32(hash ^ (hash >> 32)) // return uint32(hash ^ (hash >> 32))
} }