diff --git a/README.md b/README.md index d17409736..eac9d4405 100644 --- a/README.md +++ b/README.md @@ -57,11 +57,7 @@ _Note: GopherJS will try to write compiled object files of the core packages to #### gopherjs run, gopherjs test -If you want to use `gopherjs run` or `gopherjs test` to run the generated code locally, install Node.js 10.0.0 (or newer), and the `source-map-support` module: - -``` -npm install --global source-map-support -``` +If you want to use `gopherjs run` or `gopherjs test` to run the generated code locally, install Node.js 18 (or newer). On supported `GOOS` platforms, it's possible to make system calls (file system access, etc.) available. See [doc/syscalls.md](https://github.com/gopherjs/gopherjs/blob/master/doc/syscalls.md) for instructions on how to do so. diff --git a/compiler/compiler.go b/compiler/compiler.go index b8f6a49bc..cffd4c86d 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -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" ) @@ -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 @@ -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 diff --git a/compiler/decls.go b/compiler/decls.go index 36f97d3ff..b6427a697 100644 --- a/compiler/decls.go +++ b/compiler/decls.go @@ -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" @@ -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 @@ -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) { @@ -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 @@ -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{ @@ -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 } @@ -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() { @@ -322,30 +320,26 @@ 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() { - d.DeclCode = fc.translateTopLevelFunction(fun, inst) + fc.pkgCtx.CollectDCEDeps(d, func() { + d.DeclCode = fc.namedFuncContext(inst).translateTopLevelFunction(fun) }) return d } @@ -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) @@ -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 } diff --git a/compiler/expressions.go b/compiler/expressions.go index 4b6653731..31171ca5a 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -201,7 +201,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } case *ast.FuncLit: - fun := fc.nestedFunctionContext(fc.pkgCtx.FuncLitInfos[e], exprType.(*types.Signature), typeparams.Instance{}).translateFunctionBody(e.Type, nil, e.Body, "") + fun := fc.literalFuncContext(e).translateFunctionBody(e.Type, nil, e.Body) if len(fc.pkgCtx.escapingVars) != 0 { names := make([]string, 0, len(fc.pkgCtx.escapingVars)) for obj := range fc.pkgCtx.escapingVars { @@ -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()) @@ -730,10 +730,7 @@ func (fc *funcContext) translateExpr(expr ast.Expr) *expression { } } - methodName := sel.Obj().Name() - if reservedKeywords[methodName] { - methodName += "$" - } + methodName := fc.methodName(sel.Obj().(*types.Func)) return fc.translateCall(e, sig, fc.formatExpr("%s.%s", recv, methodName)) case types.FieldVal: @@ -911,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 diff --git a/compiler/functions.go b/compiler/functions.go index ed3062c60..31a9974eb 100644 --- a/compiler/functions.go +++ b/compiler/functions.go @@ -5,6 +5,7 @@ package compiler import ( "bytes" + "errors" "fmt" "go/ast" "go/types" @@ -17,18 +18,21 @@ import ( "github.com/gopherjs/gopherjs/compiler/typesutil" ) -// newFunctionContext creates a new nested context for a function corresponding +// nestedFunctionContext creates a new nested context for a function corresponding // to the provided info and instance. -func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types.Signature, inst typeparams.Instance) *funcContext { +func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, inst typeparams.Instance) *funcContext { if info == nil { - panic(fmt.Errorf("missing *analysis.FuncInfo")) + panic(errors.New("missing *analysis.FuncInfo")) } - if sig == nil { - panic(fmt.Errorf("missing *types.Signature")) + if inst.Object == nil { + panic(errors.New("missing inst.Object")) } + o := inst.Object.(*types.Func) + sig := o.Type().(*types.Signature) c := &funcContext{ FuncInfo: info, + instance: inst, pkgCtx: fc.pkgCtx, parent: fc, allVars: make(map[string]int, len(fc.allVars)), @@ -53,53 +57,105 @@ func (fc *funcContext) nestedFunctionContext(info *analysis.FuncInfo, sig *types c.objectNames = map[types.Object]string{} } + // Synthesize an identifier by which the function may reference itself. Since + // it appears in the stack trace, it's useful to include the receiver type in + // it. + funcRef := o.Name() + if recvType := typesutil.RecvType(sig); recvType != nil { + funcRef = recvType.Obj().Name() + midDot + funcRef + } + c.funcRef = c.newVariable(funcRef, true /*pkgLevel*/) + + return c +} + +// namedFuncContext creates a new funcContext for a named Go function +// (standalone or method). +func (fc *funcContext) namedFuncContext(inst typeparams.Instance) *funcContext { + info := fc.pkgCtx.FuncDeclInfos[inst.Object.(*types.Func)] + c := fc.nestedFunctionContext(info, inst) + + return c +} + +// literalFuncContext creates a new funcContext for a function literal. Since +// go/types doesn't generate *types.Func objects for function literals, we +// generate a synthetic one for it. +func (fc *funcContext) literalFuncContext(fun *ast.FuncLit) *funcContext { + info := fc.pkgCtx.FuncLitInfos[fun] + sig := fc.pkgCtx.TypeOf(fun).(*types.Signature) + o := types.NewFunc(fun.Pos(), fc.pkgCtx.Pkg, fc.newLitFuncName(), sig) + inst := typeparams.Instance{Object: o} + + c := fc.nestedFunctionContext(info, inst) return c } // translateTopLevelFunction translates a top-level function declaration -// (standalone function or method) into a corresponding JS function. +// (standalone function or method) into a corresponding JS function. Must be +// called on the function context created for the function corresponding instance. // -// Returns a string with a JavaScript statements that define the function or +// Returns a string with JavaScript statements that define the function or // method. For methods it returns declarations for both value- and // pointer-receiver (if appropriate). -func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typeparams.Instance) []byte { +func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl) []byte { if fun.Recv == nil { - return fc.translateStandaloneFunction(fun, inst) + return fc.translateStandaloneFunction(fun) } - o := inst.Object.(*types.Func) - info := fc.pkgCtx.FuncDeclInfos[o] + return fc.translateMethod(fun) +} + +// translateStandaloneFunction translates a package-level function. +// +// It returns JS statements which define the corresponding function in a +// package context. Exported functions are also assigned to the `$pkg` object. +func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl) []byte { + o := fc.instance.Object.(*types.Func) + + if fun.Recv != nil { + panic(fmt.Errorf("expected standalone function, got method: %s", o)) + } + + lvalue := fc.instName(fc.instance) + + if fun.Body == nil { + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) + } + + body := fc.translateFunctionBody(fun.Type, nil, fun.Body) + code := bytes.NewBuffer(nil) + fmt.Fprintf(code, "\t%s = %s;\n", lvalue, body) + if fun.Name.IsExported() { + fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) + } + return code.Bytes() +} + +// translateMethod translates a named type method. +// +// It returns one or more JS statements which define the method. Methods with +// non-pointer receiver are automatically defined for the pointer-receiver type. +func (fc *funcContext) translateMethod(fun *ast.FuncDecl) []byte { + o := fc.instance.Object.(*types.Func) + funName := fc.methodName(o) - sig := o.Type().(*types.Signature) // primaryFunction generates a JS function equivalent of the current Go function // and assigns it to the JS expression defined by lvalue. primaryFunction := func(lvalue string) []byte { if fun.Body == nil { - return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", lvalue, o.FullName())) + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fc.unimplementedFunction(o))) } var recv *ast.Ident if fun.Recv != nil && fun.Recv.List[0].Names != nil { recv = fun.Recv.List[0].Names[0] } - fun := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, recv, fun.Body, lvalue) - return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun)) - } - - funName := fun.Name.Name - if reservedKeywords[funName] { - funName += "$" - } - - // proxyFunction generates a JS function that forwards the call to the actual - // method implementation for the alternate receiver (e.g. pointer vs - // non-pointer). - proxyFunction := func(lvalue, receiver string) []byte { - fun := fmt.Sprintf("function(...$args) { return %s.%s(...$args); }", receiver, funName) + fun := fc.translateFunctionBody(fun.Type, recv, fun.Body) return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun)) } - recvInst := inst.Recv() + recvInst := fc.instance.Recv() recvInstName := fc.instName(recvInst) recvType := recvInst.Object.Type().(*types.Named) @@ -108,70 +164,51 @@ func (fc *funcContext) translateTopLevelFunction(fun *ast.FuncDecl, inst typepar prototypeVar := fmt.Sprintf("%s.prototype.%s", recvInstName, funName) ptrPrototypeVar := fmt.Sprintf("$ptrType(%s).prototype.%s", recvInstName, funName) - code := bytes.NewBuffer(nil) + // Methods with pointer-receiver are only attached to the pointer-receiver type. + if _, isPointer := fc.sig.Sig.Recv().Type().(*types.Pointer); isPointer { + return primaryFunction(ptrPrototypeVar) + } + + // Methods with non-pointer receivers must be defined both for the pointer + // and non-pointer types. To minimize generated code size, we generate a + // complete implementation for only one receiver (non-pointer for most types) + // and define a proxy function on the other, which converts the receiver type + // and forwards the call to the primary implementation. + proxyFunction := func(lvalue, receiver string) []byte { + fun := fmt.Sprintf("function(...$args) { return %s.%s(...$args); }", receiver, funName) + return []byte(fmt.Sprintf("\t%s = %s;\n", lvalue, fun)) + } + // Structs are a special case: they are represented by JS objects and their + // methods are the underlying object's methods. Due to reference semantics of + // the JS variables, the actual backing object is considered to represent the + // pointer-to-struct type, and methods are attacher to it first and foremost. if _, isStruct := recvType.Underlying().(*types.Struct); isStruct { - // Structs are a special case: they are represented by JS objects and their - // methods are the underlying object's methods. Due to reference semantics - // of the JS variables, the actual backing object is considered to represent - // the pointer-to-struct type, and methods are attacher to it first and - // foremost. + code := bytes.Buffer{} code.Write(primaryFunction(ptrPrototypeVar)) code.Write(proxyFunction(prototypeVar, "this.$val")) return code.Bytes() } - if ptr, isPointer := sig.Recv().Type().(*types.Pointer); isPointer { - if _, isArray := ptr.Elem().Underlying().(*types.Array); isArray { - // Pointer-to-array is another special case. - // TODO(nevkontakte) Find out and document why. - code.Write(primaryFunction(prototypeVar)) - code.Write(proxyFunction(ptrPrototypeVar, fmt.Sprintf("(new %s(this.$get()))", recvInstName))) - return code.Bytes() - } - - // Methods with pointer-receiver are only attached to the pointer-receiver - // type. - return primaryFunction(ptrPrototypeVar) - } - // Methods defined for non-pointer receiver are attached to both pointer- and // non-pointer-receiver types. - recvExpr := "this.$get()" + proxyRecvExpr := "this.$get()" if isWrapped(recvType) { - recvExpr = fmt.Sprintf("new %s(%s)", recvInstName, recvExpr) + proxyRecvExpr = fmt.Sprintf("new %s(%s)", recvInstName, proxyRecvExpr) } + code := bytes.Buffer{} code.Write(primaryFunction(prototypeVar)) - code.Write(proxyFunction(ptrPrototypeVar, recvExpr)) + code.Write(proxyFunction(ptrPrototypeVar, proxyRecvExpr)) return code.Bytes() } -// translateStandaloneFunction translates a package-level function. +// unimplementedFunction returns a JS function expression for a Go function +// without a body, which would throw an exception if called. // -// It returns a JS statements which define the corresponding function in a -// package context. Exported functions are also assigned to the `$pkg` object. -func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typeparams.Instance) []byte { - o := inst.Object.(*types.Func) - info := fc.pkgCtx.FuncDeclInfos[o] - sig := o.Type().(*types.Signature) - - if fun.Recv != nil { - panic(fmt.Errorf("expected standalone function, got method: %s", o)) - } - - lvalue := fc.instName(inst) - - if fun.Body == nil { - return []byte(fmt.Sprintf("\t%s = function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t};\n", lvalue, o.FullName())) - } - - body := fc.nestedFunctionContext(info, sig, inst).translateFunctionBody(fun.Type, nil, fun.Body, lvalue) - code := bytes.NewBuffer(nil) - fmt.Fprintf(code, "\t%s = %s;\n", lvalue, body) - if fun.Name.IsExported() { - fmt.Fprintf(code, "\t$pkg.%s = %s;\n", encodeIdent(fun.Name.Name), lvalue) - } - return code.Bytes() +// In Go such functions are either used with a //go:linkname directive or with +// assembler intrinsics, only former of which is supported by GopherJS. +func (fc *funcContext) unimplementedFunction(o *types.Func) string { + return fmt.Sprintf("function() {\n\t\t$throwRuntimeError(\"native function not implemented: %s\");\n\t}", o.FullName()) } // translateFunctionBody translates body of a top-level or literal function. @@ -179,7 +216,7 @@ func (fc *funcContext) translateStandaloneFunction(fun *ast.FuncDecl, inst typep // It returns a JS function expression that represents the given Go function. // Function receiver must have been created with nestedFunctionContext() to have // required metadata set up. -func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt, funcRef string) string { +func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, body *ast.BlockStmt) string { prevEV := fc.pkgCtx.escapingVars // Generate a list of function argument variables. Since Go allows nameless @@ -233,7 +270,7 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, sort.Strings(fc.localVars) - var prefix, suffix, functionName string + var prefix, suffix string if len(fc.Flattened) != 0 { // $s contains an index of the switch case a blocking function reached @@ -254,21 +291,19 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, localVarDefs := "" // Function-local var declaration at the top. if len(fc.Blocking) != 0 { - if funcRef == "" { - funcRef = "$b" - functionName = " $b" - } - localVars := append([]string{}, fc.localVars...) // There are several special variables involved in handling blocking functions: // $r is sometimes used as a temporary variable to store blocking call result. // $c indicates that a function is being resumed after a blocking call when set to true. // $f is an object used to save and restore function context for blocking calls. localVars = append(localVars, "$r") + // funcRef identifies the function object itself, so it doesn't need to be saved + // or restored. + localVars = removeMatching(localVars, fc.funcRef) // If a blocking function is being resumed, initialize local variables from the saved context. localVarDefs = fmt.Sprintf("var {%s, $c} = $restore(this, {%s});\n", strings.Join(localVars, ", "), strings.Join(args, ", ")) // If the function gets blocked, save local variables for future. - saveContext := fmt.Sprintf("var $f = {$blk: "+funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", ")) + saveContext := fmt.Sprintf("var $f = {$blk: "+fc.funcRef+", $c: true, $r, %s};", strings.Join(fc.localVars, ", ")) suffix = " " + saveContext + "return $f;" + suffix } else if len(fc.localVars) > 0 { @@ -316,5 +351,5 @@ func (fc *funcContext) translateFunctionBody(typ *ast.FuncType, recv *ast.Ident, fc.pkgCtx.escapingVars = prevEV - return fmt.Sprintf("function%s(%s) {\n%s%s}", functionName, strings.Join(args, ", "), bodyOutput, fc.Indentation(0)) + return fmt.Sprintf("function %s(%s) {\n%s%s}", fc.funcRef, strings.Join(args, ", "), bodyOutput, fc.Indentation(0)) } diff --git a/compiler/internal/dce/collector.go b/compiler/internal/dce/collector.go new file mode 100644 index 000000000..7d251029b --- /dev/null +++ b/compiler/internal/dce/collector.go @@ -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 { + dependencies map[types.Object]struct{} +} + +// 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. +// This will overwrite any previous dependencies collected for the given DCE. +func (c *Collector) CollectDCEDeps(decl Decl, f func()) { + if c.dependencies != nil { + panic(errors.New(`called CollectDCEDeps inside another CollectDCEDeps call`)) + } + + c.dependencies = make(map[types.Object]struct{}) + defer func() { c.dependencies = nil }() + + f() + + decl.Dce().setDeps(c.dependencies) +} + +// DeclareDCEDep records that the code that is currently being transpiled +// depends on a given Go object. +func (c *Collector) DeclareDCEDep(o types.Object) { + if c.dependencies == nil { + return // Dependencies are not being collected. + } + c.dependencies[o] = struct{}{} +} diff --git a/compiler/internal/dce/dce_test.go b/compiler/internal/dce/dce_test.go new file mode 100644 index 000000000..c46a7f03c --- /dev/null +++ b/compiler/internal/dce/dce_test.go @@ -0,0 +1,631 @@ +package dce + +import ( + "fmt" + "go/ast" + "go/importer" + "go/parser" + "go/token" + "go/types" + "regexp" + "sort" + "testing" +) + +func Test_Collector_CalledOnce(t *testing.T) { + var c Collector + decl1 := &testDecl{} + decl2 := &testDecl{} + + err := capturePanic(t, func() { + c.CollectDCEDeps(decl1, func() { + c.CollectDCEDeps(decl2, func() { + t.Fatal(`the nested collect function was called`) + }) + }) + }) + errorMatches(t, err, `^called CollectDCEDeps inside another`) +} + +func Test_Collector_Collecting(t *testing.T) { + pkg := testPackage(`tristan`) + obj1 := quickVar(pkg, `Primus`) + obj2 := quickVar(pkg, `Secundus`) + obj3 := quickVar(pkg, `Tertius`) + obj4 := quickVar(pkg, `Quartus`) + obj5 := quickVar(pkg, `Quintus`) + obj6 := quickVar(pkg, `Sextus`) + obj7 := quickVar(pkg, `Una`) + + decl1 := quickTestDecl(obj1) + decl2 := quickTestDecl(obj2) + var c Collector + + c.DeclareDCEDep(obj1) // no effect since a collection isn't running. + depCount(t, decl1, 0) + depCount(t, decl2, 0) + + c.CollectDCEDeps(decl1, func() { + c.DeclareDCEDep(obj2) + c.DeclareDCEDep(obj3) + c.DeclareDCEDep(obj3) // already added so has no effect. + }) + depCount(t, decl1, 2) + depCount(t, decl2, 0) + + c.DeclareDCEDep(obj4) // no effect since a collection isn't running. + depCount(t, decl1, 2) + depCount(t, decl2, 0) + + c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj5) + c.DeclareDCEDep(obj6) + c.DeclareDCEDep(obj7) + }) + depCount(t, decl1, 2) + depCount(t, decl2, 3) + + // The second collection overwrites the first collection. + c.CollectDCEDeps(decl2, func() { + c.DeclareDCEDep(obj5) + }) + depCount(t, decl1, 2) + depCount(t, decl2, 1) +} + +func Test_Info_SetNameAndDep(t *testing.T) { + tests := []struct { + name string + obj types.Object + want Info // expected Info after SetName + wantDep string // expected dep after addDep + }{ + { + name: `package`, + obj: parseObject(t, `Sarah`, + `package jim + import Sarah "fmt"`), + want: Info{ + importPath: `jim`, + objectFilter: `Sarah`, + }, + wantDep: `jim.Sarah`, + }, + { + name: `exposed var`, + obj: parseObject(t, `Toby`, + `package jim + var Toby float64`), + want: Info{ + importPath: `jim`, + objectFilter: `Toby`, + }, + wantDep: `jim.Toby`, + }, + { + name: `exposed const`, + obj: parseObject(t, `Ludo`, + `package jim + const Ludo int = 42`), + want: Info{ + importPath: `jim`, + objectFilter: `Ludo`, + }, + wantDep: `jim.Ludo`, + }, + { + name: `label`, + obj: parseObject(t, `Gobo`, + `package jim + func main() { + i := 0 + Gobo: + i++ + if i < 10 { + goto Gobo + } + }`), + want: Info{ + importPath: `jim`, + objectFilter: `Gobo`, + }, + wantDep: `jim.Gobo`, + }, + { + name: `exposed specific type`, + obj: parseObject(t, `Jen`, + `package jim + type Jen struct{}`), + want: Info{ + importPath: `jim`, + objectFilter: `Jen`, + }, + wantDep: `jim.Jen`, + }, + { + name: `exposed generic type`, + obj: parseObject(t, `Henson`, + `package jim + type Henson[T comparable] struct{}`), + want: Info{ + importPath: `jim`, + objectFilter: `Henson`, + }, + wantDep: `jim.Henson`, + }, + { + name: `exposed specific function`, + obj: parseObject(t, `Jareth`, + `package jim + func Jareth() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Jareth`, + }, + wantDep: `jim.Jareth`, + }, + { + name: `exposed generic function`, + obj: parseObject(t, `Didymus`, + `package jim + func Didymus[T comparable]() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Didymus`, + }, + wantDep: `jim.Didymus`, + }, + { + name: `exposed specific method`, + obj: parseObject(t, `Kira`, + `package jim + type Fizzgig string + func (f Fizzgig) Kira() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Fizzgig`, + }, + wantDep: `jim.Kira~`, + }, + { + name: `unexposed specific method`, + obj: parseObject(t, `frank`, + `package jim + type Aughra int + func (a Aughra) frank() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `Aughra`, + methodFilter: `frank~`, + }, + wantDep: `jim.frank~`, + }, + { + name: `specific method on unexposed type`, + obj: parseObject(t, `Red`, + `package jim + type wembley struct{} + func (w wembley) Red() {}`), + want: Info{ + importPath: `jim`, + objectFilter: `wembley`, + }, + wantDep: `jim.Red~`, + }, + } + + t.Run(`SetName`, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + equal(t, d.Dce().unnamed(), true) + 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()) + }) + } + }) + + t.Run(`addDep`, func(t *testing.T) { + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + d := &testDecl{} + t.Log(`object:`, types.ObjectString(tt.obj, nil)) + + d.Dce().setDeps(map[types.Object]struct{}{ + tt.obj: {}, + }) + equal(t, len(d.Dce().deps), 1) + equal(t, d.Dce().deps[0], tt.wantDep) + }) + } + }) +} + +func Test_Info_SetNameOnlyOnce(t *testing.T) { + pkg := testPackage(`mogwai`) + obj1 := quickVar(pkg, `Gizmo`) + obj2 := quickVar(pkg, `Stripe`) + + decl := &testDecl{} + decl.Dce().SetName(obj1) + + err := capturePanic(t, func() { + decl.Dce().SetName(obj2) + }) + errorMatches(t, err, `^may only set the name once for path/to/mogwai\.Gizmo .*$`) +} + +func Test_Info_SetAsAlive(t *testing.T) { + pkg := testPackage(`fantasia`) + + t.Run(`set alive prior to naming`, func(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] . -> []`) + + decl.Dce().SetAsAlive() + equal(t, decl.Dce().isAlive(), true) // still alive but now explicitly alive + equal(t, decl.Dce().String(), `[alive] [unnamed] . -> []`) + + decl.Dce().SetName(obj) + equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called + equal(t, decl.Dce().String(), `[alive] path/to/fantasia.Falkor -> []`) + }) + + t.Run(`set alive after naming`, func(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] . -> []`) + + decl.Dce().SetName(obj) + equal(t, decl.Dce().isAlive(), false) // named so no longer automatically alive + equal(t, decl.Dce().String(), `path/to/fantasia.Artax -> []`) + + decl.Dce().SetAsAlive() + equal(t, decl.Dce().isAlive(), true) // alive because SetAsAlive was called + equal(t, decl.Dce().String(), `[alive] path/to/fantasia.Artax -> []`) + }) +} + +func Test_Selector_JustVars(t *testing.T) { + pkg := testPackage(`tolkien`) + frodo := quickTestDecl(quickVar(pkg, `Frodo`)) + samwise := quickTestDecl(quickVar(pkg, `Samwise`)) + meri := quickTestDecl(quickVar(pkg, `Meri`)) + pippin := quickTestDecl(quickVar(pkg, `Pippin`)) + aragorn := quickTestDecl(quickVar(pkg, `Aragorn`)) + boromir := quickTestDecl(quickVar(pkg, `Boromir`)) + gimli := quickTestDecl(quickVar(pkg, `Gimli`)) + legolas := quickTestDecl(quickVar(pkg, `Legolas`)) + gandalf := quickTestDecl(quickVar(pkg, `Gandalf`)) + fellowship := []*testDecl{ + frodo, samwise, meri, pippin, aragorn, + boromir, gimli, legolas, gandalf, + } + + c := Collector{} + c.CollectDCEDeps(frodo, func() { + c.DeclareDCEDep(samwise.obj) + c.DeclareDCEDep(meri.obj) + c.DeclareDCEDep(pippin.obj) + }) + c.CollectDCEDeps(pippin, func() { + c.DeclareDCEDep(meri.obj) + }) + c.CollectDCEDeps(aragorn, func() { + c.DeclareDCEDep(boromir.obj) + }) + c.CollectDCEDeps(gimli, func() { + c.DeclareDCEDep(legolas.obj) + }) + c.CollectDCEDeps(legolas, func() { + c.DeclareDCEDep(gimli.obj) + }) + c.CollectDCEDeps(gandalf, func() { + c.DeclareDCEDep(frodo.obj) + c.DeclareDCEDep(aragorn.obj) + c.DeclareDCEDep(gimli.obj) + c.DeclareDCEDep(legolas.obj) + }) + + for _, decl := range fellowship { + equal(t, decl.Dce().isAlive(), false) + } + + tests := []struct { + name string + init []*testDecl // which decls to set explicitly alive + want []*testDecl // which decls should be determined as alive + }{ + { + name: `all alive`, + init: fellowship, + want: fellowship, + }, + { + name: `all dead`, + init: []*testDecl{}, + want: []*testDecl{}, + }, + { + name: `Frodo`, + init: []*testDecl{frodo}, + want: []*testDecl{frodo, samwise, meri, pippin}, + }, + { + name: `Sam and Pippin`, + init: []*testDecl{samwise, pippin}, + want: []*testDecl{samwise, meri, pippin}, + }, + { + name: `Gandalf`, + init: []*testDecl{gandalf}, + want: fellowship, + }, + { + name: `Legolas`, + init: []*testDecl{legolas}, + want: []*testDecl{legolas, gimli}, + }, + { + name: `Gimli`, + init: []*testDecl{gimli}, + want: []*testDecl{legolas, gimli}, + }, + { + name: `Boromir`, + init: []*testDecl{boromir}, + want: []*testDecl{boromir}, + }, + { + name: `Aragorn`, + init: []*testDecl{aragorn}, + want: []*testDecl{aragorn, boromir}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + for _, decl := range fellowship { + decl.Dce().alive = false + } + for _, decl := range tt.init { + decl.Dce().SetAsAlive() + } + + s := &Selector[*testDecl]{} + for _, decl := range fellowship { + s.Include(decl, false) + } + + selected := s.AliveDecls() + for _, decl := range tt.want { + if _, ok := selected[decl]; !ok { + t.Errorf(`expected %q to be alive`, decl.obj.String()) + } + delete(selected, decl) + } + for decl := range selected { + t.Errorf(`expected %q to be dead`, decl.obj.String()) + } + }) + } +} + +func Test_Selector_SpecificMethods(t *testing.T) { + objects := parseObjects(t, + `package pratchett + + type rincewind struct{} + func (r rincewind) Run() {} + func (r rincewind) hide() {} + + type Vimes struct{} + func (v Vimes) Run() {} + func (v Vimes) Read() {} + + func Vetinari() {}`) + + var ( + // Objects are in read order so pick the objects we want for this test + // while skipping over `r rincewind` and `v Vimes`. + rincewind = quickTestDecl(objects[0]) + rincewindRun = quickTestDecl(objects[2]) + rincewindHide = quickTestDecl(objects[4]) + vimes = quickTestDecl(objects[5]) + vimesRun = quickTestDecl(objects[7]) + vimesRead = quickTestDecl(objects[9]) + vetinari = quickTestDecl(objects[10]) + ) + allDecls := []*testDecl{rincewind, rincewindRun, rincewindHide, vimes, vimesRun, vimesRead, vetinari} + + c := Collector{} + c.CollectDCEDeps(rincewindRun, func() { + c.DeclareDCEDep(rincewind.obj) + }) + c.CollectDCEDeps(rincewindHide, func() { + c.DeclareDCEDep(rincewind.obj) + }) + c.CollectDCEDeps(vimesRun, func() { + c.DeclareDCEDep(vimes.obj) + }) + c.CollectDCEDeps(vimesRead, func() { + c.DeclareDCEDep(vimes.obj) + }) + vetinari.Dce().SetAsAlive() + + tests := []struct { + name string + deps []*testDecl // which decls are vetinari dependent on + want []*testDecl // which decls should be determined as alive + }{ + { + name: `no deps`, + deps: []*testDecl{}, + want: []*testDecl{vetinari}, + }, + { + name: `structs`, + deps: []*testDecl{rincewind, vimes}, + // rincewindHide is not included because it is not exported and not used. + want: []*testDecl{rincewind, rincewindRun, vimes, vimesRun, vimesRead, vetinari}, + }, + { + name: `exposed method`, + deps: []*testDecl{rincewind, rincewindRun}, + want: []*testDecl{rincewind, rincewindRun, vetinari}, + }, + { + name: `unexposed method`, + deps: []*testDecl{rincewind, rincewindHide}, + want: []*testDecl{rincewind, rincewindRun, rincewindHide, vetinari}, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + c.CollectDCEDeps(vetinari, func() { + for _, decl := range tt.deps { + c.DeclareDCEDep(decl.obj) + } + }) + + s := Selector[*testDecl]{} + for _, decl := range allDecls { + s.Include(decl, false) + } + selected := s.AliveDecls() + for _, decl := range tt.want { + if _, ok := selected[decl]; !ok { + t.Errorf(`expected %q to be alive`, decl.obj.String()) + } + delete(selected, decl) + } + for decl := range selected { + t.Errorf(`expected %q to be dead`, decl.obj.String()) + } + }) + } +} + +type testDecl struct { + obj types.Object // should match the object used in Dce.SetName when set + dce Info +} + +func (d *testDecl) Dce() *Info { + return &d.dce +} + +func testPackage(name string) *types.Package { + return types.NewPackage(`path/to/`+name, name) +} + +func quickTestDecl(o types.Object) *testDecl { + d := &testDecl{obj: o} + d.Dce().SetName(o) + return d +} + +func quickVar(pkg *types.Package, name string) *types.Var { + return types.NewVar(token.NoPos, pkg, name, types.Typ[types.Int]) +} + +func parseObject(t *testing.T, name, source string) types.Object { + t.Helper() + objects := parseObjects(t, source) + for _, obj := range objects { + if obj.Name() == name { + return obj + } + } + t.Fatalf(`object %q not found`, name) + return nil +} + +func parseObjects(t *testing.T, source string) []types.Object { + t.Helper() + info := &types.Info{ + Defs: map[*ast.Ident]types.Object{}, + } + parseInfo(t, source, info) + objects := make([]types.Object, 0, len(info.Defs)) + for _, obj := range info.Defs { + if obj != nil { + objects = append(objects, obj) + } + } + sort.Slice(objects, func(i, j int) bool { + return objects[i].Pos() < objects[j].Pos() + }) + return objects +} + +func parseInfo(t *testing.T, source string, info *types.Info) *types.Package { + t.Helper() + fset := token.NewFileSet() + f, err := parser.ParseFile(fset, `test.go`, source, 0) + if err != nil { + t.Fatal(`parsing source:`, err) + } + + conf := types.Config{ + Importer: importer.Default(), + DisableUnusedImportCheck: true, + } + pkg, err := conf.Check(f.Name.Name, fset, []*ast.File{f}, info) + if err != nil { + t.Fatal(`type checking:`, err) + } + return pkg +} + +func capturePanic(t *testing.T, f func()) (err error) { + t.Helper() + defer func() { + t.Helper() + if r := recover(); r != nil { + if err2, ok := r.(error); ok { + err = err2 + return + } + t.Errorf(`expected an error to be panicked but got (%[1]T) %[1]#v`, r) + return + } + t.Error(`expected a panic but got none`) + }() + + f() + return nil +} + +func errorMatches(t *testing.T, err error, wantPattern string) { + t.Helper() + re := regexp.MustCompile(wantPattern) + if got := fmt.Sprint(err); !re.MatchString(got) { + t.Errorf(`expected error %q to match %q`, got, re.String()) + } +} + +func depCount(t *testing.T, decl *testDecl, want int) { + t.Helper() + if got := len(decl.Dce().deps); got != want { + t.Errorf(`expected %d deps but got %d`, want, got) + } +} + +func equal[T comparable](t *testing.T, got, want T) { + t.Helper() + if got != want { + t.Errorf(`expected %#v but got %#v`, want, got) + } +} diff --git a/compiler/internal/dce/info.go b/compiler/internal/dce/info.go new file mode 100644 index 000000000..d5993a659 --- /dev/null +++ b/compiler/internal/dce/info.go @@ -0,0 +1,108 @@ +package dce + +import ( + "fmt" + "go/types" + "sort" + "strings" + + "github.com/gopherjs/gopherjs/compiler/typesutil" +) + +// Info contains information used by the dead-code elimination (DCE) logic to +// determine whether a declaration is alive or dead. +type Info struct { + + // alive indicates if the declaration is marked as alive + // 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 string + + // The second part of the identified used by dead-code elimination for methods. + // Empty for other types of symbols. + methodFilter string + + // List of fully qualified (including package path) DCE symbol identifiers the + // symbol depends on for dead code elimination purposes. + deps []string +} + +// String gets a human-readable representation of the DCE info. +func (d *Info) String() string { + tags := `` + if d.alive { + tags += `[alive] ` + } + if d.unnamed() { + tags += `[unnamed] ` + } + fullName := d.importPath + `.` + d.objectFilter + if len(d.methodFilter) > 0 { + fullName += `.` + d.methodFilter + } + return tags + fullName + ` -> [` + strings.Join(d.deps, `, `) + `]` +} + +// unnamed returns true if SetName has not been called for this declaration. +// This indicates that the DCE is not initialized. +func (d *Info) unnamed() bool { + return d.objectFilter == `` && d.methodFilter == `` +} + +// isAlive returns true if the declaration is marked as alive. +// +// Returns true if SetAsAlive was called on this declaration or +// if SetName was not called meaning the DCE is not initialized. +func (d *Info) isAlive() bool { + return d.alive || d.unnamed() +} + +// SetAsAlive marks the declaration as alive, meaning it will not be eliminated. +// +// This should be called by an entry point (like main() or init() functions) +// or a variable initializer which has a side effect, consider it live. +func (d *Info) SetAsAlive() { + d.alive = true +} + +// SetName sets the name used by DCE to represent the declaration +// this DCE info is attached to. +func (d *Info) SetName(o types.Object) { + if !d.unnamed() { + 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() + } +} + +// setDeps sets the 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 += "~" + } + deps = append(deps, qualifiedName) + } + sort.Strings(deps) + d.deps = deps +} diff --git a/compiler/internal/dce/selector.go b/compiler/internal/dce/selector.go new file mode 100644 index 000000000..4eea572e0 --- /dev/null +++ b/compiler/internal/dce/selector.go @@ -0,0 +1,93 @@ +package dce + +// DeclConstraint is type constraint for any code declaration that has +// dead-code elimination (DCE) information attached to it and will be +// used in a set. +type DeclConstraint interface { + Decl + comparable +} + +// Selector gathers all declarations that are still alive after dead-code elimination. +type Selector[D DeclConstraint] struct { + byFilter map[string][]*declInfo[D] + + // A queue of live decls to find other live decls. + pendingDecls []D +} + +type declInfo[D DeclConstraint] struct { + decl D + objectFilter string + methodFilter string +} + +// Include will add a new declaration to be checked as alive or not. +func (s *Selector[D]) Include(decl D, implementsLink bool) { + if s.byFilter == nil { + s.byFilter = make(map[string][]*declInfo[D]) + } + + dce := decl.Dce() + + if dce.isAlive() { + s.pendingDecls = append(s.pendingDecls, decl) + return + } + + if implementsLink { + s.pendingDecls = append(s.pendingDecls, decl) + } + + info := &declInfo[D]{decl: decl} + + if dce.objectFilter != `` { + info.objectFilter = dce.importPath + `.` + dce.objectFilter + s.byFilter[info.objectFilter] = append(s.byFilter[info.objectFilter], info) + } + + if dce.methodFilter != `` { + info.methodFilter = dce.importPath + `.` + dce.methodFilter + s.byFilter[info.methodFilter] = append(s.byFilter[info.methodFilter], info) + } +} + +func (s *Selector[D]) popPending() D { + max := len(s.pendingDecls) - 1 + d := s.pendingDecls[max] + s.pendingDecls = s.pendingDecls[:max] + return d +} + +// AliveDecls returns a set of declarations that are still alive +// after dead-code elimination. +// This should only be called once all declarations have been included. +func (s *Selector[D]) AliveDecls() map[D]struct{} { + dceSelection := make(map[D]struct{}) // Known live decls. + for len(s.pendingDecls) != 0 { + d := s.popPending() + dce := d.Dce() + + 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 dce.deps { + if infos, ok := s.byFilter[dep]; ok { + delete(s.byFilter, dep) + for _, info := range infos { + if info.objectFilter == dep { + info.objectFilter = `` + } + if info.methodFilter == dep { + info.methodFilter = `` + } + if info.objectFilter == `` && info.methodFilter == `` { + s.pendingDecls = append(s.pendingDecls, info.decl) + } + } + } + } + } + return dceSelection +} diff --git a/compiler/natives/src/crypto/ecdh/nist.go b/compiler/natives/src/crypto/ecdh/nist.go deleted file mode 100644 index ecaa84d76..000000000 --- a/compiler/natives/src/crypto/ecdh/nist.go +++ /dev/null @@ -1,58 +0,0 @@ -//go:build js -// +build js - -package ecdh - -import ( - "crypto/internal/nistec" - "io" -) - -//gopherjs:purge for go1.20 without generics -type nistPoint[T any] interface{} - -// temporarily replacement of `nistCurve[Point nistPoint[Point]]` for go1.20 without generics. -type nistCurve struct { - name string - newPoint func() nistec.WrappedPoint - scalarOrder []byte -} - -//gopherjs:override-signature -func (c *nistCurve) String() string - -//gopherjs:override-signature -func (c *nistCurve) GenerateKey(rand io.Reader) (*PrivateKey, error) - -//gopherjs:override-signature -func (c *nistCurve) NewPrivateKey(key []byte) (*PrivateKey, error) - -//gopherjs:override-signature -func (c *nistCurve) privateKeyToPublicKey(key *PrivateKey) *PublicKey - -//gopherjs:override-signature -func (c *nistCurve) NewPublicKey(key []byte) (*PublicKey, error) - -//gopherjs:override-signature -func (c *nistCurve) ecdh(local *PrivateKey, remote *PublicKey) ([]byte, error) - -// temporarily replacement for go1.20 without generics. -var p256 = &nistCurve{ - name: "P-256", - newPoint: nistec.NewP256WrappedPoint, - scalarOrder: p256Order, -} - -// temporarily replacement for go1.20 without generics. -var p384 = &nistCurve{ - name: "P-384", - newPoint: nistec.NewP384WrappedPoint, - scalarOrder: p384Order, -} - -// temporarily replacement for go1.20 without generics. -var p521 = &nistCurve{ - name: "P-521", - newPoint: nistec.NewP521WrappedPoint, - scalarOrder: p521Order, -} diff --git a/compiler/natives/src/crypto/ecdsa/ecdsa.go b/compiler/natives/src/crypto/ecdsa/ecdsa.go deleted file mode 100644 index cf3da4ec8..000000000 --- a/compiler/natives/src/crypto/ecdsa/ecdsa.go +++ /dev/null @@ -1,98 +0,0 @@ -//go:build js -// +build js - -package ecdsa - -import ( - "crypto/elliptic" - "crypto/internal/bigmod" - "crypto/internal/nistec" - "io" - "math/big" -) - -//gopherjs:override-signature -func generateNISTEC(c *nistCurve, rand io.Reader) (*PrivateKey, error) - -//gopherjs:override-signature -func randomPoint(c *nistCurve, rand io.Reader) (k *bigmod.Nat, p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func signNISTEC(c *nistCurve, priv *PrivateKey, csprng io.Reader, hash []byte) (sig []byte, err error) - -//gopherjs:override-signature -func inverse(c *nistCurve, kInv, k *bigmod.Nat) - -//gopherjs:override-signature -func hashToNat(c *nistCurve, e *bigmod.Nat, hash []byte) - -//gopherjs:override-signature -func verifyNISTEC(c *nistCurve, pub *PublicKey, hash, sig []byte) bool - -//gopherjs:purge for go1.20 without generics -type nistPoint[T any] interface{} - -// temporarily replacement of `nistCurve[Point nistPoint[Point]]` for go1.20 without generics. -type nistCurve struct { - newPoint func() nistec.WrappedPoint - curve elliptic.Curve - N *bigmod.Modulus - nMinus2 []byte -} - -//gopherjs:override-signature -func (curve *nistCurve) pointFromAffine(x, y *big.Int) (p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func (curve *nistCurve) pointToAffine(p nistec.WrappedPoint) (x, y *big.Int, err error) - -var _p224 *nistCurve - -func p224() *nistCurve { - p224Once.Do(func() { - _p224 = &nistCurve{ - newPoint: nistec.NewP224WrappedPoint, - } - precomputeParams(_p224, elliptic.P224()) - }) - return _p224 -} - -var _p256 *nistCurve - -func p256() *nistCurve { - p256Once.Do(func() { - _p256 = &nistCurve{ - newPoint: nistec.NewP256WrappedPoint, - } - precomputeParams(_p256, elliptic.P256()) - }) - return _p256 -} - -var _p384 *nistCurve - -func p384() *nistCurve { - p384Once.Do(func() { - _p384 = &nistCurve{ - newPoint: nistec.NewP384WrappedPoint, - } - precomputeParams(_p384, elliptic.P384()) - }) - return _p384 -} - -var _p521 *nistCurve - -func p521() *nistCurve { - p521Once.Do(func() { - _p521 = &nistCurve{ - newPoint: nistec.NewP521WrappedPoint, - } - precomputeParams(_p521, elliptic.P521()) - }) - return _p521 -} - -//gopherjs:override-signature -func precomputeParams(c *nistCurve, curve elliptic.Curve) diff --git a/compiler/natives/src/crypto/ecdsa/ecdsa_test.go b/compiler/natives/src/crypto/ecdsa/ecdsa_test.go deleted file mode 100644 index efb4d7b5e..000000000 --- a/compiler/natives/src/crypto/ecdsa/ecdsa_test.go +++ /dev/null @@ -1,12 +0,0 @@ -//go:build js -// +build js - -package ecdsa - -import "testing" - -//gopherjs:override-signature -func testRandomPoint(t *testing.T, c *nistCurve) - -//gopherjs:override-signature -func testHashToNat(t *testing.T, c *nistCurve) diff --git a/compiler/natives/src/crypto/elliptic/nistec.go b/compiler/natives/src/crypto/elliptic/nistec.go deleted file mode 100644 index 326c602d5..000000000 --- a/compiler/natives/src/crypto/elliptic/nistec.go +++ /dev/null @@ -1,81 +0,0 @@ -//go:build js -// +build js - -package elliptic - -import ( - "crypto/internal/nistec" - "math/big" -) - -// nistPoint uses generics so must be removed for generic-less GopherJS. -// All the following code changes in this file are to make p224, p256, -// p521, and p384 still function correctly without this generic struct. -// -//gopherjs:purge for go1.19 without generics -type nistPoint[T any] interface{} - -// nistCurve replaces the generics with a version using the wrappedPoint -// interface, then update all the method signatures to also use wrappedPoint. -type nistCurve struct { - newPoint func() nistec.WrappedPoint - params *CurveParams -} - -//gopherjs:override-signature -func (curve *nistCurve) Params() *CurveParams - -//gopherjs:override-signature -func (curve *nistCurve) IsOnCurve(x, y *big.Int) bool - -//gopherjs:override-signature -func (curve *nistCurve) pointFromAffine(x, y *big.Int) (p nistec.WrappedPoint, err error) - -//gopherjs:override-signature -func (curve *nistCurve) pointToAffine(p nistec.WrappedPoint) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Add(x1, y1, x2, y2 *big.Int) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Double(x1, y1 *big.Int) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) normalizeScalar(scalar []byte) []byte - -//gopherjs:override-signature -func (curve *nistCurve) ScalarMult(Bx, By *big.Int, scalar []byte) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) ScalarBaseMult(scalar []byte) (*big.Int, *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) CombinedMult(Px, Py *big.Int, s1, s2 []byte) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) Unmarshal(data []byte) (x, y *big.Int) - -//gopherjs:override-signature -func (curve *nistCurve) UnmarshalCompressed(data []byte) (x, y *big.Int) - -var p224 = &nistCurve{ - newPoint: nistec.NewP224WrappedPoint, -} - -type p256Curve struct { - nistCurve -} - -var p256 = &p256Curve{ - nistCurve: nistCurve{ - newPoint: nistec.NewP256WrappedPoint, - }, -} - -var p521 = &nistCurve{ - newPoint: nistec.NewP521WrappedPoint, -} - -var p384 = &nistCurve{ - newPoint: nistec.NewP384WrappedPoint, -} diff --git a/compiler/natives/src/crypto/internal/nistec/nistec_test.go b/compiler/natives/src/crypto/internal/nistec/nistec_test.go index ea91d7ed2..29af23c2f 100644 --- a/compiler/natives/src/crypto/internal/nistec/nistec_test.go +++ b/compiler/natives/src/crypto/internal/nistec/nistec_test.go @@ -3,87 +3,8 @@ package nistec_test -import ( - "crypto/elliptic" - "crypto/internal/nistec" - "testing" -) +import "testing" func TestAllocations(t *testing.T) { t.Skip("testing.AllocsPerRun not supported in GopherJS") } - -//gopherjs:purge -type nistPoint[T any] interface{} - -func TestEquivalents(t *testing.T) { - t.Run("P224", func(t *testing.T) { - testEquivalents(t, nistec.NewP224WrappedPoint, elliptic.P224()) - }) - t.Run("P256", func(t *testing.T) { - testEquivalents(t, nistec.NewP256WrappedPoint, elliptic.P256()) - }) - t.Run("P384", func(t *testing.T) { - testEquivalents(t, nistec.NewP384WrappedPoint, elliptic.P384()) - }) - t.Run("P521", func(t *testing.T) { - testEquivalents(t, nistec.NewP521WrappedPoint, elliptic.P521()) - }) -} - -//gopherjs:override-signature -func testEquivalents(t *testing.T, newPoint func() nistec.WrappedPoint, c elliptic.Curve) - -func TestScalarMult(t *testing.T) { - t.Run("P224", func(t *testing.T) { - testScalarMult(t, nistec.NewP224WrappedPoint, elliptic.P224()) - }) - t.Run("P256", func(t *testing.T) { - testScalarMult(t, nistec.NewP256WrappedPoint, elliptic.P256()) - }) - t.Run("P384", func(t *testing.T) { - testScalarMult(t, nistec.NewP384WrappedPoint, elliptic.P384()) - }) - t.Run("P521", func(t *testing.T) { - testScalarMult(t, nistec.NewP521WrappedPoint, elliptic.P521()) - }) -} - -//gopherjs:override-signature -func testScalarMult(t *testing.T, newPoint func() nistec.WrappedPoint, c elliptic.Curve) - -func BenchmarkScalarMult(b *testing.B) { - b.Run("P224", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP224WrappedPoint().SetGenerator(), 28) - }) - b.Run("P256", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP256WrappedPoint().SetGenerator(), 32) - }) - b.Run("P384", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP384WrappedPoint().SetGenerator(), 48) - }) - b.Run("P521", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP521WrappedPoint().SetGenerator(), 66) - }) -} - -//gopherjs:override-signature -func benchmarkScalarMult(b *testing.B, p nistec.WrappedPoint, scalarSize int) - -func BenchmarkScalarBaseMult(b *testing.B) { - b.Run("P224", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP224WrappedPoint().SetGenerator(), 28) - }) - b.Run("P256", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP256WrappedPoint().SetGenerator(), 32) - }) - b.Run("P384", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP384WrappedPoint().SetGenerator(), 48) - }) - b.Run("P521", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP521WrappedPoint().SetGenerator(), 66) - }) -} - -//gopherjs:override-signature -func benchmarkScalarBaseMult(b *testing.B, p nistec.WrappedPoint, scalarSize int) diff --git a/compiler/natives/src/crypto/internal/nistec/wrapper.go b/compiler/natives/src/crypto/internal/nistec/wrapper.go deleted file mode 100644 index afa2b7049..000000000 --- a/compiler/natives/src/crypto/internal/nistec/wrapper.go +++ /dev/null @@ -1,204 +0,0 @@ -//go:build js -// +build js - -package nistec - -// temporarily replacement of `nistPoint[T any]` for go1.20 without generics. -type WrappedPoint interface { - SetGenerator() WrappedPoint - Bytes() []byte - BytesX() ([]byte, error) - SetBytes(b []byte) (WrappedPoint, error) - Add(w1, w2 WrappedPoint) WrappedPoint - Double(w1 WrappedPoint) WrappedPoint - ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) - ScalarBaseMult(scalar []byte) (WrappedPoint, error) -} - -type p224Wrapper struct { - point *P224Point -} - -func wrapP224(point *P224Point) WrappedPoint { - return p224Wrapper{point: point} -} - -func NewP224WrappedPoint() WrappedPoint { - return wrapP224(NewP224Point()) -} - -func (w p224Wrapper) SetGenerator() WrappedPoint { - return wrapP224(w.point.SetGenerator()) -} - -func (w p224Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p224Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p224Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP224(p), err -} - -func (w p224Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP224(w.point.Add(w1.(p224Wrapper).point, w2.(p224Wrapper).point)) -} - -func (w p224Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP224(w.point.Double(w1.(p224Wrapper).point)) -} - -func (w p224Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p224Wrapper).point, scalar) - return wrapP224(p), err -} - -func (w p224Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP224(p), err -} - -type p256Wrapper struct { - point *P256Point -} - -func wrapP256(point *P256Point) WrappedPoint { - return p256Wrapper{point: point} -} - -func NewP256WrappedPoint() WrappedPoint { - return wrapP256(NewP256Point()) -} - -func (w p256Wrapper) SetGenerator() WrappedPoint { - return wrapP256(w.point.SetGenerator()) -} - -func (w p256Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p256Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p256Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP256(p), err -} - -func (w p256Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP256(w.point.Add(w1.(p256Wrapper).point, w2.(p256Wrapper).point)) -} - -func (w p256Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP256(w.point.Double(w1.(p256Wrapper).point)) -} - -func (w p256Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p256Wrapper).point, scalar) - return wrapP256(p), err -} - -func (w p256Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP256(p), err -} - -type p521Wrapper struct { - point *P521Point -} - -func wrapP521(point *P521Point) WrappedPoint { - return p521Wrapper{point: point} -} - -func NewP521WrappedPoint() WrappedPoint { - return wrapP521(NewP521Point()) -} - -func (w p521Wrapper) SetGenerator() WrappedPoint { - return wrapP521(w.point.SetGenerator()) -} - -func (w p521Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p521Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p521Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP521(p), err -} - -func (w p521Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP521(w.point.Add(w1.(p521Wrapper).point, w2.(p521Wrapper).point)) -} - -func (w p521Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP521(w.point.Double(w1.(p521Wrapper).point)) -} - -func (w p521Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p521Wrapper).point, scalar) - return wrapP521(p), err -} - -func (w p521Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP521(p), err -} - -type p384Wrapper struct { - point *P384Point -} - -func wrapP384(point *P384Point) WrappedPoint { - return p384Wrapper{point: point} -} - -func NewP384WrappedPoint() WrappedPoint { - return wrapP384(NewP384Point()) -} - -func (w p384Wrapper) SetGenerator() WrappedPoint { - return wrapP384(w.point.SetGenerator()) -} - -func (w p384Wrapper) Bytes() []byte { - return w.point.Bytes() -} - -func (w p384Wrapper) BytesX() ([]byte, error) { - return w.point.BytesX() -} - -func (w p384Wrapper) SetBytes(b []byte) (WrappedPoint, error) { - p, err := w.point.SetBytes(b) - return wrapP384(p), err -} - -func (w p384Wrapper) Add(w1, w2 WrappedPoint) WrappedPoint { - return wrapP384(w.point.Add(w1.(p384Wrapper).point, w2.(p384Wrapper).point)) -} - -func (w p384Wrapper) Double(w1 WrappedPoint) WrappedPoint { - return wrapP384(w.point.Double(w1.(p384Wrapper).point)) -} - -func (w p384Wrapper) ScalarMult(w1 WrappedPoint, scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarMult(w1.(p384Wrapper).point, scalar) - return wrapP384(p), err -} - -func (w p384Wrapper) ScalarBaseMult(scalar []byte) (WrappedPoint, error) { - p, err := w.point.ScalarBaseMult(scalar) - return wrapP384(p), err -} diff --git a/compiler/natives/src/encoding/gob/gob.go b/compiler/natives/src/encoding/gob/gob.go index 244f72ed7..1268bcff8 100644 --- a/compiler/natives/src/encoding/gob/gob.go +++ b/compiler/natives/src/encoding/gob/gob.go @@ -3,16 +3,13 @@ package gob -import ( - "reflect" - "sync" -) +import "sync" type typeInfo struct { id typeId encInit sync.Mutex - // temporarily replacement of atomic.Pointer[encEngine] for go1.20 without generics. + // replacing a `atomic.Pointer[encEngine]` since GopherJS does not fully support generics for go1.20 yet. encoder atomicEncEnginePointer wire *wireType } @@ -23,17 +20,3 @@ type atomicEncEnginePointer struct { func (x *atomicEncEnginePointer) Load() *encEngine { return x.v } func (x *atomicEncEnginePointer) Store(val *encEngine) { x.v = val } - -// temporarily replacement of growSlice[E any] for go1.20 without generics. -func growSlice(v reflect.Value, ps any, length int) { - vps := reflect.ValueOf(ps) // *[]E - vs := vps.Elem() // []E - zero := reflect.Zero(vs.Type().Elem()) - vs.Set(reflect.Append(vs, zero)) - cp := vs.Cap() - if cp > length { - cp = length - } - vs.Set(vs.Slice(0, cp)) - v.Set(vs) -} diff --git a/compiler/natives/src/go/doc/doc_test.go b/compiler/natives/src/go/doc/doc_test.go deleted file mode 100644 index 4d35e880c..000000000 --- a/compiler/natives/src/go/doc/doc_test.go +++ /dev/null @@ -1,37 +0,0 @@ -//go:build js - -package doc - -import ( - "fmt" - "testing" -) - -func compareSlices(t *testing.T, name string, got, want interface{}, compareElem interface{}) { - // TODO(nevkontakte): Remove this override after generics are supported. - // https://github.com/gopherjs/gopherjs/issues/1013. - switch got.(type) { - case []*Func: - got := got.([]*Func) - want := want.([]*Func) - compareElem := compareElem.(func(t *testing.T, msg string, got, want *Func)) - if len(got) != len(want) { - t.Errorf("%s: got %d, want %d", name, len(got), len(want)) - } - for i := 0; i < len(got) && i < len(want); i++ { - compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i]) - } - case []*Type: - got := got.([]*Type) - want := want.([]*Type) - compareElem := compareElem.(func(t *testing.T, msg string, got, want *Type)) - if len(got) != len(want) { - t.Errorf("%s: got %d, want %d", name, len(got), len(want)) - } - for i := 0; i < len(got) && i < len(want); i++ { - compareElem(t, fmt.Sprintf("%s[%d]", name, i), got[i], want[i]) - } - default: - t.Errorf("unexpected argument type %T", got) - } -} diff --git a/compiler/natives/src/go/token/position.go b/compiler/natives/src/go/token/position.go deleted file mode 100644 index 436c48380..000000000 --- a/compiler/natives/src/go/token/position.go +++ /dev/null @@ -1,30 +0,0 @@ -//go:build js -// +build js - -package token - -import "sync" - -type FileSet struct { - mutex sync.RWMutex - base int - files []*File - - // replaced atomic.Pointer[File] for go1.19 without generics. - last atomicFilePointer -} - -type atomicFilePointer struct { - v *File -} - -func (x *atomicFilePointer) Load() *File { return x.v } -func (x *atomicFilePointer) Store(val *File) { x.v = val } - -func (x *atomicFilePointer) CompareAndSwap(old, new *File) bool { - if x.v == old { - x.v = new - return true - } - return false -} diff --git a/compiler/natives/src/internal/godebug/godebug.go b/compiler/natives/src/internal/godebug/godebug.go index e43006c3f..07ca6630d 100644 --- a/compiler/natives/src/internal/godebug/godebug.go +++ b/compiler/natives/src/internal/godebug/godebug.go @@ -4,84 +4,8 @@ package godebug import ( - "sync" _ "unsafe" // go:linkname ) -type Setting struct { - name string - once sync.Once - - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - value *atomicStringPointer -} - -type atomicStringPointer struct { - v *string -} - -func (x *atomicStringPointer) Load() *string { return x.v } -func (x *atomicStringPointer) Store(val *string) { x.v = val } - -func (s *Setting) Value() string { - s.once.Do(func() { - v, ok := cache.Load(s.name) - if !ok { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - p := new(atomicStringPointer) - p.Store(&empty) - v, _ = cache.LoadOrStore(s.name, p) - } - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - s.value = v.(*atomicStringPointer) - }) - return *s.value.Load() -} - //go:linkname setUpdate runtime.godebug_setUpdate func setUpdate(update func(def, env string)) - -func update(def, env string) { - updateMu.Lock() - defer updateMu.Unlock() - - did := make(map[string]bool) - parse(did, env) - parse(did, def) - - cache.Range(func(name, v any) bool { - if !did[name.(string)] { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - v.(*atomicStringPointer).Store(&empty) - } - return true - }) -} - -func parse(did map[string]bool, s string) { - end := len(s) - eq := -1 - for i := end - 1; i >= -1; i-- { - if i == -1 || s[i] == ',' { - if eq >= 0 { - name, value := s[i+1:eq], s[eq+1:end] - if !did[name] { - did[name] = true - v, ok := cache.Load(name) - if !ok { - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - p := new(atomicStringPointer) - p.Store(&empty) - v, _ = cache.LoadOrStore(name, p) - } - // temporarily replacement of atomic.Pointer[string] for go1.20 without generics. - v.(*atomicStringPointer).Store(&value) - } - } - eq = -1 - end = i - } else if s[i] == '=' { - eq = i - } - } -} diff --git a/compiler/natives/src/internal/reflectlite/all_test.go b/compiler/natives/src/internal/reflectlite/all_test.go index 4445189a0..977438e4e 100644 --- a/compiler/natives/src/internal/reflectlite/all_test.go +++ b/compiler/natives/src/internal/reflectlite/all_test.go @@ -21,27 +21,3 @@ func TestTypes(t *testing.T) { func TestNameBytesAreAligned(t *testing.T) { t.Skip("TestNameBytesAreAligned") } - -// `A` is used with `B[T any]` and is otherwise not needed. -// -//gopherjs:purge for go1.19 without generics -type ( - A struct{} - B[T any] struct{} -) - -// removing the name tests using `B[T any]` for go1.19 without generics -var nameTests = []nameTest{ - {(*int32)(nil), "int32"}, - {(*D1)(nil), "D1"}, - {(*[]D1)(nil), ""}, - {(*chan D1)(nil), ""}, - {(*func() D1)(nil), ""}, - {(*<-chan D1)(nil), ""}, - {(*chan<- D1)(nil), ""}, - {(*any)(nil), ""}, - {(*interface { - F() - })(nil), ""}, - {(*TheNameOfThisTypeIsExactly255BytesLongSoWhenTheCompilerPrependsTheReflectTestPackageNameAndExtraStarTheLinkerRuntimeAndReflectPackagesWillHaveToCorrectlyDecodeTheSecondLengthByte0123456789_0123456789_0123456789_0123456789_0123456789_012345678)(nil), "TheNameOfThisTypeIsExactly255BytesLongSoWhenTheCompilerPrependsTheReflectTestPackageNameAndExtraStarTheLinkerRuntimeAndReflectPackagesWillHaveToCorrectlyDecodeTheSecondLengthByte0123456789_0123456789_0123456789_0123456789_0123456789_012345678"}, -} diff --git a/compiler/natives/src/net/http/http.go b/compiler/natives/src/net/http/http.go index f82c0363c..56bc2f425 100644 --- a/compiler/natives/src/net/http/http.go +++ b/compiler/natives/src/net/http/http.go @@ -131,7 +131,7 @@ type conn struct { bufw *bufio.Writer lastMethod string - // temporarily replacement of `atomic.Pointer[response]` for go1.20 without generics. + // replacing a `atomic.Pointer[response]` since GopherJS does not fully support generics for go1.20 yet. curReq atomicResponsePointer curState atomic.Uint64 diff --git a/compiler/natives/src/net/netip/fuzz_test.go b/compiler/natives/src/net/netip/fuzz_test.go deleted file mode 100644 index f7359c5bb..000000000 --- a/compiler/natives/src/net/netip/fuzz_test.go +++ /dev/null @@ -1,11 +0,0 @@ -//go:build js -// +build js - -package netip_test - -import "testing" - -func checkStringParseRoundTrip(t *testing.T, x interface{}, parse interface{}) { - // TODO(nevkontakte): This function requires generics to function. - // Re-enable after https://github.com/gopherjs/gopherjs/issues/1013 is resolved. -} diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go index ce290ade6..0ee16e131 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -1849,26 +1849,28 @@ func valueMethodName() string { var pc [5]uintptr n := runtime.Callers(1, pc[:]) frames := runtime.CallersFrames(pc[:n]) + valueTyp := TypeOf(Value{}) var frame runtime.Frame for more := true; more; { frame, more = frames.Next() name := frame.Function - // Function name extracted from the call stack can be different from // vanilla Go, so is not prefixed by "reflect.Value." as needed by the original. // See https://cs.opensource.google/go/go/+/refs/tags/go1.19.13:src/reflect/value.go;l=173-191 - // Here we try to fix stuff like "Object.$packages.reflect.Q.ptr.SetIterKey" - // into "reflect.Value.SetIterKey". // This workaround may become obsolete after // https://github.com/gopherjs/gopherjs/issues/1085 is resolved. - const prefix = `Object.$packages.reflect.` - if stringsHasPrefix(name, prefix) { - if idx := stringsLastIndex(name, '.'); idx >= 0 { - methodName := name[idx+1:] - if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' { - return `reflect.Value.` + methodName - } + methodName := name + if idx := stringsLastIndex(name, '.'); idx >= 0 { + methodName = name[idx+1:] + } + + // Since function name in the call stack doesn't contain receiver name, + // we are looking for the first exported function name that matches a + // known Value method. + if _, ok := valueTyp.MethodByName(methodName); ok { + if len(methodName) > 0 && 'A' <= methodName[0] && methodName[0] <= 'Z' { + return `reflect.Value.` + methodName } } } diff --git a/compiler/natives/src/reflect/reflect_test.go b/compiler/natives/src/reflect/reflect_test.go index 79bbe5385..d3a7289fd 100644 --- a/compiler/natives/src/reflect/reflect_test.go +++ b/compiler/natives/src/reflect/reflect_test.go @@ -285,16 +285,26 @@ func TestMethodCallValueCodePtr(t *testing.T) { t.Skip("methodValueCallCodePtr() is not applicable in GopherJS") } -//gopherjs:purge for go1.19 without generics -type ( - A struct{} - B[T any] struct{} -) +func TestStructOfTooLarge(t *testing.T) { + t.Skip("This test is dependent on field alignment to determine if a struct size would exceed virtual address space.") +} -func TestIssue50208(t *testing.T) { - t.Skip("This test required generics, which are not yet supported: https://github.com/gopherjs/gopherjs/issues/1013") +func TestSetLenCap(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") } -func TestStructOfTooLarge(t *testing.T) { - t.Skip("This test is dependent on field alignment to determine if a struct size would exceed virtual address space.") +func TestSetPanic(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} + +func TestCallPanic(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} + +func TestValuePanic(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") +} + +func TestSetIter(t *testing.T) { + t.Skip("Test depends on call stack function names: https://github.com/gopherjs/gopherjs/issues/1085") } diff --git a/compiler/natives/src/sync/atomic/atomic.go b/compiler/natives/src/sync/atomic/atomic.go index d993f3b80..ebc98e910 100644 --- a/compiler/natives/src/sync/atomic/atomic.go +++ b/compiler/natives/src/sync/atomic/atomic.go @@ -220,20 +220,3 @@ func sameType(x, y interface{}) bool { // existing and differing for different types. return js.InternalObject(x).Get("constructor") == js.InternalObject(y).Get("constructor") } - -// Override pointer so that the type check in the source code is satisfied -// but remove the fields and methods for go1.20 without generics. -// See https://cs.opensource.google/go/go/+/refs/tags/go1.20.14:src/sync/atomic/type.go;l=40 -type Pointer[T any] struct{} - -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) Load() *T - -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) Store(val *T) - -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) Swap(new *T) (old *T) - -//gopherjs:purge for go1.20 without generics -func (x *Pointer[T]) CompareAndSwap(old, new *T) (swapped bool) diff --git a/compiler/natives/src/sync/atomic/atomic_test.go b/compiler/natives/src/sync/atomic/atomic_test.go index e1ec6086c..8496cecb6 100644 --- a/compiler/natives/src/sync/atomic/atomic_test.go +++ b/compiler/natives/src/sync/atomic/atomic_test.go @@ -5,50 +5,12 @@ package atomic_test import ( "testing" - "unsafe" ) -//gopherjs:purge for go1.19 without generics -func testPointers() []unsafe.Pointer {} - -func TestSwapPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - func TestSwapPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestCompareAndSwapPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestCompareAndSwapPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestLoadPointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestLoadPointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestStorePointer(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -func TestStorePointerMethod(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") + t.Skip("GopherJS does not fully support generics for go1.20 yet.") } -//gopherjs:purge for go1.19 without generics -func hammerStoreLoadPointer(t *testing.T, paddr unsafe.Pointer) {} - -//gopherjs:purge for go1.19 without generics -func hammerStoreLoadPointerMethod(t *testing.T, paddr unsafe.Pointer) {} - func TestHammerStoreLoad(t *testing.T) { t.Skip("use of unsafe") } @@ -61,13 +23,6 @@ func TestAutoAligned64(t *testing.T) { t.Skip("GopherJS emulates atomics, which makes alignment irrelevant.") } -func TestNilDeref(t *testing.T) { - t.Skip("GopherJS does not support generics yet.") -} - -//gopherjs:purge for go1.19 without generics -type List struct{} - func TestHammer32(t *testing.T) { t.Skip("use of unsafe") } diff --git a/compiler/natives/src/sync/map.go b/compiler/natives/src/sync/map.go index 3f81b9b31..66a5147db 100644 --- a/compiler/natives/src/sync/map.go +++ b/compiler/natives/src/sync/map.go @@ -6,7 +6,7 @@ package sync type Map struct { mu Mutex - // replaced atomic.Pointer[readOnly] for go1.20 without generics. + // replaced atomic.Pointer[readOnly] since GopherJS does not fully support generics for go1.20 yet. read atomicReadOnlyPointer dirty map[any]*entry @@ -19,30 +19,3 @@ type atomicReadOnlyPointer struct { func (x *atomicReadOnlyPointer) Load() *readOnly { return x.v } func (x *atomicReadOnlyPointer) Store(val *readOnly) { x.v = val } - -type entry struct { - - // replaced atomic.Pointer[any] for go1.20 without generics. - p atomicAnyPointer -} - -type atomicAnyPointer struct { - v *any -} - -func (x *atomicAnyPointer) Load() *any { return x.v } -func (x *atomicAnyPointer) Store(val *any) { x.v = val } - -func (x *atomicAnyPointer) Swap(new *any) *any { - old := x.v - x.v = new - return old -} - -func (x *atomicAnyPointer) CompareAndSwap(old, new *any) bool { - if x.v == old { - x.v = new - return true - } - return false -} diff --git a/compiler/natives/src/testing/helper_test.go b/compiler/natives/src/testing/helper_test.go deleted file mode 100644 index 6815fd651..000000000 --- a/compiler/natives/src/testing/helper_test.go +++ /dev/null @@ -1,8 +0,0 @@ -//go:build js -// +build js - -package testing - -func TestTBHelper(t *T) { - t.Skip("GopherJS does not support generics yet.") -} diff --git a/compiler/natives/src/testing/helperfuncs_test.go b/compiler/natives/src/testing/helperfuncs_test.go deleted file mode 100644 index 54a1ee737..000000000 --- a/compiler/natives/src/testing/helperfuncs_test.go +++ /dev/null @@ -1,13 +0,0 @@ -//go:build js -// +build js - -package testing - -//gopherjs:purge for go1.19 without generics -func genericHelper[G any](t *T, msg string) - -//gopherjs:purge for go1.19 without generics -var genericIntHelper = genericHelper[int] - -//gopherjs:purge for go1.19 without generics (uses genericHelper) -func testHelper(t *T) diff --git a/compiler/natives/src/time/export_test.go b/compiler/natives/src/time/export_test.go deleted file mode 100644 index 5cd3fc6ab..000000000 --- a/compiler/natives/src/time/export_test.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build js -// +build js - -package time - -// replaced `parseRFC3339[string]` for go1.20 temporarily without generics. -var ParseRFC3339 = func(s string, local *Location) (Time, bool) { - return parseRFC3339(s, local) -} diff --git a/compiler/natives/src/time/format.go b/compiler/natives/src/time/format.go deleted file mode 100644 index 0e1594c19..000000000 --- a/compiler/natives/src/time/format.go +++ /dev/null @@ -1,79 +0,0 @@ -//go:build js -// +build js - -package time - -// copied and replaced for go1.20 temporarily without generics. -func atoi(sAny any) (x int, err error) { - s := asBytes(sAny) - neg := false - if len(s) > 0 && (s[0] == '-' || s[0] == '+') { - neg = s[0] == '-' - s = s[1:] - } - q, remStr, err := leadingInt(s) - rem := []byte(remStr) - x = int(q) - if err != nil || len(rem) > 0 { - return 0, atoiError - } - if neg { - x = -x - } - return x, nil -} - -// copied and replaced for go1.20 temporarily without generics. -func isDigit(sAny any, i int) bool { - s := asBytes(sAny) - if len(s) <= i { - return false - } - c := s[i] - return '0' <= c && c <= '9' -} - -// copied and replaced for go1.20 temporarily without generics. -func parseNanoseconds(sAny any, nbytes int) (ns int, rangeErrString string, err error) { - value := asBytes(sAny) - if !commaOrPeriod(value[0]) { - err = errBad - return - } - if nbytes > 10 { - value = value[:10] - nbytes = 10 - } - if ns, err = atoi(value[1:nbytes]); err != nil { - return - } - if ns < 0 { - rangeErrString = "fractional second" - return - } - scaleDigits := 10 - nbytes - for i := 0; i < scaleDigits; i++ { - ns *= 10 - } - return -} - -// copied and replaced for go1.20 temporarily without generics. -func leadingInt(sAny any) (x uint64, rem string, err error) { - s := asBytes(sAny) - i := 0 - for ; i < len(s); i++ { - c := s[i] - if c < '0' || c > '9' { - break - } - if x > 1<<63/10 { - return 0, rem, errLeadingInt - } - x = x*10 + uint64(c) - '0' - if x > 1<<63 { - return 0, rem, errLeadingInt - } - } - return x, string(s[i:]), nil -} diff --git a/compiler/natives/src/time/format_rfc3339.go b/compiler/natives/src/time/format_rfc3339.go deleted file mode 100644 index 7c69bfc95..000000000 --- a/compiler/natives/src/time/format_rfc3339.go +++ /dev/null @@ -1,85 +0,0 @@ -//go:build js -// +build js - -package time - -import "errors" - -// added for go1.20 temporarily without generics. -func asBytes(s any) []byte { - switch t := s.(type) { - case []byte: - return t - case string: - return []byte(t) - default: - panic(errors.New(`unexpected type passed to asBytes, expected string or []bytes`)) - } -} - -// copied and replaced for go1.20 temporarily without generics. -func parseRFC3339(sAny any, local *Location) (Time, bool) { - s := asBytes(sAny) - ok := true - parseUint := func(s []byte, min, max int) (x int) { - for _, c := range s { - if c < '0' || '9' < c { - ok = false - return min - } - x = x*10 + int(c) - '0' - } - if x < min || max < x { - ok = false - return min - } - return x - } - - if len(s) < len("2006-01-02T15:04:05") { - return Time{}, false - } - year := parseUint(s[0:4], 0, 9999) - month := parseUint(s[5:7], 1, 12) - day := parseUint(s[8:10], 1, daysIn(Month(month), year)) - hour := parseUint(s[11:13], 0, 23) - min := parseUint(s[14:16], 0, 59) - sec := parseUint(s[17:19], 0, 59) - if !ok || !(s[4] == '-' && s[7] == '-' && s[10] == 'T' && s[13] == ':' && s[16] == ':') { - return Time{}, false - } - s = s[19:] - - var nsec int - if len(s) >= 2 && s[0] == '.' && isDigit(s, 1) { - n := 2 - for ; n < len(s) && isDigit(s, n); n++ { - } - nsec, _, _ = parseNanoseconds(s, n) - s = s[n:] - } - - t := Date(year, Month(month), day, hour, min, sec, nsec, UTC) - if len(s) != 1 || s[0] != 'Z' { - if len(s) != len("-07:00") { - return Time{}, false - } - hr := parseUint(s[1:3], 0, 23) - mm := parseUint(s[4:6], 0, 59) - if !ok || !((s[0] == '-' || s[0] == '+') && s[3] == ':') { - return Time{}, false - } - zoneOffset := (hr*60 + mm) * 60 - if s[0] == '-' { - zoneOffset *= -1 - } - t.addSec(-int64(zoneOffset)) - - if _, offset, _, _, _ := local.lookup(t.unixSec()); offset == zoneOffset { - t.setLoc(local) - } else { - t.setLoc(FixedZone("", zoneOffset)) - } - } - return t, true -} diff --git a/compiler/package.go b/compiler/package.go index 34387b5ab..430542b65 100644 --- a/compiler/package.go +++ b/compiler/package.go @@ -11,6 +11,7 @@ import ( "time" "github.com/gopherjs/gopherjs/compiler/analysis" + "github.com/gopherjs/gopherjs/compiler/internal/dce" "github.com/gopherjs/gopherjs/compiler/internal/typeparams" "github.com/gopherjs/gopherjs/compiler/typesutil" "github.com/gopherjs/gopherjs/internal/experiments" @@ -21,6 +22,7 @@ import ( // pkgContext maintains compiler context for a specific package. type pkgContext struct { *analysis.Info + dce.Collector additionalSelections map[*ast.SelectorExpr]typesutil.Selection typesCtx *types.Context @@ -35,7 +37,6 @@ type pkgContext struct { anonTypeMap typeutil.Map escapingVars map[*types.Var]bool indentation int - dependencies map[types.Object]bool minify bool fileSet *token.FileSet errList ErrorList @@ -53,6 +54,15 @@ func (pc *pkgContext) isMain() bool { // JavaScript code (as defined for `var` declarations). type funcContext struct { *analysis.FuncInfo + // Function instance this context corresponds to, or zero if the context is + // top-level or doesn't correspond to a function. For function literals, this + // is a synthetic object that assigns a unique identity to the function. + instance typeparams.Instance + // JavaScript identifier assigned to the function object (the word after the + // "function" keyword in the generated code). This identifier can be used + // within the function scope to reference the function object. It will also + // appear in the stack trace. + funcRef string // Surrounding package context. pkgCtx *pkgContext // Function context, surrounding this function definition. For package-level @@ -104,6 +114,8 @@ type funcContext struct { typeResolver *typeparams.Resolver // Mapping from function-level objects to JS variable names they have been assigned. objectNames map[types.Object]string + // Number of function literals encountered within the current function context. + funcLitCounter int } func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, typesPkg *types.Package, isBlocking func(*types.Func) bool, minify bool) *funcContext { @@ -125,7 +137,6 @@ func newRootCtx(tContext *types.Context, srcs sources, typesInfo *types.Info, ty varPtrNames: make(map[*types.Var]string), escapingVars: make(map[*types.Var]bool), indentation: 1, - dependencies: nil, minify: minify, fileSet: srcs.FileSet, instanceSet: tc.Instances, diff --git a/compiler/statements.go b/compiler/statements.go index 3d7210e47..d4ca76471 100644 --- a/compiler/statements.go +++ b/compiler/statements.go @@ -445,7 +445,7 @@ func (fc *funcContext) translateStmt(stmt ast.Stmt, label *types.Label) { for _, spec := range decl.Specs { o := fc.pkgCtx.Defs[spec.(*ast.TypeSpec).Name].(*types.TypeName) fc.pkgCtx.typeNames.Add(o) - fc.DeclareDCEDep(o) + fc.pkgCtx.DeclareDCEDep(o) } case token.CONST: // skip, constants are inlined diff --git a/compiler/utils.go b/compiler/utils.go index 7fec5b223..c878ba8e9 100644 --- a/compiler/utils.go +++ b/compiler/utils.go @@ -23,6 +23,11 @@ import ( "github.com/gopherjs/gopherjs/compiler/typesutil" ) +// We use this character as a separator in synthetic identifiers instead of a +// regular dot. This character is safe for use in JS identifiers and helps to +// visually separate components of the name when it appears in a stack trace. +const midDot = "ยท" + // root returns the topmost function context corresponding to the package scope. func (fc *funcContext) root() *funcContext { if fc.isRoot() { @@ -102,43 +107,6 @@ func (fc *funcContext) Delayed(f func()) { fc.delayedOutput = fc.CatchOutput(0, f) } -// CollectDCEDeps captures a list of Go objects (types, functions, etc.) -// the code translated inside f() depends on. The returned list of identifiers -// can be used in dead-code elimination. -// -// Note that calling CollectDCEDeps() inside another CollectDCEDeps() call is -// not allowed. -func (fc *funcContext) CollectDCEDeps(f func()) []string { - if fc.pkgCtx.dependencies != nil { - panic(bailout(fmt.Errorf("called funcContext.CollectDependencies() inside another funcContext.CollectDependencies() call"))) - } - - fc.pkgCtx.dependencies = make(map[types.Object]bool) - defer func() { fc.pkgCtx.dependencies = nil }() - - f() - - var deps []string - for o := range fc.pkgCtx.dependencies { - qualifiedName := o.Pkg().Path() + "." + o.Name() - if typesutil.IsMethod(o) { - qualifiedName += "~" - } - deps = append(deps, qualifiedName) - } - sort.Strings(deps) - return deps -} - -// DeclareDCEDep records that the code that is currently being transpiled -// depends on a given Go object. -func (fc *funcContext) DeclareDCEDep(o types.Object) { - if fc.pkgCtx.dependencies == nil { - return // Dependencies are not being collected. - } - fc.pkgCtx.dependencies[o] = true -} - // expandTupleArgs converts a function call which argument is a tuple returned // by another function into a set of individual call arguments corresponding to // tuple elements. @@ -376,6 +344,25 @@ func (fc *funcContext) newTypeIdent(name string, obj types.Object) *ast.Ident { return ident } +// newLitFuncName generates a new synthetic name for a function literal. +func (fc *funcContext) newLitFuncName() string { + fc.funcLitCounter++ + name := &strings.Builder{} + + // If function literal is defined inside another function, qualify its + // synthetic name with the outer function to make it easier to identify. + if fc.instance.Object != nil { + if recvType := typesutil.RecvType(fc.sig.Sig); recvType != nil { + name.WriteString(recvType.Obj().Name()) + name.WriteString(midDot) + } + name.WriteString(fc.instance.Object.Name()) + name.WriteString(midDot) + } + fmt.Fprintf(name, "func%d", fc.funcLitCounter) + return name.String() +} + func (fc *funcContext) setType(e ast.Expr, t types.Type) ast.Expr { fc.pkgCtx.Types[e] = types.TypeAndValue{Type: t} return e @@ -428,7 +415,7 @@ func (fc *funcContext) assignedObjectName(o types.Object) (name string, found bo // allocated as needed. func (fc *funcContext) objectName(o types.Object) string { if isPkgLevel(o) { - fc.DeclareDCEDep(o) + fc.pkgCtx.DeclareDCEDep(o) if o.Pkg() != fc.pkgCtx.Pkg || (isVarOrConst(o) && o.Exported()) { return fc.pkgVar(o.Pkg()) + "." + o.Name() @@ -474,6 +461,21 @@ func (fc *funcContext) instName(inst typeparams.Instance) string { return fmt.Sprintf("%s[%d /* %v */]", objName, fc.pkgCtx.instanceSet.ID(inst), inst.TArgs) } +// methodName returns a JS identifier (specifically, object property name) +// corresponding to the given method. +func (fc *funcContext) methodName(fun *types.Func) string { + if fun.Type().(*types.Signature).Recv() == nil { + panic(fmt.Errorf("expected a method, got a standalone function %v", fun)) + } + name := fun.Name() + // Method names are scoped to their receiver type and guaranteed to be + // unique within that, so we only need to make sure it's not a reserved keyword + if reservedKeywords[name] { + name += "$" + } + return name +} + func (fc *funcContext) varPtrName(o *types.Var) string { if isPkgLevel(o) && o.Exported() { return fc.pkgVar(o.Pkg()) + "." + o.Name() + "$ptr" @@ -512,7 +514,7 @@ func (fc *funcContext) typeName(ty types.Type) string { } // For anonymous composite types, generate a synthetic package-level type - // declaration, which will be reused for all instances of this time. This + // declaration, which will be reused for all instances of this type. This // improves performance, since runtime won't have to synthesize the same type // repeatedly. anonType, ok := fc.pkgCtx.anonTypeMap.At(ty).(*types.TypeName) @@ -523,7 +525,7 @@ func (fc *funcContext) typeName(ty types.Type) string { fc.pkgCtx.anonTypes = append(fc.pkgCtx.anonTypes, anonType) fc.pkgCtx.anonTypeMap.Set(ty, anonType) } - fc.DeclareDCEDep(anonType) + fc.pkgCtx.DeclareDCEDep(anonType) return anonType.Name() } @@ -894,7 +896,15 @@ func rangeCheck(pattern string, constantIndex, array bool) string { } func encodeIdent(name string) string { - return strings.Replace(url.QueryEscape(name), "%", "$", -1) + // Quick-and-dirty way to make any string safe for use as an identifier in JS. + name = url.QueryEscape(name) + // We use unicode middle dot as a visual separator in synthetic identifiers. + // It is safe for use in a JS identifier, so we un-encode it for readability. + name = strings.ReplaceAll(name, "%C2%B7", midDot) + // QueryEscape uses '%' before hex-codes of escaped characters, which is not + // allowed in a JS identifier, use '$' instead. + name = strings.ReplaceAll(name, "%", "$") + return name } // formatJSStructTagVal returns JavaScript code for accessing an object's property @@ -980,3 +990,13 @@ func bailingOut(err interface{}) (*FatalError, bool) { fe, ok := err.(*FatalError) return fe, ok } + +func removeMatching[T comparable](haystack []T, needle T) []T { + var result []T + for _, el := range haystack { + if el != needle { + result = append(result, el) + } + } + return result +} diff --git a/package-lock.json b/package-lock.json index 8fa9be563..b8ba5e000 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6,12 +6,6 @@ "": { "name": "gopherjs", "license": "BSD-2-Clause", - "dependencies": { - "source-map-support": "^0.5.19" - }, - "devDependencies": { - "uglify-es": "^3.3.9" - }, "optionalDependencies": { "syscall": "file:./node-syscall" } @@ -143,11 +137,6 @@ "concat-map": "0.0.1" } }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" - }, "node_modules/cacache": { "version": "15.3.0", "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", @@ -204,12 +193,6 @@ "color-support": "bin.js" } }, - "node_modules/commander": { - "version": "2.13.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.13.0.tgz", - "integrity": "sha512-MVuS359B+YzaWqjCL/c+22gfryv+mCBPHAv3zyVI2GN8EY6IRP8VwtasXn8jyyhvvq84R4ImN1OKRtcbIasjYA==", - "dev": true - }, "node_modules/concat-map": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", @@ -883,23 +866,6 @@ "node": ">= 10" } }, - "node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, "node_modules/sprintf-js": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", @@ -983,23 +949,6 @@ "node": ">=8" } }, - "node_modules/uglify-es": { - "version": "3.3.9", - "resolved": "https://registry.npmjs.org/uglify-es/-/uglify-es-3.3.9.tgz", - "integrity": "sha512-r+MU0rfv4L/0eeW3xZrd16t4NZfK8Ld4SWVglYBb7ez5uXFWHuVRs6xCTrf1yirs9a4j4Y27nn7SRfO6v67XsQ==", - "deprecated": "support for ECMAScript is superseded by `uglify-js` as of v3.13.0", - "dev": true, - "dependencies": { - "commander": "~2.13.0", - "source-map": "~0.6.1" - }, - "bin": { - "uglifyjs": "bin/uglifyjs" - }, - "engines": { - "node": ">=0.8.0" - } - }, "node_modules/unique-filename": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", @@ -1061,6 +1010,7 @@ "optional": true }, "node-syscall": { + "name": "syscall", "hasInstallScript": true, "license": "BSD-2-Clause", "optional": true, diff --git a/package.json b/package.json index f276a4eb1..ec8add087 100644 --- a/package.json +++ b/package.json @@ -1,12 +1,6 @@ { "name": "gopherjs", "license": "BSD-2-Clause", - "devDependencies": { - "uglify-es": "^3.3.9" - }, - "dependencies": { - "source-map-support": "^0.5.19" - }, "optionalDependencies": { "syscall": "file:./node-syscall" } diff --git a/tool.go b/tool.go index 9c467d04f..2c2e909be 100644 --- a/tool.go +++ b/tool.go @@ -842,13 +842,7 @@ func sprintError(err error) string { func runNode(script string, args []string, dir string, quiet bool, out io.Writer) error { var allArgs []string if b, _ := strconv.ParseBool(os.Getenv("SOURCE_MAP_SUPPORT")); os.Getenv("SOURCE_MAP_SUPPORT") == "" || b { - allArgs = []string{"--require", "source-map-support/register"} - if err := exec.Command("node", "--require", "source-map-support/register", "--eval", "").Run(); err != nil { - if !quiet { - fmt.Fprintln(os.Stderr, "gopherjs: Source maps disabled. Install source-map-support module for nice stack traces. See https://github.com/gopherjs/gopherjs#gopherjs-run-gopherjs-test.") - } - allArgs = []string{} - } + allArgs = []string{"--enable-source-maps"} } if runtime.GOOS != "windows" {