diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go index 7d251029b..ade313b4e 100644 --- a/compiler/internal/dce/collector.go +++ b/compiler/internal/dce/collector.go @@ -14,7 +14,7 @@ type Decl interface { // Collector is a tool to collect dependencies for a declaration // that'll be used in dead-code elimination (DCE). type Collector struct { - dependencies map[types.Object]struct{} + dce *Info } // CollectDCEDeps captures a list of Go objects (types, functions, etc.) @@ -22,25 +22,25 @@ type Collector struct { // as dependencies of the given dead-code elimination info. // // Only one CollectDCEDeps call can be active at a time. -// This will overwrite any previous dependencies collected for the given DCE. func (c *Collector) CollectDCEDeps(decl Decl, f func()) { - if c.dependencies != nil { + if c.dce != nil { panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`)) } - c.dependencies = make(map[types.Object]struct{}) - defer func() { c.dependencies = nil }() + c.dce = decl.Dce() + defer func() { c.dce = nil }() f() - - decl.Dce().setDeps(c.dependencies) } // DeclareDCEDep records that the code that is currently being transpiled -// depends on a given Go object. +// depends on a given Go object with optional type arguments. +// +// 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) { - if c.dependencies == nil { - return // Dependencies are not being collected. + if c.dce != nil { + c.dce.addDep(o) } - c.dependencies[o] = struct{}{} } diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go index c46a7f03c..d75e85879 100644 --- a/compiler/internal/dce/dce_test.go +++ b/compiler/internal/dce/dce_test.go @@ -65,12 +65,13 @@ func Test_Collector_Collecting(t *testing.T) { depCount(t, decl1, 2) depCount(t, decl2, 3) - // The second collection overwrites the first collection. + // The second collection adds to existing dependencies. c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj4) c.DeclareDCEDep(obj5) }) depCount(t, decl1, 2) - depCount(t, decl2, 1) + depCount(t, decl2, 4) } func Test_Info_SetNameAndDep(t *testing.T) { @@ -86,8 +87,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim import Sarah "fmt"`), want: Info{ - importPath: `jim`, - objectFilter: `Sarah`, + objectFilter: `jim.Sarah`, }, wantDep: `jim.Sarah`, }, @@ -97,8 +97,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim var Toby float64`), want: Info{ - importPath: `jim`, - objectFilter: `Toby`, + objectFilter: `jim.Toby`, }, wantDep: `jim.Toby`, }, @@ -108,8 +107,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim const Ludo int = 42`), want: Info{ - importPath: `jim`, - objectFilter: `Ludo`, + objectFilter: `jim.Ludo`, }, wantDep: `jim.Ludo`, }, @@ -126,8 +124,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { } }`), want: Info{ - importPath: `jim`, - objectFilter: `Gobo`, + objectFilter: `jim.Gobo`, }, wantDep: `jim.Gobo`, }, @@ -137,8 +134,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim type Jen struct{}`), want: Info{ - importPath: `jim`, - objectFilter: `Jen`, + objectFilter: `jim.Jen`, }, wantDep: `jim.Jen`, }, @@ -148,8 +144,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim type Henson[T comparable] struct{}`), want: Info{ - importPath: `jim`, - objectFilter: `Henson`, + objectFilter: `jim.Henson`, }, wantDep: `jim.Henson`, }, @@ -159,8 +154,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim func Jareth() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Jareth`, + objectFilter: `jim.Jareth`, }, wantDep: `jim.Jareth`, }, @@ -170,8 +164,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { `package jim func Didymus[T comparable]() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Didymus`, + objectFilter: `jim.Didymus`, }, wantDep: `jim.Didymus`, }, @@ -182,8 +175,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { type Fizzgig string func (f Fizzgig) Kira() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Fizzgig`, + objectFilter: `jim.Fizzgig`, }, wantDep: `jim.Kira~`, }, @@ -194,9 +186,8 @@ func Test_Info_SetNameAndDep(t *testing.T) { type Aughra int func (a Aughra) frank() {}`), want: Info{ - importPath: `jim`, - objectFilter: `Aughra`, - methodFilter: `frank~`, + objectFilter: `jim.Aughra`, + methodFilter: `jim.frank~`, }, wantDep: `jim.frank~`, }, @@ -207,8 +198,7 @@ func Test_Info_SetNameAndDep(t *testing.T) { type wembley struct{} func (w wembley) Red() {}`), want: Info{ - importPath: `jim`, - objectFilter: `wembley`, + objectFilter: `jim.wembley`, }, wantDep: `jim.Red~`, }, @@ -219,12 +209,11 @@ func Test_Info_SetNameAndDep(t *testing.T) { t.Run(tt.name, func(t *testing.T) { d := &testDecl{} equal(t, d.Dce().unnamed(), true) - equal(t, d.Dce().String(), `[unnamed] . -> []`) + equal(t, d.Dce().String(), `[unnamed] -> []`) t.Log(`object:`, types.ObjectString(tt.obj, nil)) d.Dce().SetName(tt.obj) equal(t, d.Dce().unnamed(), tt.want.unnamed()) - equal(t, d.Dce().importPath, tt.want.importPath) equal(t, d.Dce().objectFilter, tt.want.objectFilter) equal(t, d.Dce().methodFilter, tt.want.methodFilter) equal(t, d.Dce().String(), tt.want.String()) @@ -238,11 +227,17 @@ func Test_Info_SetNameAndDep(t *testing.T) { d := &testDecl{} t.Log(`object:`, types.ObjectString(tt.obj, nil)) - d.Dce().setDeps(map[types.Object]struct{}{ - tt.obj: {}, + wantDeps := []string{} + if len(tt.wantDep) > 0 { + wantDeps = append(wantDeps, tt.wantDep) + } + sort.Strings(wantDeps) + + c := Collector{} + c.CollectDCEDeps(d, func() { + c.DeclareDCEDep(tt.obj) }) - equal(t, len(d.Dce().deps), 1) - equal(t, d.Dce().deps[0], tt.wantDep) + equalSlices(t, d.Dce().getDeps(), wantDeps) }) } }) @@ -269,11 +264,11 @@ func Test_Info_SetAsAlive(t *testing.T) { obj := quickVar(pkg, `Falkor`) decl := &testDecl{} equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive - equal(t, decl.Dce().String(), `[unnamed] . -> []`) + equal(t, decl.Dce().String(), `[unnamed] -> []`) decl.Dce().SetAsAlive() equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive - equal(t, decl.Dce().String(), `[alive] [unnamed] . -> []`) + equal(t, decl.Dce().String(), `[alive] [unnamed] -> []`) decl.Dce().SetName(obj) equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called @@ -284,7 +279,7 @@ func Test_Info_SetAsAlive(t *testing.T) { obj := quickVar(pkg, `Artax`) decl := &testDecl{} equal(t, decl.Dce().isAlive(), true) // unnamed is automatically alive - equal(t, decl.Dce().String(), `[unnamed] . -> []`) + equal(t, decl.Dce().String(), `[unnamed] -> []`) decl.Dce().SetName(obj) equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive @@ -493,6 +488,7 @@ func Test_Selector_SpecificMethods(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + vetinari.Dce().deps = nil // reset deps c.CollectDCEDeps(vetinari, func() { for _, decl := range tt.deps { c.DeclareDCEDep(decl.obj) @@ -626,6 +622,17 @@ func depCount(t *testing.T, decl *testDecl, want int) { func equal[T comparable](t *testing.T, got, want T) { t.Helper() if got != want { - t.Errorf(`expected %#v but got %#v`, want, got) + t.Errorf("Unexpected value was gotten:\n\texp: %#v\n\tgot: %#v", want, got) + } +} + +func equalSlices[T comparable](t *testing.T, got, want []T) { + t.Helper() + if len(got) != len(want) { + t.Errorf("expected %d but got %d\n\texp: %#v\n\tgot: %#v", len(want), len(got), want, got) + return + } + for i, wantElem := range want { + equal(t, got[i], wantElem) } } diff --git a/compiler/internal/dce/filters.go b/compiler/internal/dce/filters.go new file mode 100644 index 000000000..3d0e1eab5 --- /dev/null +++ b/compiler/internal/dce/filters.go @@ -0,0 +1,32 @@ +package dce + +import ( + "go/types" + + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// getFilters determines the DCE filters for the given object. +// This will return an object filter and optionally return a method filter. +func getFilters(o types.Object) (objectFilter, methodFilter string) { + importPath := o.Pkg().Path() + if typesutil.IsMethod(o) { + recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() + objectFilter = importPath + `.` + recv.Name() + if !o.Exported() { + methodFilter = importPath + `.` + o.Name() + `~` + } + } else { + objectFilter = importPath + `.` + o.Name() + } + return +} + +// getDepFilter returns the filter for the given object to be used as a dependency. +func getDepFilter(o types.Object) string { + qualifiedName := o.Pkg().Path() + "." + o.Name() + if typesutil.IsMethod(o) { + qualifiedName += "~" + } + return qualifiedName +} diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go index d5993a659..c80ee8c99 100644 --- a/compiler/internal/dce/info.go +++ b/compiler/internal/dce/info.go @@ -5,8 +5,6 @@ import ( "go/types" "sort" "strings" - - "github.com/gopherjs/gopherjs/compiler/typesutil" ) // Info contains information used by the dead-code elimination (DCE) logic to @@ -17,21 +15,21 @@ type Info struct { // and will not be eliminated. alive bool - // importPath is the package path of the package the declaration is in. - importPath string - - // Symbol's identifier used by the dead-code elimination logic, not including - // package path. If empty, the symbol is assumed to be alive and will not be - // eliminated. For methods it is the same as its receiver type identifier. + // objectFilter is the primary DCE name for a declaration. + // This will be the variable, function, or type identifier. + // For methods it is the receiver type identifier. + // If empty, the declaration is assumed to be alive. objectFilter string - // The second part of the identified used by dead-code elimination for methods. - // Empty for other types of symbols. + // methodFilter is the secondary DCE name for a declaration. + // This will be empty if objectFilter is empty. + // This will be set to a qualified method name if the objectFilter + // can not determine if the declaration is alive on it's own. methodFilter string - // List of fully qualified (including package path) DCE symbol identifiers the - // symbol depends on for dead code elimination purposes. - deps []string + // Set of fully qualified (including package path) DCE symbol + // and/or method names that this DCE declaration depends on. + deps map[string]struct{} } // String gets a human-readable representation of the DCE info. @@ -43,11 +41,14 @@ func (d *Info) String() string { if d.unnamed() { tags += `[unnamed] ` } - fullName := d.importPath + `.` + d.objectFilter + names := []string{} + if len(d.objectFilter) > 0 { + names = append(names, d.objectFilter+` `) + } if len(d.methodFilter) > 0 { - fullName += `.` + d.methodFilter + names = append(names, d.methodFilter+` `) } - return tags + fullName + ` -> [` + strings.Join(d.deps, `, `) + `]` + return tags + strings.Join(names, `& `) + `-> [` + strings.Join(d.getDeps(), `, `) + `]` } // unnamed returns true if SetName has not been called for this declaration. @@ -79,30 +80,35 @@ func (d *Info) SetName(o types.Object) { panic(fmt.Errorf(`may only set the name once for %s`, d.String())) } - d.importPath = o.Pkg().Path() - if typesutil.IsMethod(o) { - recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj() - d.objectFilter = recv.Name() - if !o.Exported() { - d.methodFilter = o.Name() + `~` - } - } else { - d.objectFilter = o.Name() - } + // Determine name(s) for DCE. + d.objectFilter, d.methodFilter = getFilters(o) } -// setDeps sets the declaration dependencies used by DCE +// addDep add a declaration dependencies used by DCE // for the declaration this DCE info is attached to. -// This overwrites any prior set dependencies. -func (d *Info) setDeps(objectSet map[types.Object]struct{}) { - deps := make([]string, 0, len(objectSet)) - for o := range objectSet { - qualifiedName := o.Pkg().Path() + "." + o.Name() - if typesutil.IsMethod(o) { - qualifiedName += "~" +func (d *Info) addDep(o types.Object) { + qualifiedName := getDepFilter(o) + d.addDepName(qualifiedName) +} + +// addDepName adds a declaration dependency by name. +func (d *Info) addDepName(depName string) { + if len(depName) > 0 { + if d.deps == nil { + d.deps = make(map[string]struct{}) } - deps = append(deps, qualifiedName) + d.deps[depName] = struct{}{} + } +} + +// getDeps gets the dependencies for the declaration sorted by name. +func (id *Info) getDeps() []string { + deps := make([]string, len(id.deps)) + i := 0 + for dep := range id.deps { + deps[i] = dep + i++ } sort.Strings(deps) - d.deps = deps + return deps } diff --git a/compiler/internal/dce/selector.go b/compiler/internal/dce/selector.go index 4eea572e0..3dff49028 100644 --- a/compiler/internal/dce/selector.go +++ b/compiler/internal/dce/selector.go @@ -42,12 +42,12 @@ func (s *Selector[D]) Include(decl D, implementsLink bool) { info := &declInfo[D]{decl: decl} if dce.objectFilter != `` { - info.objectFilter = dce.importPath + `.` + dce.objectFilter + info.objectFilter = dce.objectFilter s.byFilter[info.objectFilter] = append(s.byFilter[info.objectFilter], info) } if dce.methodFilter != `` { - info.methodFilter = dce.importPath + `.` + dce.methodFilter + info.methodFilter = dce.methodFilter s.byFilter[info.methodFilter] = append(s.byFilter[info.methodFilter], info) } } @@ -72,7 +72,7 @@ func (s *Selector[D]) AliveDecls() map[D]struct{} { // Consider all decls the current one is known to depend on and possible add // them to the live queue. - for _, dep := range dce.deps { + for _, dep := range dce.getDeps() { if infos, ok := s.byFilter[dep]; ok { delete(s.byFilter, dep) for _, info := range infos {