Skip to content

DCE update to support nested types #1375

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 16, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,179 @@ func TestDeclSelection_RemoveUnusedTypeConstraint(t *testing.T) {
sel.IsDead(`var:command-line-arguments.ghost`)
}

func TestDeclSelection_RemoveUnusedNestedTypesInFunction(t *testing.T) {
src := `
package main
func Foo[T any](u T) any {
type Bar struct { v T }
return Bar{v: u}
}
func deadCode() {
println(Foo[int](42))
}
func main() {
println(Foo[string]("cat"))
}`

srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}}
sel := declSelection(t, srcFiles, nil)
sel.IsAlive(`func:command-line-arguments.main`)

sel.IsAlive(`funcVar:command-line-arguments.Foo`)
sel.IsAlive(`func:command-line-arguments.Foo<string>`)
sel.IsDead(`func:command-line-arguments.Foo<int>`)

sel.IsAlive(`typeVar:command-line-arguments.Bar`)
sel.IsAlive(`type:command-line-arguments.Bar<string;>`)
sel.IsDead(`type:command-line-arguments.Bar<int;>`)

sel.IsDead(`funcVar:command-line-arguments.deadCode`)
sel.IsDead(`func:command-line-arguments.deadCode`)
}

func TestDeclSelection_RemoveUnusedNestedTypesInMethod(t *testing.T) {
src := `
package main
type Baz[T any] struct{}
func (b *Baz[T]) Foo(u T) any {
type Bar struct { v T }
return Bar{v: u}
}
func deadCode() {
b := Baz[int]{}
println(b.Foo(42))
}
func main() {
b := Baz[string]{}
println(b.Foo("cat"))
}`

srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}}
sel := declSelection(t, srcFiles, nil)
sel.IsAlive(`func:command-line-arguments.main`)

sel.IsAlive(`typeVar:command-line-arguments.Baz`)
sel.IsDead(`type:command-line-arguments.Baz<int>`)
sel.IsAlive(`type:command-line-arguments.Baz<string>`)

sel.IsDead(`func:command-line-arguments.(*Baz).Foo<int>`)
sel.IsAlive(`func:command-line-arguments.(*Baz).Foo<string>`)

sel.IsAlive(`typeVar:command-line-arguments.Bar`)
sel.IsDead(`type:command-line-arguments.Bar<int;>`)
sel.IsAlive(`type:command-line-arguments.Bar<string;>`)

sel.IsDead(`funcVar:command-line-arguments.deadCode`)
sel.IsDead(`func:command-line-arguments.deadCode`)
}

func TestDeclSelection_RemoveAllUnusedNestedTypes(t *testing.T) {
src := `
package main
func Foo[T any](u T) any {
type Bar struct { v T }
return Bar{v: u}
}
func deadCode() {
println(Foo[int](42))
println(Foo[string]("cat"))
}
func main() {}`

srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}}
sel := declSelection(t, srcFiles, nil)
sel.IsAlive(`func:command-line-arguments.main`)

sel.IsDead(`funcVar:command-line-arguments.Foo`)
sel.IsDead(`func:command-line-arguments.Foo<string>`)
sel.IsDead(`func:command-line-arguments.Foo<int>`)

sel.IsDead(`typeVar:command-line-arguments.Bar`)
sel.IsDead(`type:command-line-arguments.Bar<string;>`)
sel.IsDead(`type:command-line-arguments.Bar<int;>`)

sel.IsDead(`funcVar:command-line-arguments.deadCode`)
sel.IsDead(`func:command-line-arguments.deadCode`)
}

func TestDeclSelection_CompletelyRemoveNestedType(t *testing.T) {
src := `
package main
func Foo[T any](u T) any {
type Bar struct { v T }
return Bar{v: u}
}
func deadCode() {
println(Foo[int](42))
}
func main() {}`

srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}}
sel := declSelection(t, srcFiles, nil)

sel.IsAlive(`func:command-line-arguments.main`)

sel.IsDead(`funcVar:command-line-arguments.Foo`)
sel.IsDead(`func:command-line-arguments.Foo<int>`)

sel.IsDead(`typeVar:command-line-arguments.Bar`)
sel.IsDead(`type:command-line-arguments.Bar<int;>`)

sel.IsDead(`funcVar:command-line-arguments.deadCode`)
sel.IsDead(`func:command-line-arguments.deadCode`)
}

func TestDeclSelection_RemoveAnonNestedTypes(t *testing.T) {
// Based on test/fixedbugs/issue53635.go
// This checks that if an anon type (e.g. []T) is used in a function
// that is not used, the type is removed, otherwise it is kept.

src := `
package main
func Foo[T any](u T) any {
return []T(nil)
}
func deadCode() {
println(Foo[string]("cat"))
}
func main() {
println(Foo[int](42))
}`

srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}}
sel := declSelection(t, srcFiles, nil)
sel.IsDead(`anonType:command-line-arguments.sliceType`) // []string
sel.IsAlive(`anonType:command-line-arguments.sliceType$1`) // []int
}

func TestDeclSelection_NoNestAppliedToFuncCallInMethod(t *testing.T) {
// Checks that a function call to a non-local function isn't
// being labeled as a nested function call.
src := `
package main
func foo(a any) {
println(a)
}
type Bar[T any] struct { u T }
func (b *Bar[T]) Baz() {
foo(b.u)
}
func main() {
b := &Bar[int]{u: 42}
b.Baz()
}`

srcFiles := []srctesting.Source{{Name: `main.go`, Contents: []byte(src)}}
sel := declSelection(t, srcFiles, nil)
sel.IsAlive(`init:main`)

sel.IsAlive(`typeVar:command-line-arguments.Bar`)
sel.IsAlive(`type:command-line-arguments.Bar<int>`)
sel.IsAlive(`func:command-line-arguments.(*Bar).Baz<int>`)

sel.IsAlive(`func:command-line-arguments.foo`)
}

func TestLengthParenthesizingIssue841(t *testing.T) {
// See issue https://github.com/gopherjs/gopherjs/issues/841
//
Expand Down
6 changes: 4 additions & 2 deletions compiler/declNames.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,15 +49,17 @@ func funcDeclFullName(inst typeparams.Instance) string {
}

// typeVarDeclFullName returns a unique name for a package-level variable
// that is used for a named type declaration.
// that is used for a named type declaration or a named nested type declaration.
// If the type is generic, this declaration name is also for the list
// of instantiations of the type.
func typeVarDeclFullName(o *types.TypeName) string {
return `typeVar:` + symbol.New(o).String()
}

// typeDeclFullName returns a unique name for a package-level type declaration
// for the given instance of a type. Names are only unique per package.
// for the given instance of a type. Names are only unique per scope package
// unless the type is a nested type then the name is only unique per the
// function or method it is declared in.
func typeDeclFullName(inst typeparams.Instance) string {
return `type:` + inst.String()
}
Expand Down
11 changes: 6 additions & 5 deletions compiler/decls.go
Original file line number Diff line number Diff line change
Expand Up @@ -264,7 +264,7 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl {
fc.localVars = nil // Clean up after ourselves.
})

d.Dce().SetName(init.Lhs[0])
d.Dce().SetName(init.Lhs[0], nil, nil)
if len(init.Lhs) != 1 || analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) {
d.Dce().SetAsAlive()
}
Expand All @@ -291,7 +291,7 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) {
FullName: funcVarDeclFullName(o),
Vars: []string{objName},
}
varDecl.Dce().SetName(o)
varDecl.Dce().SetName(o, nil, nil)
if o.Type().(*types.Signature).TypeParams().Len() != 0 {
varDecl.DeclCode = fc.CatchOutput(0, func() {
fc.Printf("%s = {};", objName)
Expand Down Expand Up @@ -331,7 +331,7 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance)
Blocking: fc.pkgCtx.IsBlocking(inst),
LinkingName: symbol.New(o),
}
d.Dce().SetName(o, inst.TArgs...)
d.Dce().SetName(o, inst.TNest, inst.TArgs)

if typesutil.IsMethod(o) {
recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj()
Expand Down Expand Up @@ -433,6 +433,7 @@ func (fc *funcContext) newNamedTypeVarDecl(obj *types.TypeName) *Decl {
FullName: typeVarDeclFullName(obj),
Vars: []string{name},
}
varDecl.Dce().SetName(obj, nil, nil)
if fc.pkgCtx.instanceSet.Pkg(obj.Pkg()).ObjHasInstances(obj) {
varDecl.DeclCode = fc.CatchOutput(0, func() {
fc.Printf("%s = {};", name)
Expand Down Expand Up @@ -470,7 +471,7 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er
d := &Decl{
FullName: typeDeclFullName(inst),
}
d.Dce().SetName(inst.Object, inst.TArgs...)
d.Dce().SetName(inst.Object, inst.TNest, inst.TArgs)
fc.pkgCtx.CollectDCEDeps(d, func() {
// Code that declares a JS type (i.e. prototype) for each Go type.
d.DeclCode = fc.CatchOutput(0, func() {
Expand Down Expand Up @@ -595,7 +596,7 @@ func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl {
FullName: anonTypeDeclFullName(t),
Vars: []string{t.Name()},
}
d.Dce().SetName(t)
d.Dce().SetName(t, nil, nil)
fc.pkgCtx.CollectDCEDeps(d, func() {
d.DeclCode = []byte(fmt.Sprintf("\t%s = $%sType(%s);\n", t.Name(), strings.ToLower(typeKind(t.Type())[5:]), fc.initArgs(t.Type())))
})
Expand Down
4 changes: 2 additions & 2 deletions compiler/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression {
case types.MethodVal:
return fc.formatExpr(`$methodVal(%s, "%s")`, fc.makeReceiver(e), sel.Obj().(*types.Func).Name())
case types.MethodExpr:
fc.pkgCtx.DeclareDCEDep(sel.Obj(), inst.TArgs...)
fc.pkgCtx.DeclareDCEDep(sel.Obj(), inst.TNest, inst.TArgs)
if _, ok := sel.Recv().Underlying().(*types.Interface); ok {
return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name())
}
Expand Down Expand Up @@ -906,7 +906,7 @@ func (fc *funcContext) delegatedCall(expr *ast.CallExpr) (callable *expression,
func (fc *funcContext) makeReceiver(e *ast.SelectorExpr) *expression {
sel, _ := fc.selectionOf(e)
if !sel.Obj().Exported() {
fc.pkgCtx.DeclareDCEDep(sel.Obj())
fc.pkgCtx.DeclareDCEDep(sel.Obj(), nil, nil)
}

x := e.X
Expand Down
37 changes: 26 additions & 11 deletions compiler/internal/dce/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -265,17 +265,32 @@ of either will indicate a use of both. This will cause slightly more unexported
methods to be alive while reducing the complication of type checking which object
or type of object is performing the call.

| Declaration | exported | unexported | non-generic | generic | object name | method name |
|:------------|:--------:|:----------:|:-----------:|:-------:|:------------|:------------|
| variables | █ | █ | █ | n/a | `<package>.<var name>` | |
| functions | █ | █ | █ | | `<package>.<func name>` | |
| functions | █ | █ | | █ | `<package>.<func name>[<type args>]` | |
| named type | █ | █ | █ | | `<package>.<type name>` | |
| named type | █ | █ | | █ | `<package>.<type name>[<type args>]` | |
| method | █ | | █ | | `<package>.<receiver name>` | |
| method | █ | | | █ | `<package>.<receiver name>[<type args>]` | |
| method | | █ | █ | | `<package>.<receiver name>` | `<package>.<method name>(<parameter types>)(<result types>)` |
| method | | █ | | █ | `<package>.<receiver name>[<type args>]` | `<package>.<method name>(<parameter types>)(<result types>)` |
| Declaration | exported | unexported | generic | object name | method name |
|:------------|:--------:|:----------:|:-------:|:------------|:------------|
| variables | █ | █ | | `<package>.<var name>` | |
| functions | █ | █ | | `<package>.<func name>` | |
| functions | █ | █ | █ | `<package>.<func name>[<type args>]` | |
| named type | █ | █ | | `<package>.<type name>` | |
| named type | █ | █ | █ | `<package>.<type name>[<type args>]` | |
| method | █ | | | `<package>.<receiver name>` | |
| method | █ | | █ | `<package>.<receiver name>[<type args>]` | |
| method | | █ | | `<package>.<receiver name>` | `<package>.<method name>(<parameter types>)(<result types>)` |
| method | | █ | █ | `<package>.<receiver name>[<type args>]` | `<package>.<method name>(<parameter types>)(<result types>)` |

For nested types, the nest (the function or method in which the type is
declared) needs to be taken into consideration to differentiate types with
the same name but different nests.

| Nest type | generic nest | generic object | object name |
|:----------|:------------:|:--------------:|:------------|
| function | | | `<package>.<func name>:<type name>` |
| function | █ | | `<package>.<func name>:<type name>[<nest args>;]` |
| function | | █ | `<package>.<func name>:<type name>[<type args>]` |
| function | █ | █ | `<package>.<func name>:<type name>[<nest args>;<type args>]` |
| method | | | `<package>.<receiver name>:<method name>:<type name>` |
| method | █ | | `<package>.<receiver name>:<method name>:<type name>[<nest args>;]` |
| method | | █ | `<package>.<receiver name>:<method name>:<type name>[<type args>]` |
| method | █ | █ | `<package>.<receiver name>:<method name>:<type name>[<nest args>;<type args>]` |

#### Name Specifics

Expand Down
4 changes: 2 additions & 2 deletions compiler/internal/dce/collector.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,8 @@ func (c *Collector) CollectDCEDeps(decl Decl, f func()) {
// The given optional type arguments are used to when the object is a
// function with type parameters or anytime the object doesn't carry them.
// If not given, this attempts to get the type arguments from the object.
func (c *Collector) DeclareDCEDep(o types.Object, tArgs ...types.Type) {
func (c *Collector) DeclareDCEDep(o types.Object, tNest, tArgs []types.Type) {
if c.dce != nil {
c.dce.addDep(o, tArgs)
c.dce.addDep(o, tNest, tArgs)
}
}
Loading
Loading