Source file src/cmd/compile/internal/test/shapename_test.go

     1  // Copyright 2026 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 test
     6  
     7  import (
     8  	"internal/testenv"
     9  	"os"
    10  	"path/filepath"
    11  	"regexp"
    12  	"strings"
    13  	"testing"
    14  )
    15  
    16  // TestShapeNameLength tests that an unwieldy type argument doesn't leak into
    17  // the names of every function instantiated with it. Shape names are embedded
    18  // in those names, so without a bound a single large struct or an interface
    19  // with many methods makes nm output, stack traces and pprof graphs unreadable.
    20  // See go.dev/issue/65030 and go.dev/issue/71535.
    21  func TestShapeNameLength(t *testing.T) {
    22  	testenv.MustHaveGoBuild(t)
    23  	t.Parallel()
    24  
    25  	// Big and BigStruct are over the shape name limit and named, so their
    26  	// shapes are named after them. The anonymous struct is over the limit
    27  	// with no name to borrow. Small fits under the limit.
    28  	const src = `package p
    29  
    30  type Big interface {
    31  	Eval(Context) Big
    32  	Inner() Big
    33  	ProgString() string
    34  	Rank() int
    35  	Sprint(*Config) string
    36  	String() string
    37  	shrink() Big
    38  	toType(string, *Config, int) Big
    39  }
    40  
    41  type Context struct{ _ int }
    42  type Config struct{ _ int }
    43  
    44  type Small interface{ Len() int }
    45  
    46  type Holder[T any] struct{ v []T }
    47  
    48  func (h *Holder[T]) Append(x T) { h.v = append(h.v, x) }
    49  
    50  type BigStruct struct {
    51  	Field0, Field1, Field2, Field3, Field4 int
    52  	Field5, Field6, Field7, Field8, Field9 int
    53  }
    54  
    55  // Wrapper[T] instantiated with a hashed shape gives a shape whose own
    56  // tag contains a '#', which shapifying it again has to cope with.
    57  type Wrapper[T any] struct {
    58  	A, B, C, D, E T
    59  	F, G, H, I, J T
    60  	K, L, M, N, O T
    61  }
    62  
    63  func Nested[T any](x Wrapper[T]) {
    64  	var h Holder[Wrapper[T]]
    65  	h.Append(x)
    66  }
    67  
    68  var _ = Nested[Big]
    69  
    70  var _ Holder[Big]
    71  var _ Holder[Small]
    72  var _ Holder[BigStruct]
    73  var _ Holder[struct {
    74  	Alpha, Bravo, Charlie, Delta, Echo   int
    75  	Foxtrot, Golf, Hotel, India, Juliett int
    76  }]
    77  `
    78  
    79  	dir := t.TempDir()
    80  	srcGo := filepath.Join(dir, "p.go")
    81  	if err := os.WriteFile(srcGo, []byte(src), 0644); err != nil {
    82  		t.Fatal(err)
    83  	}
    84  	obj := filepath.Join(dir, "p.o")
    85  
    86  	gotool := testenv.GoToolPath(t)
    87  	cmd := testenv.Command(t, gotool, "tool", "compile", "-p", "p", "-o", obj, srcGo)
    88  	if out, err := cmd.CombinedOutput(); err != nil {
    89  		t.Fatalf("%v: %v\n%s", cmd, err, out)
    90  	}
    91  
    92  	cmd = testenv.Command(t, gotool, "tool", "nm", obj)
    93  	out, err := cmd.CombinedOutput()
    94  	if err != nil {
    95  		t.Fatalf("%v: %v\n%s", cmd, err, out)
    96  	}
    97  	syms := string(out)
    98  
    99  	// Big is over the limit, so its shape is named after it rather than
   100  	// spelled out. Big can't even be written as a literal, as it refers to
   101  	// itself, so its name is the only useful thing to call the shape.
   102  	if !strings.Contains(syms, "go.shape.p.Big") {
   103  		t.Errorf("no go.shape.p.Big symbol in nm output\n%s", syms)
   104  	}
   105  	if strings.Contains(syms, "go.shape.interface { Eval(") {
   106  		t.Errorf("Big's full method set still appears in symbol names")
   107  	}
   108  
   109  	if !strings.Contains(syms, "go.shape.p.BigStruct") {
   110  		t.Errorf("no go.shape.p.BigStruct symbol in nm output\n%s", syms)
   111  	}
   112  
   113  	// The shape name has to be qualified by package path: it is the shape's
   114  	// identity, and an unqualified name would make two same-named types in
   115  	// different packages share one shape.
   116  	if regexp.MustCompile(`go\.shape\.Big\b`).MatchString(syms) {
   117  		t.Errorf("Big's shape is named without its package\n%s", syms)
   118  	}
   119  
   120  	// An over-limit type argument with no name to borrow falls back to a
   121  	// hash of the underlying type.
   122  	if !regexp.MustCompile(`go\.shape\.[0-9a-f]{64}\b`).MatchString(syms) {
   123  		t.Errorf("no go.shape.<hash> symbol in nm output\n%s", syms)
   124  	}
   125  
   126  	// The Small shape is under the limit, so it's still spelled out. Hashing
   127  	// short shapes would lose readability for no benefit.
   128  	if !strings.Contains(syms, "go.shape.interface { Len() int }") {
   129  		t.Errorf("Small's shape was hashed; want it spelled out\n%s", syms)
   130  	}
   131  
   132  	// Shapifying a shape reproduces that shape's own name, so a generic
   133  	// type instantiated with a named shape names its shape after the
   134  	// instantiation, nesting and all.
   135  	if !strings.Contains(syms, "go.shape.p.Wrapper[go.shape.p.Big]") {
   136  		t.Errorf("no nested go.shape.p.Wrapper[go.shape.p.Big] symbol in nm output\n%s", syms)
   137  	}
   138  
   139  	// Shapify has to be idempotent, or the shaped object for an
   140  	// instantiation is looked up under a different name than it was
   141  	// defined with. That shows up as a duplicate symbol, so simply
   142  	// compiling the package above covers it; assert the symbols are
   143  	// distinct too, in case the duplicate check ever loosens.
   144  	seen := map[string]bool{}
   145  	for _, line := range strings.Split(syms, "\n") {
   146  		f := strings.Fields(line)
   147  		if len(f) < 3 || f[1] != "T" {
   148  			continue
   149  		}
   150  		if name := f[2]; seen[name] {
   151  			t.Errorf("duplicate text symbol %s", name)
   152  		} else {
   153  			seen[name] = true
   154  		}
   155  	}
   156  }
   157  

View as plain text