Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
6c7c3a7
runtime/wasm: add WASI single-worker scheduler
cpunion Jul 27, 2026
dfe6bcb
test(runtime): exercise WASI Asyncify scheduler
cpunion Jul 27, 2026
2cdb3cb
fix(runtime/wasm): initialize scheduler for minimal P1 mains
cpunion Jul 27, 2026
16feeaa
ci: install Binaryen for wasm cache tests
cpunion Jul 27, 2026
e3e8ea8
runtime/wasm: keep fiber host calls out of method roots
cpunion Jul 27, 2026
cf0377e
build/wasm: centralize post-link output lifecycle
cpunion Jul 28, 2026
7261611
Merge branch 'codex/wasm-single-worker-asyncify-scheduler' into codex…
cpunion Jul 28, 2026
10b459e
Merge branch 'codex/wasm-single-worker-asyncify-scheduler' into codex…
cpunion Jul 28, 2026
1b9ad7a
Merge branch 'codex/wasm-single-worker-asyncify-scheduler' into codex…
cpunion Jul 29, 2026
0da6714
Merge branch 'codex/wasm-static-defer-dispatch' into codex/wasm-wasi-…
cpunion Jul 29, 2026
98d6285
Merge remote-tracking branch 'origin/codex/wasm-single-worker-asyncif…
cpunion Jul 29, 2026
9e081db
runtime/wasm: encapsulate continuation storage
cpunion Jul 29, 2026
c954d5a
Merge remote-tracking branch 'origin/codex/wasm-single-worker-asyncif…
cpunion Jul 30, 2026
b94deee
Merge branch 'codex/wasm-single-worker-asyncify-scheduler' into codex…
cpunion Jul 30, 2026
4cbebe3
runtime/wasm: preserve explicit WASI thread fatal behavior
cpunion Jul 30, 2026
1ea45b6
Merge branch 'codex/wasm-single-worker-asyncify-scheduler' into codex…
cpunion Aug 1, 2026
50866cd
build/wasm: align 64-bit fields with LLVM layout
cpunion Aug 1, 2026
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
40 changes: 40 additions & 0 deletions .github/actions/setup-binaryen/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: "Setup Binaryen"
description: "Install a pinned Binaryen release"
inputs:
version:
description: "Binaryen release version"
required: false
default: "131"

runs:
using: "composite"
steps:
- name: Install Binaryen
shell: bash
run: |
set -euo pipefail

version="${{ inputs.version }}"
case "$(uname -s):$(uname -m)" in
Linux:x86_64) platform="x86_64-linux" ;;
Linux:aarch64|Linux:arm64) platform="aarch64-linux" ;;
Darwin:x86_64) platform="x86_64-macos" ;;
Darwin:arm64) platform="arm64-macos" ;;
*)
echo "Unsupported Binaryen host: $(uname -s) $(uname -m)" >&2
exit 1
;;
esac

archive="binaryen-version_${version}-${platform}.tar.gz"
base_url="https://github.com/WebAssembly/binaryen/releases/download/version_${version}"
cd "$RUNNER_TEMP"
curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}"
curl --retry 3 --retry-all-errors -fsSLO "${base_url}/${archive}.sha256"
if command -v sha256sum >/dev/null; then
sha256sum --check "${archive}.sha256"
else
shasum -a 256 --check "${archive}.sha256"
fi
tar -xzf "$archive" -C "$RUNNER_TEMP"
echo "$RUNNER_TEMP/binaryen-version_${version}/bin" >> "$GITHUB_PATH"
3 changes: 3 additions & 0 deletions .github/workflows/build-cache.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ jobs:
- name: Set up Go
uses: ./.github/actions/setup-go

- name: Set up Binaryen
uses: ./.github/actions/setup-binaryen

- name: Install wamr (for wasm tests)
if: startsWith(matrix.os, 'macos')
run: |
Expand Down
39 changes: 38 additions & 1 deletion .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,9 @@ jobs:
- name: Set up Go for building llgo
uses: ./.github/actions/setup-go

- name: Set up Binaryen
uses: ./.github/actions/setup-binaryen

- name: Install wamr
run: |
git clone --branch WAMR-2.4.4 --depth 1 https://github.com/bytecodealliance/wasm-micro-runtime.git
Expand Down Expand Up @@ -421,6 +424,19 @@ jobs:
with:
node-version: "25"

- name: Set up Binaryen
uses: ./.github/actions/setup-binaryen

- name: Set up Wasmtime
uses: bytecodealliance/actions/wasmtime/setup@v1
with:
version: "39.0.1"

- name: Set up wasm-tools
uses: bytecodealliance/actions/wasm-tools/setup@v1
with:
version: "1.243.0"

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

Expand Down Expand Up @@ -448,10 +464,31 @@ jobs:
grep -Fq "fatal error: all goroutines are asleep - deadlock!" <<<"$output"
}

run_wasi_scheduler() {
local module="$1"
local output
wasm-tools validate --features all "$module"
output=$(wasmtime run -W exceptions=y "$module" 2>&1)
grep -Fq "wasm scheduler ok" <<<"$output"
if output=$(wasmtime run -W exceptions=y \
--env LLGO_WASM_SCHEDULER_DEADLOCK=1 "$module" 2>&1); then
echo "deadlock scheduler fixture unexpectedly succeeded"
return 1
fi
grep -Fq "fatal error: all goroutines are asleep - deadlock!" <<<"$output"
}

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
LLGO_WASI_THREADS=1 GOOS=wasip1 GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/runtime-wasip1-threads.wasm" ./internal/build/testdata/wasm-runtime
test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/runtime-wasip1.wasm" 2>&1)" = "wasip1"
GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-scheduler-go.mjs" ./internal/build/testdata/wasm-scheduler
run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler-go.mjs"
llgo build -target wasm -o "$RUNNER_TEMP/wasm-scheduler.mjs" ./internal/build/testdata/wasm-scheduler
run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler.mjs"
file "$RUNNER_TEMP/runtime-js.wasm" "$RUNNER_TEMP/runtime-wasip1.wasm"
GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-scheduler-wasip1.wasm" ./internal/build/testdata/wasm-scheduler
run_wasi_scheduler "$RUNNER_TEMP/wasm-scheduler-wasip1.wasm"
file "$RUNNER_TEMP/runtime-js.wasm" \
"$RUNNER_TEMP/runtime-wasip1.wasm" \
"$RUNNER_TEMP/runtime-wasip1-threads.wasm"
14 changes: 9 additions & 5 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -782,7 +782,8 @@ func effectiveTypeSizes(sizes types.Sizes, goos, goarch, target string) types.Si
// Named wasm targets use the native wasm32 data model. The raw js/wasm
// entry point keeps Go's 64-bit word model and is emitted as Memory64.
if goarch == "wasm" && (target != "" || goos != "js") {
return &types.StdSizes{WordSize: 4, MaxAlign: 4}
// LLVM's wasm32 data layout gives 64-bit scalars 8-byte alignment.
return &types.StdSizes{WordSize: 4, MaxAlign: 8}
}
return sizes
}
Expand Down Expand Up @@ -1413,12 +1414,15 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
}
linkArgs = append(linkArgs, cSharedExportArgs(ctx, linkedOrder)...)

err = linkObjFiles(ctx, outputPath, linkInputs, linkArgs, verbose)
linkOutput, err := prepareWasmLinkOutput(ctx.buildConf, &ctx.crossCompile, outputPath)
if err != nil {
return err
}

return nil
defer cleanupWasmLinkOutput(linkOutput, outputPath)
if err := linkObjFiles(ctx, linkOutput, linkInputs, linkArgs, verbose); err != nil {
return err
}
return publishWasmLinkOutput(ctx, linkOutput, outputPath, verbose)
}

func linkedModuleGlobals(pkgs []Package) map[string]none {
Expand Down Expand Up @@ -2439,7 +2443,7 @@ func llvmPassPipeline(level optlevel.Level, ltoMode lto.Mode) string {
}

func IsWasiThreadsEnabled() bool {
return isEnvOn(llgoWasiThreads, true)
return isEnvOn(llgoWasiThreads, false)
}

func IsFullRpathEnabled() bool {
Expand Down
21 changes: 18 additions & 3 deletions internal/build/build_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -301,16 +301,20 @@ func TestEffectiveWasmTypeSizes(t *testing.T) {
goos string
target string
want int64
align int64
}{
{name: "Go js wasm", goos: "js", want: 8},
{name: "configured wasm", goos: "js", target: "wasm", want: 4},
{name: "WASI compatibility", goos: "wasip1", want: 4},
{name: "Go js wasm", goos: "js", want: 8, align: 8},
{name: "configured wasm", goos: "js", target: "wasm", want: 4, align: 8},
{name: "WASI compatibility", goos: "wasip1", want: 4, align: 8},
} {
t.Run(test.name, func(t *testing.T) {
got := effectiveTypeSizes(goSizes, test.goos, "wasm", test.target)
if size := got.Sizeof(types.Typ[types.Uintptr]); size != test.want {
t.Fatalf("uintptr size = %d, want %d", size, test.want)
}
if align := got.Alignof(types.Typ[types.Uint64]); align != test.align {
t.Fatalf("uint64 alignment = %d, want %d", align, test.align)
}
})
}
if got := effectiveTypeSizes(goSizes, "linux", "amd64", ""); got != goSizes {
Expand Down Expand Up @@ -1158,6 +1162,17 @@ func TestApplyBuildModeCompileFlags(t *testing.T) {
applyBuildModeCompileFlags(BuildModeCShared, nil)
}

func TestWASIThreadsAreOptIn(t *testing.T) {
t.Setenv(llgoWasiThreads, "")
if IsWasiThreadsEnabled() {
t.Fatal("WASI threads are enabled by default")
}
t.Setenv(llgoWasiThreads, "1")
if !IsWasiThreadsEnabled() {
t.Fatal("WASI threads opt-in was ignored")
}
}

func TestCHeaderPackagesExcludesStandardRuntime(t *testing.T) {
prog := llssa.NewProgram(nil)
defer prog.Dispose()
Expand Down
32 changes: 29 additions & 3 deletions internal/build/main_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g
}

var rtInit llssa.Function
if cfg.rtInit {
if cfg.rtInit || ctx.crossCompile.WasmPostLink.Asyncify {
rtInit = declareNoArgFunc(mainPkg, rtPkgPath+".init")
}

Expand Down Expand Up @@ -127,10 +127,16 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g
return mainAPkg
}

var wasmRunMain llssa.Function
if ctx.crossCompile.WasmPostLink.Asyncify {
defineWasmMainTask(mainPkg, mainInit, mainMain)
wasmRunMain = declareNoArgFunc(mainPkg, rtPkgPath+".RunWasmMain")
}
entryFn := defineEntryFunction(ctx, mainPkg, argcVar, argvVar, argvValueType, entryFunctions{
runtimeStub: runtimeStub,
mainInit: mainInit,
mainMain: mainMain,
wasmRunMain: wasmRunMain,
pyInit: pyInit,
pyFinalize: pyFinalize,
rtInit: rtInit,
Expand Down Expand Up @@ -225,6 +231,7 @@ type entryFunctions struct {
runtimeStub llssa.Function
mainInit llssa.Function
mainMain llssa.Function
wasmRunMain llssa.Function
pyInit llssa.Function
pyFinalize llssa.Function
rtInit llssa.Function
Expand Down Expand Up @@ -272,8 +279,12 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa
b.Call(fns.abiInit.Expr)
}
b.Call(fns.runtimeStub.Expr)
b.Call(fns.mainInit.Expr)
b.Call(fns.mainMain.Expr)
if fns.wasmRunMain != nil {
b.Call(fns.wasmRunMain.Expr)
} else {
b.Call(fns.mainInit.Expr)
b.Call(fns.mainMain.Expr)
}
if fns.pyFinalize != nil {
b.Call(fns.pyFinalize.Expr)
}
Expand All @@ -284,6 +295,21 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa
return fn
}

func defineWasmMainTask(pkg llssa.Package, mainInit, mainMain llssa.Function) {
prog := pkg.Prog
sig := newSignature(
[]types.Type{types.Typ[types.UnsafePointer]},
[]types.Type{types.Typ[types.UnsafePointer]},
)
fn := pkg.NewFunc("__llgo_wasm_main", sig, llssa.InC)
fnVal := pkg.Module().NamedFunction("__llgo_wasm_main")
fnVal.SetVisibility(llvm.HiddenVisibility)
b := fn.MakeBody(1)
b.Call(mainInit.Expr)
b.Call(mainMain.Expr)
b.Return(prog.Nil(prog.VoidPtr()))
}

func defineStart(pkg llssa.Package, entry llssa.Function, argvType llssa.Type) {
fn := pkg.NewFunc("_start", llssa.NoArgsNoRet, llssa.InC)
pkg.Module().NamedFunction("_start").SetLinkage(llvm.WeakAnyLinkage)
Expand Down
42 changes: 42 additions & 0 deletions internal/build/main_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"strings"
"testing"

"github.com/goplus/llgo/internal/crosscompile"
"github.com/xgo-dev/llvm"

"github.com/goplus/llgo/internal/packages"
Expand Down Expand Up @@ -57,6 +58,47 @@ func TestGenMainModuleExecutable(t *testing.T) {
)
}

func TestGenMainModuleWASIAsyncifyEntry(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
ctx := &context{
prog: llssa.NewProgram(nil),
buildConf: &Config{
BuildMode: BuildModeExe,
Goos: "wasip1",
Goarch: "wasm",
},
crossCompile: crosscompile.Export{
WasmPostLink: crosscompile.WasmPostLink{Asyncify: true},
},
}
pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"}
mod := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{})
ir := mod.LPkg.String()
checks := []string{
`define hidden ptr @__llgo_wasm_main(ptr %0)`,
`call void @"github.com/goplus/llgo/runtime/internal/runtime.init"()`,
`call void @"example.com/foo.init"()`,
`call void @"example.com/foo.main"()`,
`call void @"github.com/goplus/llgo/runtime/internal/runtime.RunWasmMain"()`,
}
for _, want := range checks {
if !strings.Contains(ir, want) {
t.Fatalf("WASI main module IR missing %q:\n%s", want, ir)
}
}
entryStart := strings.Index(ir, "define hidden i32 @__main_argc_argv(")
if entryStart < 0 {
t.Fatalf("WASI main module missing host entry:\n%s", ir)
}
entry := ir[entryStart:]
entry = entry[:strings.Index(entry, "}\n")+2]
if strings.Contains(entry, `call void @"example.com/foo.init"()`) ||
strings.Contains(entry, `call void @"example.com/foo.main"()`) {
t.Fatalf("WASI system-stack entry calls package main directly:\n%s", entry)
}
}

func TestGenMainModuleLibrary(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
Expand Down
26 changes: 26 additions & 0 deletions internal/build/testdata/wasm-scheduler/layout.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package main

import "unsafe"

type wasmStructLayoutProbe struct {
prefix byte
wide uint64
ptr unsafe.Pointer
}

func checkWasmStructLayout() {
var value wasmStructLayoutProbe
base := uintptr(unsafe.Pointer(&value))
if got, want := uintptr(unsafe.Pointer(&value.wide))-base, unsafe.Offsetof(value.wide); got != want {
panic("uint64 field layout mismatch")
}
if got, want := uintptr(unsafe.Pointer(&value.ptr))-base, unsafe.Offsetof(value.ptr); got != want {
panic("pointer field layout mismatch")
}

var values [2]wasmStructLayoutProbe
stride := uintptr(unsafe.Pointer(&values[1])) - uintptr(unsafe.Pointer(&values[0]))
if stride != unsafe.Sizeof(value) {
panic("struct size layout mismatch")
}
}
16 changes: 16 additions & 0 deletions internal/build/testdata/wasm-scheduler/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ var (
eventLog [8]int
eventCount int
done int
lifecycle int
)

func event(value int) {
Expand Down Expand Up @@ -58,6 +59,7 @@ func checkCurrentG() {
}

func main() {
checkWasmStructLayout()
checkWasmModel()
if schedulerDeadlockMode() != 0 {
testParkedMainDeadlock()
Expand Down Expand Up @@ -134,9 +136,23 @@ func main() {
if seenGCount != len(seenG) {
panic("not all goroutines ran")
}
testGoroutineLifecycle()
println("wasm scheduler ok")
}

func testGoroutineLifecycle() {
const count = 5000
for i := 1; i <= count; i++ {
want := i
go func() {
lifecycle = want
}()
for lifecycle != want {
runtime.Gosched()
}
}
}

func testParkedMainDeadlock() {
go func() {}()
parkForTesting()
Expand Down
Loading
Loading