diff --git a/compiler/symbol.go b/compiler/symbol.go index fe9adc649..fafc954cf 100644 --- a/compiler/symbol.go +++ b/compiler/symbol.go @@ -8,6 +8,7 @@ import ( "go/ast" "go/token" "go/types" + "path/filepath" "slices" "strconv" "strings" @@ -365,6 +366,22 @@ func (c *compilerContext) localTypeArgsSuffix(f *ssa.Function) string { if isLocal { hasLocal = true } + // A function-local type alias (e.g. `type F = float64` inside a + // function body) is invisible to getTypeCodeName because it calls + // types.Unalias first. Two callers that use distinct aliases with + // the same name (e.g. Go 1.27's internal/strconv.ftoa32 and ftoa64 + // both declare a local `type F = ...`) then produce identical + // RelStrings for their shortFloat[F] instantiations and collide on + // mod.NamedFunction. Treat these aliases as local so the suffix + // disambiguates them. + if alias, ok := ta.(*types.Alias); ok { + if obj := alias.Obj(); obj.Pkg() != nil && obj.Parent() != obj.Pkg().Scope() { + hasLocal = true + pos := c.program.Fset.PositionFor(obj.Pos(), false) + parts[i] = fmt.Sprintf("%s$alias:%s:%d:%d", name, filepath.Base(pos.Filename), pos.Line, pos.Column) + continue + } + } parts[i] = name } if !hasLocal { diff --git a/testdata/localtypes.go b/testdata/localtypes.go index f9081d00b..945ccebff 100644 --- a/testdata/localtypes.go +++ b/testdata/localtypes.go @@ -1,6 +1,9 @@ package main -import "reflect" +import ( + "reflect" + "unsafe" +) type checker = func(any) bool @@ -216,6 +219,28 @@ func issue5180CopyIgnoreNilMembers() (ok bool) { return ok } +// aliasSize is the pattern used by Go 1.27's internal/strconv.ftoa32 and +// ftoa64 (and by shortFloat in internal/strconv/uscale.go): a generic +// function parameterized on F={float32|float64} that switches on +// unsafe.Sizeof(F(0)). Callers pass F via a function-local `type F = ...` +// alias. If the two instantiations share an SSA function name (because +// x/tools' go/ssa targstr uses the alias's declared name), TinyGo's +// getFunction reuses the first LLVM function for the second call and the +// second instantiation returns the first's result. +func aliasSize[F float32 | float64]() int { + return 8 * int(unsafe.Sizeof(F(0))) +} + +func aliasSizeCaller32() int { + type F = float32 + return aliasSize[F]() +} + +func aliasSizeCaller64() int { + type F = float64 + return aliasSize[F]() +} + func main() { expect("issue5180 TestCopy1", issue5180Copy1()) expect("issue5180 TestCopyIgnoreNilMembers", issue5180CopyIgnoreNilMembers()) @@ -300,4 +325,9 @@ func main() { println("issue4931PairB labels:", issue4931PairB()) println("issue4931MethodA labels:", issue4931MethodA()) println("issue4931MethodB labels:", issue4931MethodB()) + + // Generic instances distinguished only by function-local type aliases + // must not share bodies either (the Go 1.27 strconv shortFloat pattern). + expect("aliasSize[float32]==32", aliasSizeCaller32() == 32) + expect("aliasSize[float64]==64", aliasSizeCaller64() == 64) } diff --git a/testdata/localtypes.txt b/testdata/localtypes.txt index dd5d72ddd..9f3d778ec 100644 --- a/testdata/localtypes.txt +++ b/testdata/localtypes.txt @@ -47,3 +47,5 @@ issue4931PairA labels: 0 issue4931PairB labels: 0 issue4931MethodA labels: 0 issue4931MethodB labels: 0 +ok: aliasSize[float32]==32 +ok: aliasSize[float64]==64