Skip to content

Moving DCE into its own package #1339

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
Oct 2, 2024
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
57 changes: 6 additions & 51 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
"strings"
"time"

"github.com/gopherjs/gopherjs/compiler/internal/dce"
"github.com/gopherjs/gopherjs/compiler/prelude"
"golang.org/x/tools/go/gcexportdata"
)
Expand Down Expand Up @@ -125,12 +126,6 @@ func ImportDependencies(archive *Archive, importPkg func(string) (*Archive, erro
return deps, nil
}

type dceInfo struct {
decl *Decl
objectFilter string
methodFilter string
}

func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) error {
mainPkg := pkgs[len(pkgs)-1]
minify := mainPkg.Minified
Expand All @@ -141,61 +136,21 @@ func WriteProgramCode(pkgs []*Archive, w *SourceMapFilter, goVersion string) err
gls.Add(pkg.GoLinknames)
}

byFilter := make(map[string][]*dceInfo)
var pendingDecls []*Decl // A queue of live decls to find other live decls.
sel := &dce.Selector[*Decl]{}
for _, pkg := range pkgs {
for _, d := range pkg.Declarations {
if d.DceObjectFilter == "" && d.DceMethodFilter == "" {
// This is an entry point (like main() or init() functions) or a variable
// initializer which has a side effect, consider it live.
pendingDecls = append(pendingDecls, d)
continue
}
implementsLink := false
if gls.IsImplementation(d.LinkingName) {
// If a decl is referenced by a go:linkname directive, we just assume
// it's not dead.
// TODO(nevkontakte): This is a safe, but imprecise assumption. We should
// try and trace whether the referencing functions are actually live.
pendingDecls = append(pendingDecls, d)
}
info := &dceInfo{decl: d}
if d.DceObjectFilter != "" {
info.objectFilter = pkg.ImportPath + "." + d.DceObjectFilter
byFilter[info.objectFilter] = append(byFilter[info.objectFilter], info)
}
if d.DceMethodFilter != "" {
info.methodFilter = pkg.ImportPath + "." + d.DceMethodFilter
byFilter[info.methodFilter] = append(byFilter[info.methodFilter], info)
}
}
}

dceSelection := make(map[*Decl]struct{}) // Known live decls.
for len(pendingDecls) != 0 {
d := pendingDecls[len(pendingDecls)-1]
pendingDecls = pendingDecls[:len(pendingDecls)-1]

dceSelection[d] = struct{}{} // Mark the decl as live.

// Consider all decls the current one is known to depend on and possible add
// them to the live queue.
for _, dep := range d.DceDeps {
if infos, ok := byFilter[dep]; ok {
delete(byFilter, dep)
for _, info := range infos {
if info.objectFilter == dep {
info.objectFilter = ""
}
if info.methodFilter == dep {
info.methodFilter = ""
}
if info.objectFilter == "" && info.methodFilter == "" {
pendingDecls = append(pendingDecls, info.decl)
}
}
implementsLink = true
}
sel.Include(d, implementsLink)
}
}
dceSelection := sel.AliveDecls()

if _, err := w.Write([]byte("\"use strict\";\n(function() {\n\n")); err != nil {
return err
Expand Down
63 changes: 28 additions & 35 deletions compiler/decls.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import (
"strings"

"github.com/gopherjs/gopherjs/compiler/analysis"
"github.com/gopherjs/gopherjs/compiler/internal/dce"
"github.com/gopherjs/gopherjs/compiler/internal/symbol"
"github.com/gopherjs/gopherjs/compiler/internal/typeparams"
"github.com/gopherjs/gopherjs/compiler/typesutil"
Expand Down Expand Up @@ -51,16 +52,8 @@ type Decl struct {
// JavaScript code that needs to be executed during the package init phase to
// set the symbol up (e.g. initialize package-level variable value).
InitCode []byte
// 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.
DceObjectFilter string
// The second part of the identified used by dead-code elimination for methods.
// Empty for other types of symbols.
DceMethodFilter string
// List of fully qualified (including package path) DCE symbol identifiers the
// symbol depends on for dead code elimination purposes.
DceDeps []string
// dce stores the information for dead-code elimination.
dce dce.Info
// Set to true if a function performs a blocking operation (I/O or
// synchronization). The compiler will have to generate function code such
// that it can be resumed after a blocking operation completes without
Expand All @@ -78,6 +71,11 @@ func (d Decl) minify() Decl {
return d
}

// Dce gets the information for dead-code elimination.
func (d *Decl) Dce() *dce.Info {
return &d.dce
}

// topLevelObjects extracts package-level variables, functions and named types
// from the package AST.
func (fc *funcContext) topLevelObjects(srcs sources) (vars []*types.Var, functions []*ast.FuncDecl, typeNames typesutil.TypeNames) {
Expand Down Expand Up @@ -161,11 +159,13 @@ func (fc *funcContext) importDecls() (importedPaths []string, importDecls []*Dec
// newImportDecl registers the imported package and returns a Decl instance for it.
func (fc *funcContext) newImportDecl(importedPkg *types.Package) *Decl {
pkgVar := fc.importedPkgVar(importedPkg)
return &Decl{
d := &Decl{
Vars: []string{pkgVar},
DeclCode: []byte(fmt.Sprintf("\t%s = $packages[\"%s\"];\n", pkgVar, importedPkg.Path())),
InitCode: fc.CatchOutput(1, func() { fc.translateStmt(fc.importInitializer(importedPkg.Path()), nil) }),
}
d.Dce().SetAsAlive()
return d
}

// importInitializer calls the imported package $init() function to ensure it is
Expand Down Expand Up @@ -241,7 +241,7 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl {
}
}

d.DceDeps = fc.CollectDCEDeps(func() {
fc.pkgCtx.CollectDCEDeps(&d, func() {
fc.localVars = nil
d.InitCode = fc.CatchOutput(1, func() {
fc.translateStmt(&ast.AssignStmt{
Expand All @@ -257,10 +257,9 @@ func (fc *funcContext) newVarDecl(init *types.Initializer) *Decl {
fc.localVars = nil // Clean up after ourselves.
})

if len(init.Lhs) == 1 {
if !analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) {
d.DceObjectFilter = init.Lhs[0].Name()
}
d.Dce().SetName(init.Lhs[0])
if len(init.Lhs) != 1 || analysis.HasSideEffect(init.Rhs, fc.pkgCtx.Info.Info) {
d.Dce().SetAsAlive()
}
return &d
}
Expand All @@ -280,9 +279,8 @@ func (fc *funcContext) funcDecls(functions []*ast.FuncDecl) ([]*Decl, error) {
if fun.Recv == nil {
// Auxiliary decl shared by all instances of the function that defines
// package-level variable by which they all are referenced.
// TODO(nevkontakte): Set DCE attributes such that it is eliminated if all
// instances are dead.
varDecl := Decl{}
varDecl.Dce().SetName(o)
varDecl.Vars = []string{fc.objectName(o)}
if o.Type().(*types.Signature).TypeParams().Len() != 0 {
varDecl.DeclCode = fc.CatchOutput(0, func() {
Expand Down Expand Up @@ -322,29 +320,25 @@ func (fc *funcContext) newFuncDecl(fun *ast.FuncDecl, inst typeparams.Instance)
Blocking: fc.pkgCtx.IsBlocking(o),
LinkingName: symbol.New(o),
}
d.Dce().SetName(o)

if typesutil.IsMethod(o) {
recv := typesutil.RecvType(o.Type().(*types.Signature)).Obj()
d.NamedRecvType = fc.objectName(recv)
d.DceObjectFilter = recv.Name()
if !fun.Name.IsExported() {
d.DceMethodFilter = o.Name() + "~"
}
} else {
d.RefExpr = fc.instName(inst)
d.DceObjectFilter = o.Name()
switch o.Name() {
case "main":
if fc.pkgCtx.isMain() { // Found main() function of the program.
d.DceObjectFilter = "" // Always reachable.
d.Dce().SetAsAlive() // Always reachable.
}
case "init":
d.InitCode = fc.CatchOutput(1, func() { fc.translateStmt(fc.callInitFunc(o), nil) })
d.DceObjectFilter = "" // init() function is always reachable.
d.Dce().SetAsAlive() // init() function is always reachable.
}
}

d.DceDeps = fc.CollectDCEDeps(func() {
fc.pkgCtx.CollectDCEDeps(d, func() {
d.DeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun)
})
return d
Expand Down Expand Up @@ -455,10 +449,9 @@ func (fc *funcContext) newNamedTypeInstDecl(inst typeparams.Instance) (*Decl, er
}

underlying := instanceType.Underlying()
d := &Decl{
DceObjectFilter: inst.Object.Name(),
}
d.DceDeps = fc.CollectDCEDeps(func() {
d := &Decl{}
d.Dce().SetName(inst.Object)
fc.pkgCtx.CollectDCEDeps(d, func() {
// Code that declares a JS type (i.e. prototype) for each Go type.
d.DeclCode = fc.CatchOutput(0, func() {
size := int64(0)
Expand Down Expand Up @@ -577,14 +570,14 @@ func (fc *funcContext) anonTypeDecls(anonTypes []*types.TypeName) []*Decl {
}
decls := []*Decl{}
for _, t := range anonTypes {
d := Decl{
Vars: []string{t.Name()},
DceObjectFilter: t.Name(),
d := &Decl{
Vars: []string{t.Name()},
}
d.DceDeps = fc.CollectDCEDeps(func() {
d.Dce().SetName(t)
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())))
})
decls = append(decls, &d)
decls = append(decls, d)
}
return decls
}
4 changes: 2 additions & 2 deletions compiler/expressions.go
Original file line number Diff line number Diff line change
Expand Up @@ -592,7 +592,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression {
return fc.formatExpr(`$methodVal(%s, "%s")`, fc.makeReceiver(e), sel.Obj().(*types.Func).Name())
case types.MethodExpr:
if !sel.Obj().Exported() {
fc.DeclareDCEDep(sel.Obj())
fc.pkgCtx.DeclareDCEDep(sel.Obj())
}
if _, ok := sel.Recv().Underlying().(*types.Interface); ok {
return fc.formatExpr(`$ifaceMethodExpr("%s")`, sel.Obj().(*types.Func).Name())
Expand Down Expand Up @@ -908,7 +908,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.DeclareDCEDep(sel.Obj())
fc.pkgCtx.DeclareDCEDep(sel.Obj())
}

x := e.X
Expand Down
46 changes: 46 additions & 0 deletions compiler/internal/dce/collector.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package dce

import (
"errors"
"go/types"
)

// Decl is any code declaration that has dead-code elimination (DCE)
// information attached to it.
type Decl interface {
Dce() *Info
}

// Collector is a tool to collect dependencies for a declaration
// that'll be used in dead-code elimination (DCE).
type Collector struct {
dce *Info
}

// CollectDCEDeps captures a list of Go objects (types, functions, etc.)
// the code translated inside f() depends on. Then sets those objects
// as dependencies of the given dead-code elimination info.
//
// Only one CollectDCEDeps call can be active at a time.
func (c *Collector) CollectDCEDeps(decl Decl, f func()) {
if c.dce != nil {
panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`))
}

c.dce = decl.Dce()
defer func() { c.dce = nil }()

f()
}

// DeclareDCEDep records that the code that is currently being transpiled
// 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.dce != nil {
c.dce.addDep(o)
}
}
Loading
Loading