Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions internal/build/plan9asm.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ import (
// NOTE: golang.org/x/tools/go/packages.Package does not expose SFiles, so we
// query `go list -json` here to get the exact filtered set for GOOS/GOARCH.
func compilePkgSFiles(ctx *context, aPkg *aPackage, pkg *packages.Package, verbose bool) ([]string, error) {
if llruntime.SourcePatchReplacesAsmForGOARCH(pkg.PkgPath, ctx.buildConf.Goarch) {
return nil, nil
}
sfiles, err := pkgSFiles(ctx, pkg)
if err != nil {
return nil, err
Expand Down
22 changes: 22 additions & 0 deletions internal/build/source_patch.go
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,28 @@ func applySourcePatchForPkg(base, current map[string][]byte, runtimeDir, goroot,
}
}

if llruntime.SourcePatchReplacesAsmForGOARCH(pkgPath, ctx.goarch) {
for _, entry := range srcEntries {
if entry.IsDir() {
continue
}
name := entry.Name()
if !strings.HasSuffix(name, ".s") || strings.HasSuffix(name, "_test.s") {
continue
}
match, err := buildCtx.MatchFile(srcDir, name)
if err != nil {
return false, nil, fmt.Errorf("match stdlib assembly file %s: %w", filepath.Join(srcDir, name), err)
}
if !match {
continue
}
ensureOverlay()
out[filepath.Join(srcDir, name)] = []byte("// replaced by LLGo source patch\n")
changed = true
}
}

if skipAll {
for _, entry := range srcEntries {
if entry.IsDir() {
Expand Down
132 changes: 132 additions & 0 deletions internal/build/source_patch_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"go/ast"
"go/parser"
"go/token"
"go/types"
"io/fs"
"os"
"path/filepath"
Expand All @@ -13,9 +14,140 @@ import (
"testing"

"github.com/goplus/llgo/internal/env"
"github.com/goplus/llgo/internal/packages"
llruntime "github.com/goplus/llgo/runtime"
)

func TestWasmRuntimeSourcePatchTypeChecks(t *testing.T) {
for _, goos := range []string{"js", "wasip1"} {
t.Run(goos, func(t *testing.T) {
cfgEnv := append(os.Environ(), "GOOS="+goos, "GOARCH=wasm")
goroot, goversion, err := env.GOROOTAndGOVERSIONWithEnv(cfgEnv)
if err != nil {
t.Fatal(err)
}
overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), goroot, sourcePatchBuildContext{
goos: goos,
goarch: "wasm",
goversion: goversion,
})
if err != nil {
t.Fatal(err)
}

pkgs, err := packages.LoadEx(nil, func(types.Sizes, string, string) types.Sizes {
return &types.StdSizes{WordSize: 4, MaxAlign: 4}
}, &packages.Config{
Mode: loadSyntax | packages.NeedDeps | packages.NeedModule | packages.NeedExportFile,
Env: cfgEnv,
Fset: token.NewFileSet(),
Overlay: overlay,
}, "runtime")
if err != nil {
t.Fatal(err)
}
if len(pkgs) != 1 {
t.Fatalf("loaded %d runtime packages, want 1", len(pkgs))
}
if pkgs[0].IllTyped {
logPackageErrors(t, pkgs[0], make(map[string]bool))
t.Fatal("runtime did not type-check with wasm32 sizes")
}
})
}
}

func logPackageErrors(t *testing.T, pkg *packages.Package, seen map[string]bool) {
t.Helper()
if pkg == nil || seen[pkg.ID] {
return
}
seen[pkg.ID] = true
for _, err := range pkg.Errors {
t.Log(err)
}
for _, imported := range pkg.Imports {
if imported.IllTyped {
logPackageErrors(t, imported, seen)
}
}
}

func TestWasmBytealgSourcePatchReplacesAsm(t *testing.T) {
for _, pkgPath := range []string{"internal/bytealg", "internal/chacha8rand", "internal/runtime/atomic"} {
if !llruntime.HasSourcePatchPkg(pkgPath) {
t.Fatalf("%s should be registered as a source patch package", pkgPath)
}
if !llruntime.SourcePatchReplacesAsmForGOARCH(pkgPath, "wasm") {
t.Fatalf("%s wasm assembly should be replaced by its source patch", pkgPath)
}
if llruntime.SourcePatchReplacesAsmForGOARCH(pkgPath, "arm64") {
t.Fatalf("%s native assembly should remain enabled", pkgPath)
}
}

overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), runtime.GOROOT(), sourcePatchBuildContext{
goos: "js",
goarch: "wasm",
goversion: runtime.Version(),
})
if err != nil {
t.Fatal(err)
}
for _, file := range []string{
"internal/bytealg/compare_wasm.s",
"internal/bytealg/equal_wasm.s",
"internal/bytealg/indexbyte_wasm.s",
"internal/chacha8rand/chacha8_stub.s",
"internal/runtime/atomic/atomic_wasm.s",
} {
path := filepath.Join(runtime.GOROOT(), "src", filepath.FromSlash(file))
if got := string(overlay[path]); got != "// replaced by LLGo source patch\n" {
t.Fatalf("overlay[%q] = %q, want assembly replacement", path, got)
}
}
}

func TestCompilePkgSFilesSkipsSourcePatchedAssembly(t *testing.T) {
got, err := compilePkgSFiles(
&context{buildConf: &Config{Goarch: "wasm"}},
nil,
&packages.Package{PkgPath: "internal/bytealg"},
false,
)
if err != nil {
t.Fatal(err)
}
if got != nil {
t.Fatalf("compilePkgSFiles returned %v, want no object files", got)
}
}

func TestSourcePatchAssemblyMatchError(t *testing.T) {
goroot := t.TempDir()
runtimeDir := t.TempDir()
const pkgPath = "internal/bytealg"
srcDir := filepath.Join(goroot, "src", filepath.FromSlash(pkgPath))
patchDir := filepath.Join(runtimeDir, "_patch", filepath.FromSlash(pkgPath))

if err := os.MkdirAll(filepath.Join(srcDir, "adir"), 0755); err != nil {
t.Fatal(err)
}
mustWriteFile(t, filepath.Join(srcDir, "bad_wasm.s"), "//go:build (\n")
mustWriteFile(t, filepath.Join(patchDir, "bytealg_wasm.go"), `//go:build wasm

package bytealg
`)

_, _, err := applySourcePatchForPkg(nil, nil, runtimeDir, goroot, pkgPath, sourcePatchBuildContext{
goos: "js",
goarch: "wasm",
})
if err == nil || !strings.Contains(err.Error(), "match stdlib assembly file") {
t.Fatalf("applySourcePatchForPkg error = %v, want assembly match error", err)
}
}

func TestBuildSourcePatchOverlayForIter(t *testing.T) {
overlay, err := buildSourcePatchOverlayForGOROOT(nil, env.LLGoRuntimeDir(), runtime.GOROOT(), sourcePatchBuildContext{})
if err != nil {
Expand Down
76 changes: 76 additions & 0 deletions runtime/_patch/internal/bytealg/bytealg_wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
// Copyright 2018 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build wasm

package bytealg

import _ "unsafe" // for go:linkname

func Compare(a, b []byte) int {
l := len(a)
if len(b) < l {
l = len(b)
}
for i := 0; i < l; i++ {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
if len(a) < len(b) {
return -1
}
if len(a) > len(b) {
return 1
}
return 0
}

func CompareString(a, b string) int {
return runtime_cmpstring(a, b)
}

//go:linkname runtime_cmpstring runtime.cmpstring
func runtime_cmpstring(a, b string) int {
l := len(a)
if len(b) < l {
l = len(b)
}
for i := 0; i < l; i++ {
if a[i] < b[i] {
return -1
}
if a[i] > b[i] {
return 1
}
}
if len(a) < len(b) {
return -1
}
if len(a) > len(b) {
return 1
}
return 0
}

func IndexByte(b []byte, c byte) int {
for i, value := range b {
if value == c {
return i
}
}
return -1
}

func IndexByteString(s string, c byte) int {
for i := 0; i < len(s); i++ {
if s[i] == c {
return i
}
}
return -1
}
11 changes: 11 additions & 0 deletions runtime/_patch/internal/chacha8rand/chacha8_wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build wasm

package chacha8rand

func block(seed *[4]uint64, blocks *[32]uint64, counter uint32) {
block_generic(seed, blocks, counter)
}
14 changes: 14 additions & 0 deletions runtime/_patch/internal/runtime/atomic/atomic_wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// Copyright 2020 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build wasm

package atomic

import "unsafe"

// StorepNoWB performs *ptr = val without a write barrier.
func StorepNoWB(ptr unsafe.Pointer, val unsafe.Pointer) {
*(*uintptr)(ptr) = uintptr(val)
}
58 changes: 58 additions & 0 deletions runtime/_patch/runtime/hash32_wasm_pre_go126.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

//go:build wasm && !go1.26

package runtime

import "unsafe"

func memhash32Fallback(p unsafe.Pointer, seed uintptr) uintptr {
a, b := mix32(uint32(seed), uint32(4^hashkey[0]))
t := readUnaligned32(p)
a ^= t
b ^= t
a, b = mix32(a, b)
a, b = mix32(a, b)
return uintptr(a ^ b)
}

func memhash64Fallback(p unsafe.Pointer, seed uintptr) uintptr {
a, b := mix32(uint32(seed), uint32(8^hashkey[0]))
a ^= readUnaligned32(p)
b ^= readUnaligned32(add(p, 4))
a, b = mix32(a, b)
a, b = mix32(a, b)
return uintptr(a ^ b)
}

func memhashFallback(p unsafe.Pointer, seed, s uintptr) uintptr {
a, b := mix32(uint32(seed), uint32(s^hashkey[0]))
if s == 0 {
return uintptr(a ^ b)
}
for ; s > 8; s -= 8 {
a ^= readUnaligned32(p)
b ^= readUnaligned32(add(p, 4))
a, b = mix32(a, b)
p = add(p, 8)
}
if s >= 4 {
a ^= readUnaligned32(p)
b ^= readUnaligned32(add(p, s-4))
} else {
t := uint32(*(*byte)(p))
t |= uint32(*(*byte)(add(p, s>>1))) << 8
t |= uint32(*(*byte)(add(p, s-1))) << 16
b ^= t
}
a, b = mix32(a, b)
a, b = mix32(a, b)
return uintptr(a ^ b)
}

func mix32(a, b uint32) (uint32, uint32) {
c := uint64(a^uint32(hashkey[1])) * uint64(b^uint32(hashkey[2]))
return uint32(c), uint32(c >> 32)
}
11 changes: 11 additions & 0 deletions runtime/_patch/runtime/malloc_wasm.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build wasm && !go1.26

package runtime

// LLGo uses native wasm32 pointers, while the Go wasm port models uintptr as
// 64 bits. Keep the GC heap geometry in the wasm32 address space.
const (
heapAddrBits = 32
maxAlloc = (1 << heapAddrBits) - 1
logHeapArenaBytes = 22
)
11 changes: 11 additions & 0 deletions runtime/_patch/runtime/malloc_wasm_go126.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
//go:build wasm && go1.26

package runtime

// Go 1.26 reduced wasm heap arenas to 512 KiB. LLGo still needs the heap
// address and allocation limits adjusted for native wasm32 pointers.
const (
heapAddrBits = 32
maxAlloc = (1 << heapAddrBits) - 1
logHeapArenaBytes = 19
)
15 changes: 15 additions & 0 deletions runtime/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,9 +82,24 @@ func SourcePatchPkgPaths() []string {
return paths
}

func SourcePatchReplacesAsmForGOARCH(path, goarch string) bool {
goarchs, ok := sourcePatchAsmPkgs[path]
return ok && hasGoarch(goarchs, goarch)
}

var sourcePatchPkgs = map[string]struct{}{
"crypto/internal/constanttime": {},
"internal/bytealg": {},
"internal/chacha8rand": {},
"internal/runtime/atomic": {},
"internal/sync": {},
"iter": {},
"runtime": {},
"runtime/metrics": {},
}

var sourcePatchAsmPkgs = map[string]map[string]struct{}{
"internal/bytealg": {"wasm": {}},
"internal/chacha8rand": {"wasm": {}},
"internal/runtime/atomic": {"wasm": {}},
}
Loading