diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 07ff3844a..5c4677658 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -14,7 +14,7 @@ concurrency: cancel-in-progress: true env: - GO_VERSION: 1.19.13 + GO_VERSION: 1.20.14 NODE_VERSION: 18 GOLANGCI_VERSION: v1.53.3 GOPHERJS_EXPERIMENT: generics diff --git a/.github/workflows/measure-size.yml b/.github/workflows/measure-size.yml index 1697b1127..a67a7dd4a 100644 --- a/.github/workflows/measure-size.yml +++ b/.github/workflows/measure-size.yml @@ -3,7 +3,8 @@ name: Measure canonical app size on: ['pull_request'] env: - GO_VERSION: '~1.19.13' + GO_VERSION: '~1.20.14' + GOPHERJS_EXPERIMENT: generics jobs: measure: diff --git a/README.md b/README.md index b653bb177..d17409736 100644 --- a/README.md +++ b/README.md @@ -32,21 +32,21 @@ Nearly everything, including Goroutines ([compatibility documentation](https://g ### Installation and Usage -GopherJS [requires Go 1.19 or newer](https://github.com/gopherjs/gopherjs/blob/master/doc/compatibility.md#go-version-compatibility). If you need an older Go +GopherJS [requires Go 1.20 or newer](https://github.com/gopherjs/gopherjs/blob/master/doc/compatibility.md#go-version-compatibility). If you need an older Go version, you can use an [older GopherJS release](https://github.com/gopherjs/gopherjs/releases). Install GopherJS with `go install`: ``` -go install github.com/gopherjs/gopherjs@v1.19.0-beta1 # Or replace 'v1.19.0-beta1' with another version. +go install github.com/gopherjs/gopherjs@v1.20.0-beta1 # Or replace 'v1.20.0-beta1' with another version. ``` -If your local Go distribution as reported by `go version` is newer than Go 1.19, then you need to set the `GOPHERJS_GOROOT` environment variable to a directory that contains a Go 1.19 distribution. For example: +If your local Go distribution as reported by `go version` is newer than Go 1.20, then you need to set the `GOPHERJS_GOROOT` environment variable to a directory that contains a Go 1.20 distribution. For example: ``` -go install golang.org/dl/go1.19.13@latest -go1.19.13 download -export GOPHERJS_GOROOT="$(go1.19.13 env GOROOT)" # Also add this line to your .profile or equivalent. +go install golang.org/dl/go1.20.14@latest +go1.20.14 download +export GOPHERJS_GOROOT="$(go1.20.14 env GOROOT)" # Also add this line to your .profile or equivalent. ``` Now you can use `gopherjs build [package]`, `gopherjs build [files]` or `gopherjs install [package]` which behave similar to the `go` tool. For `main` packages, these commands create a `.js` file and `.js.map` source map in the current directory or in `$GOPATH/bin`. The generated JavaScript file can be used as usual in a website. Use `gopherjs help [command]` to get a list of possible command line flags, e.g. for minification and automatically watching for changes. diff --git a/build/build.go b/build/build.go index 62b360c61..42b979846 100644 --- a/build/build.go +++ b/build/build.go @@ -728,19 +728,33 @@ func (p *PackageData) XTestPackage() *PackageData { // InstallPath returns the path where "gopherjs install" command should place the // generated output. -func (p *PackageData) InstallPath() string { +func (p *PackageData) InstallPath() (string, error) { if p.IsCommand() { name := filepath.Base(p.ImportPath) + ".js" + // For executable packages, mimic go tool behavior if possible. if gobin := os.Getenv("GOBIN"); gobin != "" { - return filepath.Join(gobin, name) - } else if gopath := os.Getenv("GOPATH"); gopath != "" { - return filepath.Join(gopath, "bin", name) - } else if home, err := os.UserHomeDir(); err == nil { - return filepath.Join(home, "go", "bin", name) + return filepath.Join(gobin, name), nil + } + + if gopath := os.Getenv("GOPATH"); gopath != "" { + return filepath.Join(gopath, "bin", name), nil + } + + if home, err := os.UserHomeDir(); err == nil { + return filepath.Join(home, "go", "bin", name), nil } } - return p.PkgObj + + if p.PkgObj != "" { + return p.PkgObj, nil + } + + // The build.Context.Import method stopped populating build.Package.PkgObj + // in 1.20 for packages found in the Goroot. Currently we don't use the + // build.Package.PkgObj except for a fallback when no other locations + // can be found for command packages to install. + return "", fmt.Errorf(`no install location available for %q`, p.ImportPath) } // Session manages internal state GopherJS requires to perform a build. diff --git a/build/context_test.go b/build/context_test.go index 5b377f6b7..a809c066b 100644 --- a/build/context_test.go +++ b/build/context_test.go @@ -1,7 +1,6 @@ package build import ( - "fmt" "go/build" "net/http" "path/filepath" @@ -30,16 +29,19 @@ func TestSimpleCtx(t *testing.T) { t.Run("exists", func(t *testing.T) { tests := []struct { + name string buildCtx XContext wantPkg *PackageData }{ { + name: `embeddedCtx`, buildCtx: ec, wantPkg: &PackageData{ Package: expectedPackage(&ec.bctx, "github.com/gopherjs/gopherjs/js", "wasm"), IsVirtual: true, }, }, { + name: `goCtx`, buildCtx: gc, wantPkg: &PackageData{ Package: expectedPackage(&gc.bctx, "fmt", "wasm"), @@ -49,7 +51,7 @@ func TestSimpleCtx(t *testing.T) { } for _, test := range tests { - t.Run(fmt.Sprintf("%T", test.buildCtx), func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { importPath := test.wantPkg.ImportPath got, err := test.buildCtx.Import(importPath, "", build.FindOnly) if err != nil { @@ -64,25 +66,27 @@ func TestSimpleCtx(t *testing.T) { t.Run("not found", func(t *testing.T) { tests := []struct { + name string buildCtx XContext importPath string }{ { + name: `embeddedCtx`, buildCtx: ec, importPath: "package/not/found", }, { - // Outside of the main module. + name: `goCtx outside of the main module`, buildCtx: gc, importPath: "package/not/found", }, { - // In the main module. + name: `goCtx in the main module`, buildCtx: gc, importPath: "github.com/gopherjs/gopherjs/not/found", }, } for _, test := range tests { - t.Run(fmt.Sprintf("%T", test.buildCtx), func(t *testing.T) { + t.Run(test.name, func(t *testing.T) { _, err := ec.Import(test.importPath, "", build.FindOnly) want := "cannot find package" if err == nil || !strings.Contains(err.Error(), want) { @@ -209,6 +213,6 @@ func expectedPackage(bctx *build.Context, importPath string, goarch string) *bui PkgTargetRoot: targetRoot, BinDir: filepath.Join(bctx.GOROOT, "bin"), Goroot: true, - PkgObj: filepath.Join(targetRoot, importPath+".a"), + PkgObj: ``, // not populated for Goroot packages since 1.20 } } diff --git a/compiler/expressions.go b/compiler/expressions.go index fea80b65a..4b6653731 100644 --- a/compiler/expressions.go +++ b/compiler/expressions.go @@ -1058,6 +1058,9 @@ func (fc *funcContext) translateBuiltin(name string, sig *types.Signature, args case "Offsetof": sel, _ := fc.selectionOf(astutil.RemoveParens(args[0]).(*ast.SelectorExpr)) return fc.formatExpr("%d", typesutil.OffsetOf(sizes32, sel)) + case "SliceData": + t := fc.typeOf(args[0]).Underlying().(*types.Slice) + return fc.formatExpr(`$sliceData(%e, %s)`, args[0], fc.typeName(t)) default: panic(fmt.Sprintf("Unhandled builtin: %s\n", name)) } diff --git a/compiler/linkname.go b/compiler/linkname.go index c4f15a23e..6dd93a709 100644 --- a/compiler/linkname.go +++ b/compiler/linkname.go @@ -21,6 +21,83 @@ type GoLinkname struct { Reference symbol.Name } +// readLinknameFromComment reads the given comment to determine if it's a go:linkname +// directive then returns the linkname information, otherwise returns nil. +func readLinknameFromComment(pkgPath string, comment *ast.Comment) (*GoLinkname, error) { + if !strings.HasPrefix(comment.Text, `//go:linkname `) { + return nil, nil // Not a linkname compiler directive. + } + + fields := strings.Fields(comment.Text) + + // Check that the directive comment has both parts and is on the line by itself. + switch len(fields) { + case 2: + // Ignore one-argument form //go:linkname localName + // This is typically used with "insert"-style links to + // suppresses the usual error for a function that lacks a body. + // The "insert"-style links aren't supported by GopherJS so + // these bodiless functions have to be overridden in the natives anyway. + return nil, nil + case 3: + // Continue for two-argument form //go:linkname localName importPath.extName + break + default: + return nil, fmt.Errorf(`gopherjs: usage requires 2 arguments: //go:linkname localName importPath.extName`) + } + + localPkg, localName := pkgPath, fields[1] + extPkg, extName := ``, fields[2] + + if localName == extName { + // Ignore self referencing links, //go:linkname localName localName + // These function similar to one-argument links. + return nil, nil + } + + pathOffset := 0 + if pos := strings.LastIndexByte(extName, '/'); pos != -1 { + pathOffset = pos + 1 + } + + if idx := strings.IndexByte(extName[pathOffset:], '.'); idx != -1 { + extPkg, extName = extName[:pathOffset+idx], extName[pathOffset+idx+1:] + } + + return &GoLinkname{ + Reference: symbol.Name{PkgPath: localPkg, Name: localName}, + Implementation: symbol.Name{PkgPath: extPkg, Name: extName}, + }, nil +} + +// isMitigatedVarLinkname checks if the given go:linkname directive on +// a variable, which GopherJS doesn't support, is known about. +// We silently ignore such directives, since it doesn't seem to cause any problems. +func isMitigatedVarLinkname(sym symbol.Name) bool { + mitigatedLinks := map[string]bool{ + `reflect.zeroVal`: true, + `math/bits.overflowError`: true, // Defaults in bits_errors_bootstrap.go + `math/bits.divideError`: true, // Defaults in bits_errors_bootstrap.go + } + return mitigatedLinks[sym.String()] +} + +// isMitigatedInsertLinkname checks if the given go:linkname directive +// on a function, where the function has a body, is known about. +// These are unsupported "insert"-style go:linkname directives, +// that we ignore as a link and handle case-by-case in native overrides. +func isMitigatedInsertLinkname(sym symbol.Name) bool { + mitigatedPkg := map[string]bool{ + `runtime`: true, // Lots of "insert"-style links + `internal/fuzz`: true, // Defaults to no-op stubs + } + mitigatedLinks := map[string]bool{ + `internal/bytealg.runtime_cmpstring`: true, + `os.net_newUnixFile`: true, + } + return mitigatedPkg[sym.PkgPath] || mitigatedLinks[sym.String()] +} + // parseGoLinknames processed comments in a source file and extracts //go:linkname // compiler directive from the comments. // @@ -44,63 +121,36 @@ func parseGoLinknames(fset *token.FileSet, pkgPath string, file *ast.File) ([]Go isUnsafe := astutil.ImportsUnsafe(file) processComment := func(comment *ast.Comment) error { - if !strings.HasPrefix(comment.Text, "//go:linkname ") { - return nil // Not a linkname compiler directive. + link, err := readLinknameFromComment(pkgPath, comment) + if err != nil || link == nil { + return err } - // TODO(nevkontakte): Ideally we should check that the directive comment - // is on a line by itself, line Go compiler does, but ast.Comment doesn't - // provide an easy way to find that out. - if !isUnsafe { return fmt.Errorf(`//go:linkname is only allowed in Go files that import "unsafe"`) } - fields := strings.Fields(comment.Text) - if len(fields) != 3 { - return fmt.Errorf(`usage (all fields required): //go:linkname localname importpath.extname`) - } - - localPkg, localName := pkgPath, fields[1] - extPkg, extName := "", fields[2] - if pos := strings.LastIndexByte(extName, '/'); pos != -1 { - if idx := strings.IndexByte(extName[pos+1:], '.'); idx != -1 { - extPkg, extName = extName[0:pos+idx+1], extName[pos+idx+2:] - } - } else if idx := strings.IndexByte(extName, '.'); idx != -1 { - extPkg, extName = extName[0:idx], extName[idx+1:] - } - - obj := file.Scope.Lookup(localName) + obj := file.Scope.Lookup(link.Reference.Name) if obj == nil { - return fmt.Errorf("//go:linkname local symbol %q is not found in the current source file", localName) + return fmt.Errorf("//go:linkname local symbol %q is not found in the current source file", link.Reference.Name) } if obj.Kind != ast.Fun { - if pkgPath == "math/bits" || pkgPath == "reflect" { - // These standard library packages are known to use go:linkname with - // variables, which GopherJS doesn't support. We silently ignore such - // directives, since it doesn't seem to cause any problems. + if isMitigatedVarLinkname(link.Reference) { return nil } return fmt.Errorf("gopherjs: //go:linkname is only supported for functions, got %q", obj.Kind) } - decl := obj.Decl.(*ast.FuncDecl) - if decl.Body != nil { - if pkgPath == "runtime" || pkgPath == "internal/bytealg" || pkgPath == "internal/fuzz" { - // These standard library packages are known to use unsupported - // "insert"-style go:linkname directives, which we ignore here and handle - // case-by-case in native overrides. + if decl := obj.Decl.(*ast.FuncDecl); decl.Body != nil { + if isMitigatedInsertLinkname(link.Reference) { return nil } - return fmt.Errorf("gopherjs: //go:linkname can not insert local implementation into an external package %q", extPkg) + return fmt.Errorf("gopherjs: //go:linkname can not insert local implementation into an external package %q", link.Implementation.PkgPath) } + // Local function has no body, treat it as a reference to an external implementation. - directives = append(directives, GoLinkname{ - Reference: symbol.Name{PkgPath: localPkg, Name: localName}, - Implementation: symbol.Name{PkgPath: extPkg, Name: extName}, - }) + directives = append(directives, *link) return nil } diff --git a/compiler/linkname_test.go b/compiler/linkname_test.go index 7f46c6cfb..9f991d394 100644 --- a/compiler/linkname_test.go +++ b/compiler/linkname_test.go @@ -45,6 +45,7 @@ func makePackage(t *testing.T, src string) *types.Package { func TestParseGoLinknames(t *testing.T) { tests := []struct { desc string + pkgPath string src string wantError string wantDirectives []GoLinkname @@ -106,7 +107,7 @@ func TestParseGoLinknames(t *testing.T) { `, wantError: `import "unsafe"`, }, { - desc: "gopherjs: both parameters are required", + desc: "gopherjs: ignore one-argument linknames", src: `package testcase import _ "unsafe" @@ -114,6 +115,16 @@ func TestParseGoLinknames(t *testing.T) { //go:linkname a func a() `, + wantDirectives: []GoLinkname{}, + }, { + desc: `gopherjs: linkname has too many arguments`, + src: `package testcase + + import _ "unsafe" + + //go:linkname a other/package.a too/many.args + func a() + `, wantError: "usage", }, { desc: "referenced function doesn't exist", @@ -135,6 +146,17 @@ func TestParseGoLinknames(t *testing.T) { var a string = "foo" `, wantError: `is only supported for functions`, + }, { + desc: `gopherjs: ignore know referenced variables`, + pkgPath: `reflect`, + src: `package reflect + + import _ "unsafe" + + //go:linkname zeroVal other/package.zeroVal + var zeroVal []bytes + `, + wantDirectives: []GoLinkname{}, }, { desc: "gopherjs: can not insert local implementation", src: `package testcase @@ -145,13 +167,60 @@ func TestParseGoLinknames(t *testing.T) { func a() { println("do a") } `, wantError: `can not insert local implementation`, + }, { + desc: `gopherjs: ignore known local implementation insert`, + pkgPath: `runtime`, // runtime is known and ignored + src: `package runtime + + import _ "unsafe" + + //go:linkname a other/package.a + func a() { println("do a") } + `, + wantDirectives: []GoLinkname{}, + }, { + desc: `gopherjs: link to function with receiver`, + // //go:linkname .. + src: `package testcase + + import _ "unsafe" + + //go:linkname a other/package.b.a + func a() + `, + wantDirectives: []GoLinkname{ + { + Reference: symbol.Name{PkgPath: `testcase`, Name: `a`}, + Implementation: symbol.Name{PkgPath: `other/package`, Name: `b.a`}, + }, + }, + }, { + desc: `gopherjs: link to function with pointer receiver`, + // //go:linkname .<(*type)>. + src: `package testcase + + import _ "unsafe" + + //go:linkname a other/package.*b.a + func a() + `, + wantDirectives: []GoLinkname{ + { + Reference: symbol.Name{PkgPath: `testcase`, Name: `a`}, + Implementation: symbol.Name{PkgPath: `other/package`, Name: `*b.a`}, + }, + }, }, } for _, test := range tests { t.Run(test.desc, func(t *testing.T) { file, fset := parseSource(t, test.src) - directives, err := parseGoLinknames(fset, "testcase", file) + pkgPath := `testcase` + if len(test.pkgPath) > 0 { + pkgPath = test.pkgPath + } + directives, err := parseGoLinknames(fset, pkgPath, file) if test.wantError != "" { if err == nil { diff --git a/compiler/natives/src/crypto/ecdh/nist.go b/compiler/natives/src/crypto/ecdh/nist.go new file mode 100644 index 000000000..ecaa84d76 --- /dev/null +++ b/compiler/natives/src/crypto/ecdh/nist.go @@ -0,0 +1,58 @@ +//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 new file mode 100644 index 000000000..cf3da4ec8 --- /dev/null +++ b/compiler/natives/src/crypto/ecdsa/ecdsa.go @@ -0,0 +1,98 @@ +//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 new file mode 100644 index 000000000..efb4d7b5e --- /dev/null +++ b/compiler/natives/src/crypto/ecdsa/ecdsa_test.go @@ -0,0 +1,12 @@ +//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/vendor/golang.org/x/crypto/internal/subtle/aliasing.go b/compiler/natives/src/crypto/internal/alias/alias.go similarity index 88% rename from compiler/natives/src/vendor/golang.org/x/crypto/internal/subtle/aliasing.go rename to compiler/natives/src/crypto/internal/alias/alias.go index 104ac82bb..e6bb87536 100644 --- a/compiler/natives/src/vendor/golang.org/x/crypto/internal/subtle/aliasing.go +++ b/compiler/natives/src/crypto/internal/alias/alias.go @@ -1,11 +1,11 @@ //go:build js // +build js -package subtle +package alias // This file duplicated is these two locations: -// - src/crypto/internal/subtle/ -// - src/golang.org/x/crypto/internal/subtle/ +// - src/crypto/internal/alias/ +// - src/golang.org/x/crypto/internal/alias/ import "github.com/gopherjs/gopherjs/js" diff --git a/compiler/natives/src/crypto/internal/boring/aes.go b/compiler/natives/src/crypto/internal/boring/aes.go new file mode 100644 index 000000000..e2a840440 --- /dev/null +++ b/compiler/natives/src/crypto/internal/boring/aes.go @@ -0,0 +1,10 @@ +//go:build js +// +build js + +package boring + +import "crypto/internal/alias" + +func anyOverlap(x, y []byte) bool { + return alias.AnyOverlap(x, y) +} diff --git a/compiler/natives/src/crypto/internal/boring/bcache/cache.go b/compiler/natives/src/crypto/internal/boring/bcache/cache.go index afff404ce..4c4e0dab6 100644 --- a/compiler/natives/src/crypto/internal/boring/bcache/cache.go +++ b/compiler/natives/src/crypto/internal/boring/bcache/cache.go @@ -20,6 +20,9 @@ func (c *Cache) Put(k, v unsafe.Pointer) {} //gopherjs:purge func (c *Cache) table() *[cacheSize]unsafe.Pointer +//gopherjs:purge +type cacheTable struct{} + //gopherjs:purge type cacheEntry struct{} diff --git a/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go b/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go index 12f2c4da4..a23e975a0 100644 --- a/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go +++ b/compiler/natives/src/crypto/internal/boring/bcache/cache_test.go @@ -5,6 +5,8 @@ package bcache import "testing" +var registeredCache Cache + func TestCache(t *testing.T) { t.Skip(`This test uses runtime.GC(), which GopherJS doesn't support`) } diff --git a/compiler/natives/src/crypto/internal/nistec/nistec_test.go b/compiler/natives/src/crypto/internal/nistec/nistec_test.go index d755e7ec3..ea91d7ed2 100644 --- a/compiler/natives/src/crypto/internal/nistec/nistec_test.go +++ b/compiler/natives/src/crypto/internal/nistec/nistec_test.go @@ -18,52 +18,52 @@ type nistPoint[T any] interface{} func TestEquivalents(t *testing.T) { t.Run("P224", func(t *testing.T) { - testEquivalents(t, nistec.NewP224WrappedPoint, nistec.NewP224WrappedGenerator, elliptic.P224()) + testEquivalents(t, nistec.NewP224WrappedPoint, elliptic.P224()) }) t.Run("P256", func(t *testing.T) { - testEquivalents(t, nistec.NewP256WrappedPoint, nistec.NewP256WrappedGenerator, elliptic.P256()) + testEquivalents(t, nistec.NewP256WrappedPoint, elliptic.P256()) }) t.Run("P384", func(t *testing.T) { - testEquivalents(t, nistec.NewP384WrappedPoint, nistec.NewP384WrappedGenerator, elliptic.P384()) + testEquivalents(t, nistec.NewP384WrappedPoint, elliptic.P384()) }) t.Run("P521", func(t *testing.T) { - testEquivalents(t, nistec.NewP521WrappedPoint, nistec.NewP521WrappedGenerator, elliptic.P521()) + testEquivalents(t, nistec.NewP521WrappedPoint, elliptic.P521()) }) } //gopherjs:override-signature -func testEquivalents(t *testing.T, newPoint, newGenerator func() nistec.WrappedPoint, c elliptic.Curve) +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, nistec.NewP224WrappedGenerator, elliptic.P224()) + testScalarMult(t, nistec.NewP224WrappedPoint, elliptic.P224()) }) t.Run("P256", func(t *testing.T) { - testScalarMult(t, nistec.NewP256WrappedPoint, nistec.NewP256WrappedGenerator, elliptic.P256()) + testScalarMult(t, nistec.NewP256WrappedPoint, elliptic.P256()) }) t.Run("P384", func(t *testing.T) { - testScalarMult(t, nistec.NewP384WrappedPoint, nistec.NewP384WrappedGenerator, elliptic.P384()) + testScalarMult(t, nistec.NewP384WrappedPoint, elliptic.P384()) }) t.Run("P521", func(t *testing.T) { - testScalarMult(t, nistec.NewP521WrappedPoint, nistec.NewP521WrappedGenerator, elliptic.P521()) + testScalarMult(t, nistec.NewP521WrappedPoint, elliptic.P521()) }) } //gopherjs:override-signature -func testScalarMult(t *testing.T, newPoint, newGenerator func() nistec.WrappedPoint, c elliptic.Curve) +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.NewP224WrappedGenerator(), 28) + benchmarkScalarMult(b, nistec.NewP224WrappedPoint().SetGenerator(), 28) }) b.Run("P256", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP256WrappedGenerator(), 32) + benchmarkScalarMult(b, nistec.NewP256WrappedPoint().SetGenerator(), 32) }) b.Run("P384", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP384WrappedGenerator(), 48) + benchmarkScalarMult(b, nistec.NewP384WrappedPoint().SetGenerator(), 48) }) b.Run("P521", func(b *testing.B) { - benchmarkScalarMult(b, nistec.NewP521WrappedGenerator(), 66) + benchmarkScalarMult(b, nistec.NewP521WrappedPoint().SetGenerator(), 66) }) } @@ -72,16 +72,16 @@ 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.NewP224WrappedGenerator(), 28) + benchmarkScalarBaseMult(b, nistec.NewP224WrappedPoint().SetGenerator(), 28) }) b.Run("P256", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP256WrappedGenerator(), 32) + benchmarkScalarBaseMult(b, nistec.NewP256WrappedPoint().SetGenerator(), 32) }) b.Run("P384", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP384WrappedGenerator(), 48) + benchmarkScalarBaseMult(b, nistec.NewP384WrappedPoint().SetGenerator(), 48) }) b.Run("P521", func(b *testing.B) { - benchmarkScalarBaseMult(b, nistec.NewP521WrappedGenerator(), 66) + benchmarkScalarBaseMult(b, nistec.NewP521WrappedPoint().SetGenerator(), 66) }) } diff --git a/compiler/natives/src/crypto/internal/nistec/wrapper.go b/compiler/natives/src/crypto/internal/nistec/wrapper.go index 0d6706b52..afa2b7049 100644 --- a/compiler/natives/src/crypto/internal/nistec/wrapper.go +++ b/compiler/natives/src/crypto/internal/nistec/wrapper.go @@ -3,8 +3,11 @@ 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 @@ -24,14 +27,18 @@ func NewP224WrappedPoint() WrappedPoint { return wrapP224(NewP224Point()) } -func NewP224WrappedGenerator() WrappedPoint { - return wrapP224(NewP224Generator()) +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 @@ -67,14 +74,18 @@ func NewP256WrappedPoint() WrappedPoint { return wrapP256(NewP256Point()) } -func NewP256WrappedGenerator() WrappedPoint { - return wrapP256(NewP256Generator()) +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 @@ -110,14 +121,18 @@ func NewP521WrappedPoint() WrappedPoint { return wrapP521(NewP521Point()) } -func NewP521WrappedGenerator() WrappedPoint { - return wrapP521(NewP521Generator()) +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 @@ -153,14 +168,18 @@ func NewP384WrappedPoint() WrappedPoint { return wrapP384(NewP384Point()) } -func NewP384WrappedGenerator() WrappedPoint { - return wrapP384(NewP384Generator()) +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 diff --git a/compiler/natives/src/crypto/internal/subtle/aliasing.go b/compiler/natives/src/crypto/internal/subtle/aliasing.go deleted file mode 100644 index 145687d59..000000000 --- a/compiler/natives/src/crypto/internal/subtle/aliasing.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build js -// +build js - -package subtle - -// This file duplicated is these two locations: -// - src/crypto/internal/subtle/aliasing.go -// - src/golang.org/x/crypto/internal/subtle/aliasing.go -// - src/golang.org/x/crypto/internal/alias/alias.go - -import "github.com/gopherjs/gopherjs/js" - -// AnyOverlap reports whether x and y share memory at any (not necessarily -// corresponding) index. The memory beyond the slice length is ignored. -func AnyOverlap(x, y []byte) bool { - // GopherJS: We can't rely on pointer arithmetic, so use GopherJS slice internals. - return len(x) > 0 && len(y) > 0 && - js.InternalObject(x).Get("$array") == js.InternalObject(y).Get("$array") && - js.InternalObject(x).Get("$offset").Int() <= js.InternalObject(y).Get("$offset").Int()+len(y)-1 && - js.InternalObject(y).Get("$offset").Int() <= js.InternalObject(x).Get("$offset").Int()+len(x)-1 -} diff --git a/compiler/natives/src/crypto/subtle/xor.go b/compiler/natives/src/crypto/subtle/xor.go new file mode 100644 index 000000000..eccf98531 --- /dev/null +++ b/compiler/natives/src/crypto/subtle/xor.go @@ -0,0 +1,78 @@ +//go:build js +// +build js + +package subtle + +import "github.com/gopherjs/gopherjs/js" + +const wordSize = 4 // bytes for a Uint32Array + +func XORBytes(dst, x, y []byte) int { + n := len(x) + if len(y) < n { + n = len(y) + } + if n == 0 { + return 0 + } + if n > len(dst) { + panic("subtle.XORBytes: dst too short") + } + + // The original uses unsafe and uintptr for specific architecture + // to pack registers full instead of doing one byte at a time. + // We can't do the unsafe conversions from []byte to []uintptr + // but we can convert a Uint8Array into a Uint32Array, + // so we'll simply do it four bytes at a time plus any remainder. + // The following is similar to xorBytes from xor_generic.go + + dst = dst[:n] + x = x[:n] + y = y[:n] + if wordCount := n / wordSize; wordCount > 0 && + aligned(dst) && aligned(x) && aligned(y) { + dstWords := words(dst) + xWords := words(x) + yWords := words(y) + for i := range dstWords { + dstWords[i] = xWords[i] ^ yWords[i] + } + done := n &^ int(wordSize-1) + dst = dst[done:] + x = x[done:] + y = y[done:] + } + for i := range dst { + dst[i] = x[i] ^ y[i] + } + return n +} + +// aligned determines whether the slice is word-aligned since +// Uint32Array's require the offset to be multiples of 4. +func aligned(b []byte) bool { + slice := js.InternalObject(b) + offset := slice.Get(`$offset`).Int() + return offset%wordSize == 0 +} + +// words returns a []uint pointing at the same data as b, +// with any trailing partial word removed. +// The given b must have a word aligned offset. +func words(b []byte) []uint { + slice := js.InternalObject(b) + offset := slice.Get(`$offset`).Int() + length := slice.Get(`$length`).Int() + byteBuffer := slice.Get(`$array`).Get(`buffer`) + wordBuffer := js.Global.Get(`Uint32Array`).New(byteBuffer, offset, length/wordSize) + return wordBuffer.Interface().([]uint) +} + +//gopherjs:purge +const supportsUnaligned = false + +//gopherjs:purge +func xorBytes(dstb, xb, yb *byte, n int) + +//gopherjs:purge +func xorLoop[T byte | uintptr](dst, x, y []T) {} diff --git a/compiler/natives/src/crypto/tls/cache_test.go b/compiler/natives/src/crypto/tls/cache_test.go new file mode 100644 index 000000000..a0881b3fb --- /dev/null +++ b/compiler/natives/src/crypto/tls/cache_test.go @@ -0,0 +1,13 @@ +//go:build js + +package tls + +import "testing" + +func TestCertCache(t *testing.T) { + t.Skip("GC based Cache is not supported by GopherJS") +} + +func BenchmarkCertCache(b *testing.B) { + b.Skip("GC based Cache is not supported by GopherJS") +} diff --git a/compiler/natives/src/encoding/gob/gob.go b/compiler/natives/src/encoding/gob/gob.go new file mode 100644 index 000000000..244f72ed7 --- /dev/null +++ b/compiler/natives/src/encoding/gob/gob.go @@ -0,0 +1,39 @@ +//go:build js +// +build js + +package gob + +import ( + "reflect" + "sync" +) + +type typeInfo struct { + id typeId + encInit sync.Mutex + + // temporarily replacement of atomic.Pointer[encEngine] for go1.20 without generics. + encoder atomicEncEnginePointer + wire *wireType +} + +type atomicEncEnginePointer struct { + v *encEngine +} + +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/encoding/gob/gob_test.go b/compiler/natives/src/encoding/gob/gob_test.go index 823b572ac..a2f303ab6 100644 --- a/compiler/natives/src/encoding/gob/gob_test.go +++ b/compiler/natives/src/encoding/gob/gob_test.go @@ -105,3 +105,11 @@ func TestTypeRace(t *testing.T) { // cannot succeed when nosync is used. t.Skip("using nosync") } + +func TestCountEncodeMallocs(t *testing.T) { + t.Skip("testing.AllocsPerRun not supported in GopherJS") +} + +func TestCountDecodeMallocs(t *testing.T) { + t.Skip("testing.AllocsPerRun not supported in GopherJS") +} diff --git a/compiler/natives/src/go/token/position.go b/compiler/natives/src/go/token/position.go index 6a1ee0c15..436c48380 100644 --- a/compiler/natives/src/go/token/position.go +++ b/compiler/natives/src/go/token/position.go @@ -20,3 +20,11 @@ type atomicFilePointer struct { 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/golang.org/x/crypto/internal/subtle/aliasing.go b/compiler/natives/src/golang.org/x/crypto/internal/subtle/aliasing.go deleted file mode 100644 index 145687d59..000000000 --- a/compiler/natives/src/golang.org/x/crypto/internal/subtle/aliasing.go +++ /dev/null @@ -1,21 +0,0 @@ -//go:build js -// +build js - -package subtle - -// This file duplicated is these two locations: -// - src/crypto/internal/subtle/aliasing.go -// - src/golang.org/x/crypto/internal/subtle/aliasing.go -// - src/golang.org/x/crypto/internal/alias/alias.go - -import "github.com/gopherjs/gopherjs/js" - -// AnyOverlap reports whether x and y share memory at any (not necessarily -// corresponding) index. The memory beyond the slice length is ignored. -func AnyOverlap(x, y []byte) bool { - // GopherJS: We can't rely on pointer arithmetic, so use GopherJS slice internals. - return len(x) > 0 && len(y) > 0 && - js.InternalObject(x).Get("$array") == js.InternalObject(y).Get("$array") && - js.InternalObject(x).Get("$offset").Int() <= js.InternalObject(y).Get("$offset").Int()+len(y)-1 && - js.InternalObject(y).Get("$offset").Int() <= js.InternalObject(x).Get("$offset").Int()+len(x)-1 -} diff --git a/compiler/natives/src/internal/coverage/slicereader/slicereader.go b/compiler/natives/src/internal/coverage/slicereader/slicereader.go new file mode 100644 index 000000000..4346d7c97 --- /dev/null +++ b/compiler/natives/src/internal/coverage/slicereader/slicereader.go @@ -0,0 +1,9 @@ +//go:build js +// +build js + +package slicereader + +// Overwritten to avoid `unsafe.String` +func toString(b []byte) string { + return string(b) +} diff --git a/compiler/natives/src/internal/godebug/godebug.go b/compiler/natives/src/internal/godebug/godebug.go new file mode 100644 index 000000000..e43006c3f --- /dev/null +++ b/compiler/natives/src/internal/godebug/godebug.go @@ -0,0 +1,87 @@ +//go:build js +// +build js + +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/unsafeheader/unsafeheader.go b/compiler/natives/src/internal/unsafeheader/unsafeheader.go new file mode 100644 index 000000000..4a7e43342 --- /dev/null +++ b/compiler/natives/src/internal/unsafeheader/unsafeheader.go @@ -0,0 +1,16 @@ +//go:build js +// +build js + +package unsafeheader + +// Slice and String is Go's runtime representations which is different +// from GopherJS's runtime representations. By purging these types, +// it will prevent failures in JS where the code compiles fine but +// expects there to be a constructor which doesn't exist when casting +// from GopherJS's representation into Go's representation. + +//gopherjs:purge +type Slice struct{} + +//gopherjs:purge +type String struct{} diff --git a/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go b/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go index f20cf31fa..52e814636 100644 --- a/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go +++ b/compiler/natives/src/internal/unsafeheader/unsafeheader_test.go @@ -5,6 +5,16 @@ package unsafeheader_test import "testing" +func TestTypeMatchesReflectType(t *testing.T) { + t.Skip("GopherJS uses different slice and string implementation than internal/unsafeheader.") +} + +//gopherjs:purge +func testHeaderMatchesReflect() + +//gopherjs:purge +func typeCompatible() + func TestWriteThroughHeader(t *testing.T) { t.Skip("GopherJS uses different slice and string implementation than internal/unsafeheader.") } diff --git a/compiler/natives/src/math/rand/rand.go b/compiler/natives/src/math/rand/rand.go new file mode 100644 index 000000000..0dfb1b279 --- /dev/null +++ b/compiler/natives/src/math/rand/rand.go @@ -0,0 +1,9 @@ +//go:build js +// +build js + +package rand + +import _ "unsafe" + +//go:linkname fastrand64 runtime.fastrand64 +func fastrand64() uint64 diff --git a/compiler/natives/src/net/fd_unix.go b/compiler/natives/src/net/fd_unix.go new file mode 100644 index 000000000..d819677d6 --- /dev/null +++ b/compiler/natives/src/net/fd_unix.go @@ -0,0 +1,14 @@ +//go:build js +// +build js + +package net + +import ( + "os" + _ "unsafe" // for go:linkname +) + +// Reversing the linkname direction +// +//go:linkname newUnixFile os.net_newUnixFile +func newUnixFile(fd uintptr, name string) *os.File diff --git a/compiler/natives/src/net/http/client_test.go b/compiler/natives/src/net/http/client_test.go index 302b800df..b3739fc09 100644 --- a/compiler/natives/src/net/http/client_test.go +++ b/compiler/natives/src/net/http/client_test.go @@ -6,14 +6,14 @@ import ( "testing" ) -func testClientTimeout(t *testing.T, h2 bool) { +func testClientTimeout(t *testing.T, mode testMode) { // The original test expects Client.Timeout error to be returned, but under // GopherJS an "i/o timeout" error is frequently returned. Otherwise the test // seems to be working correctly. t.Skip("Flaky test under GopherJS.") } -func testClientTimeout_Headers(t *testing.T, h2 bool) { +func testClientTimeout_Headers(t *testing.T, mode testMode) { // The original test expects Client.Timeout error to be returned, but under // GopherJS an "i/o timeout" error is frequently returned. Otherwise the test // seems to be working correctly. diff --git a/compiler/natives/src/net/http/clientserver_test.go b/compiler/natives/src/net/http/clientserver_test.go index 35b44dd4d..39f1a2d73 100644 --- a/compiler/natives/src/net/http/clientserver_test.go +++ b/compiler/natives/src/net/http/clientserver_test.go @@ -7,10 +7,10 @@ import ( "testing" ) -func testTransportGCRequest(t *testing.T, h2, body bool) { +func testTransportGCRequest(t *testing.T, mode testMode, body bool) { t.Skip("The test relies on runtime.SetFinalizer(), which is not supported by GopherJS.") } -func testWriteHeaderAfterWrite(t *testing.T, h2, hijack bool) { +func testWriteHeaderAfterWrite(t *testing.T, mode testMode, hijack bool) { t.Skip("GopherJS source maps don't preserve original function names in stack traces, which this test relied on.") } diff --git a/compiler/natives/src/net/http/http.go b/compiler/natives/src/net/http/http.go index 8fd607c4d..f82c0363c 100644 --- a/compiler/natives/src/net/http/http.go +++ b/compiler/natives/src/net/http/http.go @@ -6,10 +6,15 @@ package http import ( "bufio" "bytes" + "context" + "crypto/tls" "errors" "io" + "net" "net/textproto" "strconv" + "sync" + "sync/atomic" "github.com/gopherjs/gopherjs/js" ) @@ -113,3 +118,30 @@ func (t *XHRTransport) CancelRequest(req *Request) { xhr.Call("abort") } } + +type conn struct { + server *Server + cancelCtx context.CancelFunc + rwc net.Conn + remoteAddr string + tlsState *tls.ConnectionState + werr error + r *connReader + bufr *bufio.Reader + bufw *bufio.Writer + lastMethod string + + // temporarily replacement of `atomic.Pointer[response]` for go1.20 without generics. + curReq atomicResponsePointer + + curState atomic.Uint64 + mu sync.Mutex + hijackedv bool +} + +type atomicResponsePointer struct { + v *response +} + +func (x *atomicResponsePointer) Load() *response { return x.v } +func (x *atomicResponsePointer) Store(val *response) { x.v = val } diff --git a/compiler/natives/src/os/os.go b/compiler/natives/src/os/os.go index a45e13508..4adf5bb6e 100644 --- a/compiler/natives/src/os/os.go +++ b/compiler/natives/src/os/os.go @@ -30,7 +30,7 @@ func init() { } } -func runtime_beforeExit() {} +func runtime_beforeExit(exitCode int) {} func executable() (string, error) { return "", errors.New("Executable not implemented for GOARCH=js") diff --git a/compiler/natives/src/reflect/reflect.go b/compiler/natives/src/reflect/reflect.go index 47b93662e..ce290ade6 100644 --- a/compiler/natives/src/reflect/reflect.go +++ b/compiler/natives/src/reflect/reflect.go @@ -832,6 +832,21 @@ func cvtSliceArrayPtr(v Value, t Type) Value { return Value{t.common(), unsafe.Pointer(array.Unsafe()), v.flag&^(flagIndir|flagAddr|flagKindMask) | flag(Ptr)} } +// convertOp: []T -> [N]T +func cvtSliceArray(v Value, t Type) Value { + n := t.Len() + if n > v.Len() { + panic("reflect: cannot convert slice with length " + itoa.Itoa(v.Len()) + " to array with length " + itoa.Itoa(n)) + } + + slice := v.object() + dst := MakeSlice(SliceOf(t.Elem()), n, n).object() + js.Global.Call("$copySlice", dst, slice) + + arr := dst.Get("$array") + return Value{t.common(), unsafe.Pointer(arr.Unsafe()), v.flag&^(flagAddr|flagKindMask) | flag(Array)} +} + func Copy(dst, src Value) int { dk := dst.kind() if dk != Array && dk != Slice { @@ -1325,6 +1340,54 @@ func getJsTag(tag string) string { return "" } +func (v Value) UnsafePointer() unsafe.Pointer { + return unsafe.Pointer(v.Pointer()) +} + +func (v Value) grow(n int) { + if n < 0 { + panic(`reflect.Value.Grow: negative len`) + } + + s := v.object() + len := s.Get(`$length`).Int() + if len+n < 0 { + panic(`reflect.Value.Grow: slice overflow`) + } + + cap := s.Get(`$capacity`).Int() + if len+n > cap { + ns := js.Global.Call("$growSlice", s, len+n) + js.InternalObject(v.ptr).Call("$set", ns) + } +} + +// extendSlice is used by native reflect.Append and reflect.AppendSlice +// Overridden to avoid the use of `unsafeheader.Slice` since GopherJS +// uses different slice implementation. +func (v Value) extendSlice(n int) Value { + v.mustBeExported() + v.mustBe(Slice) + + s := v.object() + sNil := jsType(v.typ).Get(`nil`) + fl := flagIndir | flag(Slice) + if s == sNil && n <= 0 { + return makeValue(v.typ, wrapJsObject(v.typ, sNil), fl) + } + + newSlice := jsType(v.typ).New(s.Get("$array")) + newSlice.Set("$offset", s.Get("$offset")) + newSlice.Set("$length", s.Get("$length")) + newSlice.Set("$capacity", s.Get("$capacity")) + + v2 := makeValue(v.typ, wrapJsObject(v.typ, newSlice), fl) + v2.grow(n) + s2 := v2.object() + s2.Set(`$length`, s2.Get(`$length`).Int()+n) + return v2 +} + func (v Value) Index(i int) Value { switch k := v.kind(); k { case Array: @@ -1381,6 +1444,11 @@ func (v Value) InterfaceData() [2]uintptr { panic(errors.New("InterfaceData is not supported by GopherJS")) } +func (v Value) SetZero() { + v.mustBeAssignable() + v.Set(Zero(v.typ)) +} + func (v Value) IsNil() bool { switch k := v.kind(); k { case Ptr, Slice: @@ -1420,6 +1488,9 @@ func (v Value) Len() int { } } +//gopherjs:purge Not used since Len() is overridden. +func (v Value) lenNonSlice() int + func (v Value) Pointer() uintptr { switch k := v.kind(); k { case Chan, Map, Ptr, UnsafePointer: @@ -1810,3 +1881,13 @@ func verifyNotInHeapPtr(p uintptr) bool { // always return true. return true } + +// typedslicecopy is implemented in prelude.js as $copySlice +// +//gopherjs:purge +func typedslicecopy(elemType *rtype, dst, src unsafeheader.Slice) int + +// growslice is implemented in prelude.js as $growSlice. +// +//gopherjs:purge +func growslice(t *rtype, old unsafeheader.Slice, num int) unsafeheader.Slice diff --git a/compiler/natives/src/runtime/runtime.go b/compiler/natives/src/runtime/runtime.go index 9f8425af8..037e150b9 100644 --- a/compiler/natives/src/runtime/runtime.go +++ b/compiler/natives/src/runtime/runtime.go @@ -492,3 +492,45 @@ func nanotime() int64 { const millisecond = 1_000_000 return js.Global.Get("Date").New().Call("getTime").Int64() * millisecond } + +const godebugEnvKey = `GODEBUG` + +var godebugUpdate func(def, env string) + +// godebug_setUpdate implements the setUpdate in src/internal/godebug/godebug.go +func godebug_setUpdate(update func(def, env string)) { + godebugUpdate = update + godebugEnv := getEnvString(godebugEnvKey) + godebug_notify(godebugEnvKey, godebugEnv) +} + +func getEnvString(key string) string { + process := js.Global.Get(`process`) + if process == js.Undefined { + return `` + } + + env := process.Get(`env`) + if env == js.Undefined { + return `` + } + + value := env.Get(key) + if value == js.Undefined { + return `` + } + + return value.String() +} + +// godebug_notify is the function is called by syscall anytime an environment +// variable is set or unset. It emit the GODEBUG setting if it was changed. +func godebug_notify(key, value string) { + update := godebugUpdate + if update == nil || key != godebugEnvKey { + return + } + + godebugDefault := `` + update(godebugDefault, value) +} diff --git a/compiler/natives/src/strings/strings.go b/compiler/natives/src/strings/strings.go index 2867872f6..f7eef55af 100644 --- a/compiler/natives/src/strings/strings.go +++ b/compiler/natives/src/strings/strings.go @@ -73,3 +73,48 @@ func Clone(s string) string { // memory overheads and simply return the string as-is. return s } + +// Repeat is the go1.19 implementation of strings.Repeat. +// +// In the go1.20 implementation, the function was changed to use chunks that +// are 8KB in size to improve speed and cache access. This change is faster +// when running native Go code. However, for GopherJS, the change is much +// slower than the go1.19 implementation. +// +// The go1.20 change made tests like encoding/pem TestCVE202224675 take +// significantly longer to run for GopherJS. +// go1.19 concatenates 24 times and the test takes about 8 seconds. +// go1.20 concatenates about 15000 times and can take over a hour. +// +// We can't use `js.InternalObject(s).Call("repeat", count).String()` +// because JS performs additional UTF-8 escapes meaning tests like +// hash/adler32 TestGolden will fail because the wrong input is created. +func Repeat(s string, count int) string { + if count == 0 { + return "" + } + + // Since we cannot return an error on overflow, + // we should panic if the repeat will generate + // an overflow. + // See Issue golang.org/issue/16237 + if count < 0 { + panic("strings: negative Repeat count") + } else if len(s)*count/count != len(s) { + panic("strings: Repeat count causes overflow") + } + + n := len(s) * count + var b Builder + b.Grow(n) + b.WriteString(s) + for b.Len() < n { + if b.Len() <= n/2 { + b.WriteString(b.String()) + } else { + b.WriteString(b.String()[:n-b.Len()]) + break + } + } + return b.String() +} diff --git a/compiler/natives/src/strings/strings_test.go b/compiler/natives/src/strings/strings_test.go index fb9a4a57a..3b0775e63 100644 --- a/compiler/natives/src/strings/strings_test.go +++ b/compiler/natives/src/strings/strings_test.go @@ -18,5 +18,9 @@ func TestCompareStrings(t *testing.T) { } func TestClone(t *testing.T) { - t.Skip("conversion to reflect.StringHeader is not supported in GopherJS") + t.Skip("conversion to unsafe.StringData is not supported in GopherJS") +} + +func TestMap(t *testing.T) { + t.Skip("identity test uses unsafe.StringData is not supported in GopherJS") } diff --git a/compiler/natives/src/sync/atomic/atomic.go b/compiler/natives/src/sync/atomic/atomic.go index 1cbfe65f9..d993f3b80 100644 --- a/compiler/natives/src/sync/atomic/atomic.go +++ b/compiler/natives/src/sync/atomic/atomic.go @@ -221,5 +221,19 @@ func sameType(x, y interface{}) bool { return js.InternalObject(x).Get("constructor") == js.InternalObject(y).Get("constructor") } -//gopherjs:purge for go1.19 without generics +// 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/map.go b/compiler/natives/src/sync/map.go new file mode 100644 index 000000000..3f81b9b31 --- /dev/null +++ b/compiler/natives/src/sync/map.go @@ -0,0 +1,48 @@ +//go:build js +// +build js + +package sync + +type Map struct { + mu Mutex + + // replaced atomic.Pointer[readOnly] for go1.20 without generics. + read atomicReadOnlyPointer + + dirty map[any]*entry + misses int +} + +type atomicReadOnlyPointer struct { + v *readOnly +} + +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/sync/sync.go b/compiler/natives/src/sync/sync.go index 294b0b109..b37a9e9f9 100644 --- a/compiler/natives/src/sync/sync.go +++ b/compiler/natives/src/sync/sync.go @@ -46,6 +46,14 @@ func runtime_SemacquireMutex(s *uint32, lifo bool, skipframes int) { *s-- } +func runtime_SemacquireRWMutexR(s *uint32, lifo bool, skipframes int) { + runtime_SemacquireMutex(s, lifo, skipframes) +} + +func runtime_SemacquireRWMutex(s *uint32, lifo bool, skipframes int) { + runtime_SemacquireMutex(s, lifo, skipframes) +} + func runtime_Semrelease(s *uint32, handoff bool, skipframes int) { // TODO: Use handoff if needed/possible. *s++ diff --git a/compiler/natives/src/syscall/syscall_js_wasm.go b/compiler/natives/src/syscall/syscall_js_wasm.go index 5bcbdeed4..346da22d4 100644 --- a/compiler/natives/src/syscall/syscall_js_wasm.go +++ b/compiler/natives/src/syscall/syscall_js_wasm.go @@ -2,6 +2,7 @@ package syscall import ( "syscall/js" + _ "unsafe" // go:linkname ) func runtime_envs() []string { @@ -22,12 +23,21 @@ func runtime_envs() []string { return envs } +func runtimeSetenv(k, v string) { + setenv_c(k, v) +} + +func runtimeUnsetenv(k string) { + unsetenv_c(k) +} + func setenv_c(k, v string) { process := js.Global().Get("process") if process.IsUndefined() { return } process.Get("env").Set(k, v) + godebug_notify(k, v) } func unsetenv_c(k string) { @@ -36,8 +46,12 @@ func unsetenv_c(k string) { return } process.Get("env").Delete(k) + godebug_notify(k, ``) } +//go:linkname godebug_notify runtime.godebug_notify +func godebug_notify(key, value string) + func setStat(st *Stat_t, jsSt js.Value) { // This method is an almost-exact copy of upstream, except for 4 places where // time stamps are obtained as floats in lieu of int64. Upstream wasm emulates diff --git a/compiler/natives/src/testing/example.go b/compiler/natives/src/testing/example.go index bf8d06482..b80ae2e99 100644 --- a/compiler/natives/src/testing/example.go +++ b/compiler/natives/src/testing/example.go @@ -6,12 +6,11 @@ package testing import ( "fmt" "os" - "strings" "time" ) func runExample(eg InternalExample) (ok bool) { - if *chatty { + if chatty.on { fmt.Printf("=== RUN %s\n", eg.Name) } @@ -24,12 +23,12 @@ func runExample(eg InternalExample) (ok bool) { } os.Stdout = w + finished := false start := time.Now() - ok = true // Clean up in a deferred call so we can recover if the example panics. defer func() { - dstr := fmtDuration(time.Now().Sub(start)) + timeSpent := time.Since(start) // Close file, restore stdout, get output. w.Close() @@ -41,31 +40,12 @@ func runExample(eg InternalExample) (ok bool) { os.Exit(1) } - var fail string err := recover() - got := strings.TrimSpace(string(out)) - want := strings.TrimSpace(eg.Output) - if eg.Unordered { - if sortLines(got) != sortLines(want) && err == nil { - fail = fmt.Sprintf("got:\n%s\nwant (unordered):\n%s\n", string(out), eg.Output) - } - } else { - if got != want && err == nil { - fail = fmt.Sprintf("got:\n%s\nwant:\n%s\n", got, want) - } - } - if fail != "" || err != nil { - fmt.Printf("--- FAIL: %s (%s)\n%s", eg.Name, dstr, fail) - ok = false - } else if *chatty { - fmt.Printf("--- PASS: %s (%s)\n", eg.Name, dstr) - } - if err != nil { - panic(err) - } + ok = eg.processRunResult(string(out), timeSpent, finished, err) }() // Run example. eg.F() + finished = true return } diff --git a/compiler/natives/src/time/export_test.go b/compiler/natives/src/time/export_test.go new file mode 100644 index 000000000..5cd3fc6ab --- /dev/null +++ b/compiler/natives/src/time/export_test.go @@ -0,0 +1,9 @@ +//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 new file mode 100644 index 000000000..0e1594c19 --- /dev/null +++ b/compiler/natives/src/time/format.go @@ -0,0 +1,79 @@ +//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 new file mode 100644 index 000000000..7c69bfc95 --- /dev/null +++ b/compiler/natives/src/time/format_rfc3339.go @@ -0,0 +1,85 @@ +//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/natives/src/golang.org/x/crypto/internal/alias/alias.go b/compiler/natives/src/vendor/golang.org/x/crypto/internal/alias/alias.go similarity index 82% rename from compiler/natives/src/golang.org/x/crypto/internal/alias/alias.go rename to compiler/natives/src/vendor/golang.org/x/crypto/internal/alias/alias.go index a3e1e7f79..e6bb87536 100644 --- a/compiler/natives/src/golang.org/x/crypto/internal/alias/alias.go +++ b/compiler/natives/src/vendor/golang.org/x/crypto/internal/alias/alias.go @@ -4,9 +4,8 @@ package alias // This file duplicated is these two locations: -// - src/crypto/internal/subtle/aliasing.go -// - src/golang.org/x/crypto/internal/subtle/aliasing.go -// - src/golang.org/x/crypto/internal/alias/alias.go +// - src/crypto/internal/alias/ +// - src/golang.org/x/crypto/internal/alias/ import "github.com/gopherjs/gopherjs/js" diff --git a/compiler/prelude/prelude.js b/compiler/prelude/prelude.js index d35de6b01..85db83727 100644 --- a/compiler/prelude/prelude.js +++ b/compiler/prelude/prelude.js @@ -185,7 +185,7 @@ var $sliceToNativeArray = slice => { return slice.$array.slice(slice.$offset, slice.$offset + slice.$length); }; -// Convert Go slice to a pointer to an underlying Go array. +// Convert Go slice to a pointer to an underlying Go array, `[]T -> *[N]T`. // // Note that an array pointer can be represented by an "unwrapped" native array // type, and it will be wrapped back into its Go type when necessary. @@ -428,39 +428,74 @@ var $appendSlice = (slice, toAppend) => { return $internalAppend(slice, toAppend.$array, toAppend.$offset, toAppend.$length); }; +// Internal helper function for appending to a slice. +// The given slice will not be modified. +// +// If no values are being appended, the original slice will be returned. +// Otherwise, a new slice will be created with the appended values. +// +// If the underlying array has enough capacity, it will be used. +// Otherwise, a new array will be allocated with enough capacity to hold +// the new values and the original array will not be modified. var $internalAppend = (slice, array, offset, length) => { if (length === 0) { return slice; } - var newArray = slice.$array; - var newOffset = slice.$offset; - var newLength = slice.$length + length; - var newCapacity = slice.$capacity; + let newLength = slice.$length + length; + let newSlice = $growSlice(slice, newLength); + + let newArray = newSlice.$array; + $copyArray(newArray, array, newSlice.$offset + newSlice.$length, offset, length, newSlice.constructor.elem); + + newSlice.$length = newLength; + return newSlice; +}; - if (newLength > newCapacity) { - newOffset = 0; - newCapacity = Math.max(newLength, slice.$capacity < 1024 ? slice.$capacity * 2 : Math.floor(slice.$capacity * 5 / 4)); +// Calculates the new capacity for a slice that is expected to grow to at least +// the given minCapacity. This follows the Go runtime's growth strategy. +// The oldCapacity is the current capacity of the slice that is being grown. +const $calculateNewCapacity = (minCapacity, oldCapacity) => { + return Math.max(minCapacity, oldCapacity < 1024 ? oldCapacity * 2 : Math.floor(oldCapacity * 5 / 4)); +}; - if (slice.$array.constructor === Array) { - newArray = slice.$array.slice(slice.$offset, slice.$offset + slice.$length); - newArray.length = newCapacity; - var zero = slice.constructor.elem.zero; - for (var i = slice.$length; i < newCapacity; i++) { +// Potentially grows the slice to have a capacity of at least minCapacity. +// +// A new slice will always be returned, even if the given slice had the required capacity. +// If the slice didn't have enough capacity, the new slice will have a +// new array created for it with the required minimum capacity. +// +// This takes the place of the growSlice function in the reflect package. +var $growSlice = (slice, minCapacity) => { + let array = slice.$array; + let offset = slice.$offset; + const length = slice.$length; + let capacity = slice.$capacity; + + if (minCapacity > capacity) { + capacity = $calculateNewCapacity(minCapacity, capacity); + + let newArray; + if (array.constructor === Array) { + newArray = array.slice(offset, offset + length); + newArray.length = capacity; + const zero = slice.constructor.elem.zero; + for (let i = slice.$length; i < capacity; i++) { newArray[i] = zero(); } } else { - newArray = new slice.$array.constructor(newCapacity); - newArray.set(slice.$array.subarray(slice.$offset, slice.$offset + slice.$length)); + newArray = new array.constructor(capacity); + newArray.set(array.subarray(offset, offset + length)); } - } - $copyArray(newArray, array, newOffset + slice.$length, offset, length, slice.constructor.elem); + array = newArray; + offset = 0; + } - var newSlice = new slice.constructor(newArray); - newSlice.$offset = newOffset; - newSlice.$length = newLength; - newSlice.$capacity = newCapacity; + let newSlice = new slice.constructor(array); + newSlice.$offset = offset; + newSlice.$length = length; + newSlice.$capacity = capacity; return newSlice; }; @@ -569,3 +604,10 @@ var $instanceOf = (x, y) => { var $typeOf = x => { return typeof (x); }; + +var $sliceData = (slice, typ) => { + if (slice === typ.nil) { + return $ptrType(typ.elem).nil; + } + return $indexPtr(slice.$array, slice.$offset, typ.elem); +}; diff --git a/compiler/prelude/types.js b/compiler/prelude/types.js index 9570b2fed..a64071985 100644 --- a/compiler/prelude/types.js +++ b/compiler/prelude/types.js @@ -150,7 +150,16 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { }), "$"); }; typ.copy = (dst, src) => { - $copyArray(dst, src, 0, 0, src.length, elem); + if (src.length === undefined) { + // copy from a slice, the slice may be bigger but not smaller than the array + if (src.$length < dst.length) { + $throwRuntimeError("cannot convert slice with length "+src.$length+" to array or pointer to array with length "+dst.length); + } + $copyArray(dst, src.$array, 0, 0, dst.length, elem); + } else { + // copy from another array + $copyArray(dst, src, 0, 0, src.length, elem); + } }; typ.ptr.init(typ); Object.defineProperty(typ.ptr.nil, "nilCheck", { get: $throwNilPointerError }); @@ -231,6 +240,7 @@ var $newType = (size, kind, string, named, pkg, exported, constructor) => { typ.comparable = false; typ.nativeArray = $nativeArray(elem.kind); typ.nil = new typ([]); + Object.freeze(typ.nil); }; break; diff --git a/compiler/version_check.go b/compiler/version_check.go index d672fa45a..4186a0d8b 100644 --- a/compiler/version_check.go +++ b/compiler/version_check.go @@ -1,4 +1,4 @@ -//go:build go1.19 +//go:build go1.20 package compiler @@ -12,10 +12,10 @@ import ( ) // Version is the GopherJS compiler version string. -const Version = "1.19.0-beta1+go1.19.13" +const Version = "1.20.0-beta1+go1.20.14" // GoVersion is the current Go 1.x version that GopherJS is compatible with. -const GoVersion = 19 +const GoVersion = 20 // CheckGoVersion checks the version of the Go distribution // at goroot, and reports an error if it's not compatible @@ -49,7 +49,7 @@ func goRootVersion(goroot string) (string, error) { if err != nil { return "", fmt.Errorf("`go version` command failed: %w", err) } - // Expected output: go version go1.18.1 linux/amd64 + // Expected output: go version go1.20.14 linux/amd64 parts := strings.Split(string(out), " ") if len(parts) != 4 { return "", fmt.Errorf("unexpected `go version` output %q, expected 4 words", string(out)) diff --git a/go.mod b/go.mod index ccb130f48..4ace51ddf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/gopherjs/gopherjs -go 1.18 +go 1.20 require ( github.com/evanw/esbuild v0.18.0 diff --git a/tests/arrays_test.go b/tests/arrays_test.go index e79989991..12449db03 100644 --- a/tests/arrays_test.go +++ b/tests/arrays_test.go @@ -1,6 +1,7 @@ package tests import ( + "fmt" "reflect" "testing" "unsafe" @@ -83,3 +84,151 @@ func TestReflectArraySize(t *testing.T) { t.Errorf("array type size gave %v, want %v", got, want) } } + +func TestNilPrototypeNotModifiedByPointer(t *testing.T) { + const growth = 3 + + s1 := []int(nil) + p1 := &s1 + *p1 = make([]int, 0, growth) + if c := cap(s1); c != growth { + t.Errorf(`expected capacity of nil to increase to %d, got %d`, growth, c) + println("s1:", s1) + } + + s2 := []int(nil) + if c := cap(s2); c != 0 { + t.Errorf(`the capacity of nil must always be zero, it was %d`, c) + println("s1:", s1) + println("s2:", s2) + } +} + +func TestNilPrototypeNotModifiedByReflectGrow(t *testing.T) { + const growth = 3 + + s1 := []int(nil) + v1 := reflect.ValueOf(&s1).Elem() + v1.Grow(growth) + if c := cap(s1); c != growth { + t.Errorf(`expected capacity of nil to increase to %d, got %d`, growth, c) + println("s1:", s1) + } + + s2 := []int(nil) + if c := cap(s2); c != 0 { + t.Errorf(`the capacity of nil must always be zero, it was %d`, c) + println("s1:", s1) + println("s2:", s2) + } +} + +func TestConversionFromSliceToArray(t *testing.T) { + t.Run(`nil byte slice to zero byte array`, func(t *testing.T) { + s := []byte(nil) + _ = [0]byte(s) // should not have runtime panic + }) + + t.Run(`empty byte slice to zero byte array`, func(t *testing.T) { + s := []byte{} + _ = [0]byte(s) // should not have runtime panic + }) + + t.Run(`3 byte slice to 3 byte array`, func(t *testing.T) { + s := []byte{12, 34, 56} + a := [3]byte(s) + if s[0] != a[0] || s[1] != a[1] || s[2] != a[2] { + t.Errorf("slice and array are not equal after conversion:\n\tslice: %#v\n\tarray: %#v", s, a) + } + }) + + t.Run(`4 byte slice to 4 byte array`, func(t *testing.T) { + s := []byte{12, 34, 56, 78} + a := [4]byte(s) + if s[0] != a[0] || s[1] != a[1] || s[2] != a[2] || s[3] != a[3] { + t.Errorf("slice and array are not equal after conversion:\n\tslice: %#v\n\tarray: %#v", s, a) + } + }) + + t.Run(`5 byte slice to 5 byte array`, func(t *testing.T) { + s := []byte{12, 34, 56, 78, 90} + a := [5]byte(s) + if s[0] != a[0] || s[1] != a[1] || s[2] != a[2] || s[3] != a[3] || s[4] != a[4] { + t.Errorf("slice and array are not equal after conversion:\n\tslice: %#v\n\tarray: %#v", s, a) + } + }) + + t.Run(`larger 5 byte slice to smaller 4 byte array`, func(t *testing.T) { + s := []byte{12, 34, 56, 78, 90} + a := [4]byte(s) + if s[0] != a[0] || s[1] != a[1] || s[2] != a[2] || s[3] != a[3] { + t.Errorf("slice and array are not equal after conversion:\n\tslice: %#v\n\tarray: %#v", s, a) + } + }) + + t.Run(`larger 4 byte slice to smaller zero byte array`, func(t *testing.T) { + s := []byte{12, 34, 56, 78} + _ = [0]byte(s) // should not have runtime panic + }) + + t.Run(`smaller 3 byte slice to larger 4 byte array`, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + err := fmt.Sprintf(`%v`, r) + exp := `runtime error: cannot convert slice with length 3 to array or pointer to array with length 4` + if err != exp { + t.Error(`unexpected panic message:`, r) + t.Log("\texpected:", exp) + } + } + }() + + s := []byte{12, 34, 56} + a := [4]byte(s) + t.Errorf("expected a runtime panic:\n\tslice: %#v\n\tarray: %#v", s, a) + }) + + t.Run(`nil byte slice to 5 byte array`, func(t *testing.T) { + defer func() { + if r := recover(); r != nil { + err := fmt.Sprintf(`%v`, r) + exp := `runtime error: cannot convert slice with length 0 to array or pointer to array with length 5` + if err != exp { + t.Error(`unexpected panic message:`, r) + t.Log("\texpected:", exp) + } + } + }() + + s := []byte(nil) + a := [5]byte(s) + t.Errorf("expected a runtime panic:\n\tslice: %#v\n\tarray: %#v", s, a) + }) + + type Cat struct { + name string + age int + } + cats := []Cat{ + {name: "Tom", age: 3}, + {name: "Jonesy", age: 5}, + {name: "Sylvester", age: 7}, + {name: "Rita", age: 2}, + } + + t.Run(`4 Cat slice to 4 Cat array`, func(t *testing.T) { + s := cats + a := [4]Cat(s) + if s[0] != a[0] || s[1] != a[1] || s[2] != a[2] || s[3] != a[3] { + t.Errorf("slice and array are not equal after conversion:\n\tslice: %#v\n\tarray: %#v", s, a) + } + }) + + t.Run(`4 *Cat slice to 4 *Cat array`, func(t *testing.T) { + s := []*Cat{&cats[0], &cats[1], &cats[2], &cats[3]} + a := [4]*Cat(s) + if s[0] != a[0] || s[1] != a[1] || s[2] != a[2] || s[3] != a[3] { + t.Errorf("slice and array are not equal after conversion:\n\tslice: %#v\n\tarray: %#v", s, a) + } + }) +} diff --git a/tests/gorepo/run.go b/tests/gorepo/run.go index 6720f50d7..203d33205 100644 --- a/tests/gorepo/run.go +++ b/tests/gorepo/run.go @@ -60,7 +60,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue11656.go": {desc: "Error: Native function not implemented: runtime/debug.setPanicOnFault"}, "fixedbugs/issue4085b.go": {desc: "Error: got panic JavaScript error: Invalid typed array length, want len out of range"}, "fixedbugs/issue4316.go": {desc: "Error: runtime error: invalid memory address or nil pointer dereference"}, - "fixedbugs/issue4388.go": {desc: "Error: expected :1 have anonymous function:0"}, "fixedbugs/issue4562.go": {desc: "Error: cannot find issue4562.go on stack"}, "fixedbugs/issue4620.go": {desc: "map[0:1 1:2], Error: m[i] != 2"}, "fixedbugs/issue5856.go": {category: requiresSourceMapSupport}, @@ -69,17 +68,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue7690.go": {desc: "Error: runtime error: slice bounds out of range"}, "fixedbugs/issue8047b.go": {desc: "Error: [object Object]"}, - // Failing due to use of os/exec.Command, which is unsupported. Now skipped via !nacl build tag. - /*"fixedbugs/bug248.go": {desc: "os/exec.Command unsupported"}, - "fixedbugs/bug302.go": {desc: "os/exec.Command unsupported"}, - "fixedbugs/bug345.go": {desc: "os/exec.Command unsupported"}, - "fixedbugs/bug369.go": {desc: "os/exec.Command unsupported"}, - "fixedbugs/bug429_run.go": {desc: "os/exec.Command unsupported"}, - "fixedbugs/issue9862_run.go": {desc: "os/exec.Command unsupported"},*/ - "fixedbugs/issue10607.go": {desc: "os/exec.Command unsupported"}, - "fixedbugs/issue13268.go": {desc: "os/exec.Command unsupported"}, - "fixedbugs/issue14636.go": {desc: "os/exec.Command unsupported"}, - // These are new tests in Go 1.7. "fixedbugs/issue14646.go": {category: unsureIfGopherJSSupportsThisFeature, desc: "tests runtime.Caller behavior in a deferred func in SSA backend... does GopherJS even support runtime.Caller?"}, "fixedbugs/issue15039.go": {desc: "valid bug but deal with after Go 1.7 support is out? it's likely not a regression"}, @@ -100,15 +88,13 @@ var knownFails = map[string]failReason{ // These are new tests in Go 1.10. "fixedbugs/issue21879.go": {desc: "incorrect output related to runtime.Callers, runtime.CallersFrames, etc."}, "fixedbugs/issue21887.go": {desc: "incorrect output (although within spec, not worth fixing) for println(^uint64(0)). got: { '$high': 4294967295, '$low': 4294967295, '$val': [Circular] } want: 18446744073709551615"}, - "fixedbugs/issue22660.go": {category: notApplicable, desc: "test of gc compiler, uses os/exec.Command"}, "fixedbugs/issue23305.go": {desc: "GopherJS fails to compile println(0xffffffff), maybe because 32-bit arch"}, // These are new tests in Go 1.11. - "fixedbugs/issue21221.go": {category: usesUnsupportedPackage, desc: "uses unsafe package and compares nil pointers"}, - "fixedbugs/issue22662.go": {desc: "line directives not fully working. Error: got /private/var/folders/b8/66r1c5856mqds1mrf2tjtq8w0000gn/T:1; want ??:1"}, - "fixedbugs/issue22662b.go": {category: usesUnsupportedPackage, desc: "os/exec.Command unsupported"}, - "fixedbugs/issue23188.go": {desc: "incorrect order of evaluation of index operations"}, - "fixedbugs/issue24547.go": {desc: "incorrect computing method sets with shadowed methods"}, + "fixedbugs/issue21221.go": {category: usesUnsupportedPackage, desc: "uses unsafe package and compares nil pointers"}, + "fixedbugs/issue22662.go": {desc: "line directives not fully working. Error: got /private/var/folders/b8/66r1c5856mqds1mrf2tjtq8w0000gn/T:1; want ??:1"}, + "fixedbugs/issue23188.go": {desc: "incorrect order of evaluation of index operations"}, + "fixedbugs/issue24547.go": {desc: "incorrect computing method sets with shadowed methods"}, // These are new tests in Go 1.12. "fixedbugs/issue23837.go": {desc: "missing panic on nil pointer-to-empty-struct dereference"}, @@ -150,7 +136,6 @@ var knownFails = map[string]failReason{ "fixedbugs/issue48898.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1128"}, "fixedbugs/issue53600.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, "typeparam/chans.go": {category: neverTerminates, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, - "typeparam/issue51733.go": {category: usesUnsupportedPackage, desc: "unsafe: uintptr to struct pointer conversion is unsupported"}, "typeparam/typeswitch5.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format is different from Go's"}, // Failures related to the lack of generics support. Ideally, this section @@ -162,6 +147,13 @@ var knownFails = map[string]failReason{ "typeparam/issue51521.go": {category: lowLevelRuntimeDifference, desc: "different panic message when calling a method on nil interface"}, "fixedbugs/issue50672.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1271"}, "fixedbugs/issue53653.go": {category: lowLevelRuntimeDifference, desc: "GopherJS println format of int64 is different from Go's"}, + + // These are new tests in Go 1.20 + "fixedbugs/issue25897a.go": {category: neverTerminates, desc: "does for { runtime.GC() }"}, + "fixedbugs/issue54343.go": {category: notApplicable, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, + "fixedbugs/issue57823.go": {category: notApplicable, desc: "uses runtime.SetFinalizer() and runtime.GC()."}, + "fixedbugs/issue59293.go": {category: usesUnsupportedPackage, desc: "uses unsafe.SliceData() and unsafe.StringData()."}, + "fixedbugs/issue43942.go": {category: other, desc: "https://github.com/gopherjs/gopherjs/issues/1126"}, } type failCategory uint8 @@ -568,6 +560,8 @@ func (ctxt *context) match(name string) bool { func init() { checkShouldTest() } +var errTimeout = errors.New("command exceeded time limit") + // run runs a test. func (t *test) run() { start := time.Now() @@ -619,6 +613,7 @@ func (t *test) run() { } var args, flags []string + var tim int wantError := false f, err := splitQuoted(action) if err != nil { @@ -652,14 +647,6 @@ func (t *test) run() { case "errorcheck", "errorcheckdir", "errorcheckoutput": t.action = action wantError = true - for len(args) > 0 && strings.HasPrefix(args[0], "-") { - if args[0] == "-0" { - wantError = false - } else { - flags = append(flags, args[0]) - } - args = args[1:] - } case "skip": t.action = "skip" return @@ -669,6 +656,38 @@ func (t *test) run() { return } + // collect flags + for len(args) > 0 && strings.HasPrefix(args[0], "-") { + switch args[0] { + case "-1": + wantError = true + case "-0": + wantError = false + case "-s": + // GOPHERJS: Doesn't use singlefilepkgs in test yet. + case "-t": // timeout in seconds + args = args[1:] + var err error + tim, err = strconv.Atoi(args[0]) + if err != nil { + t.err = fmt.Errorf("need number of seconds for -t timeout, got %s instead", args[0]) + } + if s := os.Getenv("GO_TEST_TIMEOUT_SCALE"); s != "" { + timeoutScale, err := strconv.Atoi(s) + if err != nil { + log.Fatalf("failed to parse $GO_TEST_TIMEOUT_SCALE = %q as integer: %v", s, err) + } + tim *= timeoutScale + } + case "-goexperiment": // set GOEXPERIMENT environment + args = args[1:] + // GOPHERJS: Ignore GOEXPERIMENT for now + default: + flags = append(flags, args[0]) + } + args = args[1:] + } + t.makeTempDir() defer os.RemoveAll(t.tempDir) @@ -706,8 +725,40 @@ func (t *test) run() { cmd.Dir = t.tempDir cmd.Env = envForDir(cmd.Dir) } - err := cmd.Run() - if err != nil { + + var err error + if tim != 0 { + err = cmd.Start() + // This command-timeout code adapted from cmd/go/test.go + // Note: the Go command uses a more sophisticated timeout + // strategy, first sending SIGQUIT (if appropriate for the + // OS in question) to try to trigger a stack trace, then + // finally much later SIGKILL. If timeouts prove to be a + // common problem here, it would be worth porting over + // that code as well. See https://do.dev/issue/50973 + // for more discussion. + if err == nil { + tick := time.NewTimer(time.Duration(tim) * time.Second) + done := make(chan error) + go func() { + done <- cmd.Wait() + }() + select { + case err = <-done: + // ok + case <-tick.C: + cmd.Process.Signal(os.Interrupt) + time.Sleep(1 * time.Second) + cmd.Process.Kill() + <-done + err = errTimeout + } + tick.Stop() + } + } else { + err = cmd.Run() + } + if err != nil && err != errTimeout { err = fmt.Errorf("%s\n%s", err, buf.Bytes()) } return buf.Bytes(), err @@ -728,6 +779,10 @@ func (t *test) run() { t.err = fmt.Errorf("compilation succeeded unexpectedly\n%s", out) return } + if err == errTimeout { + t.err = fmt.Errorf("compilation timed out") + return + } } else { if err != nil { t.err = err diff --git a/tests/js_test.go b/tests/js_test.go index 6f6eaa542..2d67fb99a 100644 --- a/tests/js_test.go +++ b/tests/js_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" "time" + "unsafe" "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/js" @@ -975,3 +976,41 @@ func TestStructWithNonIdentifierJSTag(t *testing.T) { t.Errorf("value via js.Object.Get gave %q, want %q", got, want) } } + +func TestSliceData(t *testing.T) { + var ( + s0 = []int(nil) + s1 = []int{} + s2 = []int{1, 2, 3} + s3 = s2[1:] + s4 = []int{4, 5, 6} + + sd0 = unsafe.SliceData(s0) + sd1 = unsafe.SliceData(s1) + sd2 = unsafe.SliceData(s2) + sd3 = unsafe.SliceData(s3) + sd4 = unsafe.SliceData(s4) + ) + + if sd0 != nil { + t.Errorf("slice data for nil slice was not nil") + } + if sd1 == nil { + t.Errorf("slice data for empty slice was nil") + } + if sd2 == nil { + t.Errorf("slice data for non-empty slice was nil") + } + if sd3 == nil { + t.Errorf("slice data for sub-slice was nil") + } + if sd1 == sd2 { + t.Errorf("slice data for empty and non-empty slices were the same") + } + if sd2 == sd3 { + t.Errorf("slice data for slice and sub-slice were the same") + } + if sd2 == sd4 { + t.Errorf("slice data for different slices were the same") + } +} diff --git a/tests/runtime_test.go b/tests/runtime_test.go index 12f0b34c3..80c7cce58 100644 --- a/tests/runtime_test.go +++ b/tests/runtime_test.go @@ -5,7 +5,10 @@ package tests import ( "fmt" "runtime" + "strconv" + "strings" "testing" + _ "unsafe" "github.com/google/go-cmp/cmp" "github.com/gopherjs/gopherjs/js" @@ -160,3 +163,42 @@ func TestCallers(t *testing.T) { panic("panic") }) } + +// Need this to tunnel into `internal/godebug` and run a test +// without causing a dependency cycle with the `testing` package. +// +//go:linkname godebug_setUpdate runtime.godebug_setUpdate +func godebug_setUpdate(update func(string, string)) + +func Test_GoDebugInjection(t *testing.T) { + buf := []string{} + update := func(def, env string) { + if def != `` { + t.Errorf(`Expected the default value to be empty but got %q`, def) + } + buf = append(buf, strconv.Quote(env)) + } + check := func(want string) { + if got := strings.Join(buf, `, `); got != want { + t.Errorf(`Unexpected result: got: %q, want: %q`, got, want) + } + buf = buf[:0] + } + + // Call it multiple times to ensure that the watcher is only injected once. + // Each one of these calls should emit an update first, then when GODEBUG is set. + godebug_setUpdate(update) + godebug_setUpdate(update) + check(`"", ""`) // two empty strings for initial update calls. + + t.Setenv(`GODEBUG`, `gopherJSTest=ben`) + check(`"gopherJSTest=ben"`) // must only be once for update for new value. + + godebug_setUpdate(update) + check(`"gopherJSTest=ben"`) // must only be once for initial update with already set value. + + t.Setenv(`GODEBUG`, `gopherJSTest=tom`) + t.Setenv(`GODEBUG`, `gopherJSTest=sam`) + t.Setenv(`NOT_GODEBUG`, `gopherJSTest=bob`) + check(`"gopherJSTest=tom", "gopherJSTest=sam"`) +} diff --git a/tool.go b/tool.go index 06483e96b..9c467d04f 100644 --- a/tool.go +++ b/tool.go @@ -221,7 +221,11 @@ func main() { } if pkg.IsCommand() && !pkg.UpToDate { - if err := s.WriteCommandPackage(archive, pkg.InstallPath()); err != nil { + pkgObj, err := pkg.InstallPath() + if err != nil { + return err + } + if err := s.WriteCommandPackage(archive, pkgObj); err != nil { return err } }