From c7923d1c64f101884ff05dc63b76d5ec827aadd3 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Sun, 12 Jul 2026 14:12:54 -0700 Subject: [PATCH] compiler: canonicalize generic instance identities x/tools SSA names and go/types strings can preserve the source spelling of type arguments, so aliases can make distinct instances collide. Use the canonical type encoding for function names, synthetic local type owners, instantiated named types, and method sets. --- compiler/interface.go | 37 +++++++++++++++++++++++------------ compiler/symbol.go | 24 +++++------------------ compiler/testdata/generics.ll | 24 ++++++++++++++--------- compiler/testdata/pragma.ll | 4 ++-- testdata/localtypes.txt | 8 ++++---- 5 files changed, 51 insertions(+), 46 deletions(-) diff --git a/compiler/interface.go b/compiler/interface.go index 3908a69d1..5c7460a31 100644 --- a/compiler/interface.go +++ b/compiler/interface.go @@ -599,8 +599,21 @@ func (c *compilerContext) getTypeCodeName(t types.Type) (name string, isLocal bo case *types.Named: tn := t.Obj() if tn.Pkg() == nil || tn.Parent() == tn.Pkg().Scope() { - // Package-scope or builtin: the printed name is unique. - return "named:" + t.String(), false + name := tn.Name() + if tn.Pkg() != nil { + name = tn.Pkg().Path() + "." + name + } + isLocal := false + if targs := t.TypeArgs(); targs.Len() != 0 { + parts := make([]string, targs.Len()) + for i := range parts { + var local bool + parts[i], local = c.getTypeCodeName(targs.At(i)) + isLocal = isLocal || local + } + name += "[" + strings.Join(parts, ",") + "]" + } + return "named:" + name, isLocal } if tn.Parent() != nil { // Ordinary function-local type. Use the un-//line-adjusted @@ -707,8 +720,8 @@ func (c *compilerContext) getTypeCodeName(t types.Type) (name string, isLocal bo // Synthetic TypeNames are produced by generic instantiation: two // instantiations of the same generic function (e.g. F[int] and // F[string]) produce TypeNames with the same printed name and the -// same source position, so each is named with the enclosing -// instance's RelString as prefix. RelString encodes the type +// same source position, so each is named with the enclosing instance's +// canonical function name as prefix. The function name encodes the type // arguments, matching Go's runtime behavior, where F[int].Inner and // F[string].Inner are distinct types even when Inner does not mention // the type parameter. @@ -717,9 +730,9 @@ func (c *compilerContext) getTypeCodeName(t types.Type) (name string, isLocal bo // of F[int] is compiled in every package that calls F[int]); its // reflect/types.type:* global has LinkOnceODRLinkage and is merged by // name at link time. The chosen name therefore depends only on -// intrinsic SSA properties (RelString and the raw token.Pos used as a -// sort key), so any package compiling the same instance produces the -// same identifier. +// intrinsic SSA properties (the canonical function name and raw token.Pos), +// so any package compiling the same instance produces the same +// identifier. // // Ordinary function-local TypeNames (TypeName.Parent() != nil) are // not handled here: they are nameable only inside their declaring @@ -807,9 +820,8 @@ func (c *compilerContext) scanLocalTypes(ssaPkg *ssa.Package) { // registerSyntheticLocalTypes walks every type reachable from fn's // body and records each synthetic *types.Named (TypeName.Parent() == -// nil) in c.localTypeNames. Each is named with fn.RelString as the -// owning function plus a per-function counter assigned in source -// order. +// nil) in c.localTypeNames. Each is named with the canonical function +// name plus a per-function counter assigned in source order. // // First-writer-wins: a *types.Named already present in // c.localTypeNames is left alone, so a synthetic type reachable from @@ -920,7 +932,7 @@ func (c *compilerContext) registerSyntheticLocalTypes(fn *ssa.Function) { sort.Slice(found, func(i, j int) bool { return found[i].Obj().Pos() < found[j].Obj().Pos() }) - enclosing := fn.RelString(nil) + enclosing := c.canonicalFunctionName(fn) for i, named := range found { c.localTypeNames.Set(named, fmt.Sprintf("%s.%s$%d", enclosing, named.Obj().Name(), i)) } @@ -929,7 +941,8 @@ func (c *compilerContext) registerSyntheticLocalTypes(fn *ssa.Function) { // getTypeMethodSet returns a reference (GEP) to a global method set. This // method set should be unreferenced after the interface lowering pass. func (c *compilerContext) getTypeMethodSet(typ types.Type) llvm.Value { - globalName := typ.String() + "$methodset" + typeName, _ := c.getTypeCodeName(typ) + globalName := typeName + "$methodset" global := c.mod.NamedGlobal(globalName) if global.IsNil() { ms := c.program.MethodSets.MethodSet(typ) diff --git a/compiler/symbol.go b/compiler/symbol.go index 2114aa611..16f5b85d8 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -320,13 +320,7 @@ func (c *compilerContext) getFunctionInfo(f *ssa.Function) functionInfo { } info := functionInfo{ // Pick the default linkName. - linkName: f.RelString(nil), - } - - // RelString is not unique for local type arguments, so add a suffix - // when needed. - if suffix := c.localTypeArgsSuffix(f); suffix != "" { - info.linkName += suffix + linkName: c.canonicalFunctionName(f), } // Check for a few runtime functions that are treated specially. @@ -353,24 +347,16 @@ func (c *compilerContext) getFunctionInfo(f *ssa.Function) functionInfo { return info } -func (c *compilerContext) localTypeArgsSuffix(f *ssa.Function) string { +func (c *compilerContext) canonicalFunctionName(f *ssa.Function) string { typeArgs := f.TypeArgs() if len(typeArgs) == 0 { - return "" + return f.RelString(nil) } - var hasLocal bool parts := make([]string, len(typeArgs)) for i, ta := range typeArgs { - name, isLocal := c.getTypeCodeName(ta) - if isLocal { - hasLocal = true - } - parts[i] = name + parts[i], _ = c.getTypeCodeName(ta) } - if !hasLocal { - return "" - } - return "$localtype:" + strings.Join(parts, ",") + return f.Origin().RelString(nil) + "[" + strings.Join(parts, ",") + "]" } // parsePragmas is used by getFunctionInfo to parse function pragmas such as diff --git a/compiler/testdata/generics.ll b/compiler/testdata/generics.ll index abe80e2ed..4896f9348 100644 --- a/compiler/testdata/generics.ll +++ b/compiler/testdata/generics.ll @@ -17,12 +17,12 @@ entry: ; Function Attrs: nounwind define hidden i32 @main.aliasSize32(ptr %context) unnamed_addr #1 { entry: - %0 = call i32 @"main.aliasSize[main.F]"(ptr undef) + %0 = call i32 @"main.aliasSize[basic:float32]"(ptr undef) ret i32 %0 } ; Function Attrs: nounwind -define linkonce_odr hidden i32 @"main.aliasSize[main.F]"(ptr %context) unnamed_addr #1 { +define linkonce_odr hidden i32 @"main.aliasSize[basic:float32]"(ptr %context) unnamed_addr #1 { entry: ret i32 4 } @@ -30,15 +30,21 @@ entry: ; Function Attrs: nounwind define hidden i32 @main.aliasSize64(ptr %context) unnamed_addr #1 { entry: - %0 = call i32 @"main.aliasSize[main.F]"(ptr undef) + %0 = call i32 @"main.aliasSize[basic:float64]"(ptr undef) ret i32 %0 } +; Function Attrs: nounwind +define linkonce_odr hidden i32 @"main.aliasSize[basic:float64]"(ptr %context) unnamed_addr #1 { +entry: + ret i32 8 +} + ; Function Attrs: nounwind define hidden void @main.main(ptr %context) unnamed_addr #1 { entry: - %0 = call %"main.Point[float32]" @"main.Add[float32]"(float 0.000000e+00, float 0.000000e+00, float 0.000000e+00, float 0.000000e+00, ptr undef) - %1 = call %"main.Point[int]" @"main.Add[int]"(i32 0, i32 0, i32 0, i32 0, ptr undef) + %0 = call %"main.Point[float32]" @"main.Add[basic:float32]"(float 0.000000e+00, float 0.000000e+00, float 0.000000e+00, float 0.000000e+00, ptr undef) + %1 = call %"main.Point[int]" @"main.Add[basic:int]"(i32 0, i32 0, i32 0, i32 0, ptr undef) %2 = call i32 @main.aliasSize32(ptr undef) call void @main.checkSize(i32 %2, ptr undef) #4 %3 = call i32 @main.aliasSize64(ptr undef) @@ -47,7 +53,7 @@ entry: } ; Function Attrs: nounwind -define linkonce_odr hidden %"main.Point[float32]" @"main.Add[float32]"(float %a.X, float %a.Y, float %b.X, float %b.Y, ptr %context) unnamed_addr #1 { +define linkonce_odr hidden %"main.Point[float32]" @"main.Add[basic:float32]"(float %a.X, float %a.Y, float %b.X, float %b.Y, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 %a = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #4 @@ -110,7 +116,7 @@ declare void @main.checkSize(i32, ptr) #0 declare void @runtime.nilPanic(ptr) #0 ; Function Attrs: nounwind -define linkonce_odr hidden %"main.Point[int]" @"main.Add[int]"(i32 %a.X, i32 %a.Y, i32 %b.X, i32 %b.Y, ptr %context) unnamed_addr #1 { +define linkonce_odr hidden %"main.Point[int]" @"main.Add[basic:int]"(i32 %a.X, i32 %a.Y, i32 %b.X, i32 %b.Y, ptr %context) unnamed_addr #1 { entry: %stackalloc = alloca i8, align 1 %a = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #4 @@ -170,7 +176,7 @@ declare void @main.checkBool(i1, ptr) #0 ; Function Attrs: nounwind define hidden void @main.aliasMethod32(ptr %x.typecode, ptr %x.value, ptr %context) unnamed_addr #1 { entry: - %0 = call i1 @"interface:{Get:func:{}{named:main.aliasMethodResult[main.F]}}.$typeassert"(ptr %x.typecode) #4 + %0 = call i1 @"interface:{Get:func:{}{named:main.aliasMethodResult[basic:float32]}}.$typeassert"(ptr %x.typecode) #4 br i1 %0, label %typeassert.ok, label %typeassert.next typeassert.next: ; preds = %typeassert.ok, %entry @@ -181,7 +187,7 @@ typeassert.ok: ; preds = %entry br label %typeassert.next } -declare i1 @"interface:{Get:func:{}{named:main.aliasMethodResult[main.F]}}.$typeassert"(ptr) #3 +declare i1 @"interface:{Get:func:{}{named:main.aliasMethodResult[basic:float32]}}.$typeassert"(ptr) #3 attributes #0 = { "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } attributes #1 = { nounwind "target-features"="+bulk-memory,+bulk-memory-opt,+call-indirect-overlong,+mutable-globals,+nontrapping-fptoint,+sign-ext,-multivalue,-reference-types" } diff --git a/compiler/testdata/pragma.ll b/compiler/testdata/pragma.ll index 5348018b8..d8b446b89 100644 --- a/compiler/testdata/pragma.ll +++ b/compiler/testdata/pragma.ll @@ -50,12 +50,12 @@ entry: ; Function Attrs: nounwind define hidden void @main.useGeneric(ptr %context) unnamed_addr #1 { entry: - call void @"main.noinlineGenericFunc[int8]"(ptr undef) + call void @"main.noinlineGenericFunc[basic:int8]"(ptr undef) ret void } ; Function Attrs: noinline nounwind -define linkonce_odr hidden void @"main.noinlineGenericFunc[int8]"(ptr %context) unnamed_addr #4 { +define linkonce_odr hidden void @"main.noinlineGenericFunc[basic:int8]"(ptr %context) unnamed_addr #4 { entry: ret void } diff --git a/testdata/localtypes.txt b/testdata/localtypes.txt index 85a851741..b0910553d 100644 --- a/testdata/localtypes.txt +++ b/testdata/localtypes.txt @@ -48,13 +48,13 @@ issue4931PairB labels: 0 issue4931MethodA labels: 0 issue4931MethodB labels: 0 ok: aliasSize[float32]==32 -BUG: aliasSize[float64]==64 +ok: aliasSize[float64]==64 ok: aliasLocal[float32] accepts own ok: aliasLocal[float64] accepts own -BUG: aliasLocal[float32] rejects [float64] -BUG: aliasLocal[float64] rejects [float32] +ok: aliasLocal[float32] rejects [float64] +ok: aliasLocal[float64] rejects [float32] ok: aliasBox[float32] implements Get() float32 -BUG: aliasBox[float64] implements Get() float64 +ok: aliasBox[float64] implements Get() float64 ok: aliasMethod[float32] accepts own ok: aliasMethod[float64] accepts own BUG: aliasMethod[float32] rejects [float64]