Skip to content

Commit

Permalink
Merge pull request gopherjs#1339 from Workiva/encapDCE2
Browse files Browse the repository at this point in the history
Moving DCE into its own package
  • Loading branch information
nevkontakte authored Oct 2, 2024
2 parents 1ebb325 + b9976d5 commit 56cec36
Show file tree
Hide file tree
Showing 11 changed files with 964 additions and 130 deletions.
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

0 comments on commit 56cec36

Please sign in to comment.