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
39 changes: 39 additions & 0 deletions .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,42 @@ jobs:

# Run the wasm binary using llgo_wasm
iwasm --stack-size=819200000 --heap-size=800000000 hello.wasm

wasm-runtime:
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
go: ["1.24.2", "1.26.5"]
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- name: Install dependencies
uses: ./.github/actions/setup-deps
with:
llvm-version: 19

- name: Set up Emscripten
uses: emscripten-core/setup-emsdk@v15
with:
version: "4.0.21"

- name: Set up Go for building llgo
uses: ./.github/actions/setup-go

- name: Install llgo
run: |
go install ./...
echo "LLGO_ROOT=$GITHUB_WORKSPACE" >> $GITHUB_ENV

- name: Set up Go for testing
uses: ./.github/actions/setup-go
with:
go-version: ${{matrix.go}}

- name: Build standard runtime for wasm
shell: bash
run: |
GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-js.wasm" ./internal/build/testdata/wasm-runtime
GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/runtime-wasip1.wasm" ./internal/build/testdata/wasm-runtime
file "$RUNNER_TEMP/runtime-js.wasm" "$RUNNER_TEMP/runtime-wasip1.wasm"
14 changes: 13 additions & 1 deletion internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -322,7 +322,7 @@ func Do(args []string, conf *Config) ([]Package, error) {

verbose := conf.Verbose
patterns := args
tags := "llgo,math_big_pure_go,purego"
tags := defaultBuildTags(conf.Goarch, conf.Target)
if conf.PCLNMode == PCLNExternal {
// Select the optional runtime loader as part of the normal package
// cache key. Embedded and none builds do not compile any loader or
Expand Down Expand Up @@ -654,6 +654,18 @@ func applyBuildModeCompileFlags(mode BuildMode, export *crosscompile.Export) {
}
}

func defaultBuildTags(goarch, target string) string {
tags := "llgo,math_big_pure_go,purego"
// Raw GOOS/GOARCH wasm builds do not have a target configuration that
// selects a collector. BDWGC is not available in either wasm host, so use
// the supported collector-free runtime unless a named target supplies its
// own runtime configuration.
if goarch == "wasm" && target == "" {
tags += ",nogc"
}
return tags
}

func allowMissingFunctionBodies(initial []*packages.Package) {
for _, pkg := range initial {
hasMissingBody := false
Expand Down
71 changes: 71 additions & 0 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"debug/macho"
"fmt"
"go/ast"
gobuild "go/build"
"go/parser"
"go/token"
"io"
Expand All @@ -16,11 +17,13 @@ import (
"path/filepath"
"runtime"
"slices"
"strconv"
"strings"
"testing"

"github.com/goplus/llgo/internal/buildenv"
"github.com/goplus/llgo/internal/crosscompile"
"github.com/goplus/llgo/internal/env"
"github.com/goplus/llgo/internal/lto"
"github.com/goplus/llgo/internal/mockable"
"github.com/goplus/llgo/internal/packages"
Expand Down Expand Up @@ -59,6 +62,74 @@ func TestNeedsLinuxNoPIE(t *testing.T) {
}
}

func TestDefaultBuildTags(t *testing.T) {
const base = "llgo,math_big_pure_go,purego"
for _, test := range []struct {
name string
goarch string
target string
want string
}{
{name: "native", goarch: "arm64", want: base},
{name: "raw wasm", goarch: "wasm", want: base + ",nogc"},
{name: "configured wasm target", goarch: "wasm", target: "wasip1", want: base},
} {
t.Run(test.name, func(t *testing.T) {
if got := defaultBuildTags(test.goarch, test.target); got != test.want {
t.Fatalf("defaultBuildTags(%q, %q) = %q, want %q", test.goarch, test.target, got, test.want)
}
})
}
}

func TestWasmRuntimeAvoidsNativeHostDependencies(t *testing.T) {
runtimeDir := filepath.Join(env.LLGoRuntimeDir(), "internal", "lib", "runtime")
for _, goos := range []string{"js", "wasip1"} {
t.Run(goos, func(t *testing.T) {
ctx := gobuild.Default
ctx.GOOS = goos
ctx.GOARCH = "wasm"
ctx.BuildTags = []string{"llgo", "nogc"}
pkg, err := ctx.ImportDir(runtimeDir, 0)
if err != nil {
t.Fatal(err)
}

selected := make(map[string]bool)
for _, name := range append(pkg.GoFiles, pkg.CgoFiles...) {
selected[name] = true
file, err := parser.ParseFile(token.NewFileSet(), filepath.Join(runtimeDir, name), nil, parser.ImportsOnly)
if err != nil {
t.Fatal(err)
}
for _, spec := range file.Imports {
path, err := strconv.Unquote(spec.Path.Value)
if err != nil {
t.Fatal(err)
}
switch path {
case "github.com/goplus/llgo/runtime/internal/clite/libuv",
"github.com/goplus/llgo/runtime/internal/clite/bdwgc":
t.Fatalf("wasm selected %s, which imports native host dependency %s", name, path)
}
}
}

for _, name := range []string{
"mfinal_nogc.go",
"runtime_baremetal.go",
"signal_baremetal_llgo.go",
"time_wasm_llgo.go",
"unwind_wasm_llgo.go",
} {
if !selected[name] {
t.Errorf("wasm runtime did not select %s", name)
}
}
})
}
}

func TestNeedsLinuxExportDynamic(t *testing.T) {
t.Setenv(llgoFuncInfo, "")
ctx := &context{buildConf: &Config{Goos: "linux"}}
Expand Down
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
7 changes: 7 additions & 0 deletions internal/build/testdata/wasm-runtime/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package main

import "runtime"

func main() {
println(runtime.GOOS)
}
Loading
Loading