Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
ef13eb5
runtime/wasm: define resumable frame dispatch contract
cpunion Jul 30, 2026
2680e9c
ssa/wasm: inventory resumable Go calls
cpunion Jul 30, 2026
6d2bafd
internal/wasmresume: number generated Go calls
cpunion Jul 30, 2026
b4b0466
internal/wasmresume: plan persistent frame values
cpunion Jul 30, 2026
4bbfaa7
runtime/wasm: retain completed resume frames
cpunion Jul 30, 2026
aace160
internal/wasmresume: lay out persistent frames
cpunion Jul 30, 2026
9057d12
runtime/wasm: define generated frame descriptors
cpunion Jul 30, 2026
3eb1fea
internal/wasmresume: emit leaf resume entries
cpunion Jul 30, 2026
c52c6bf
internal/wasmresume: split call continuations safely
cpunion Jul 30, 2026
d8ab6e9
internal/wasmresume: canonicalize live values in frames
cpunion Jul 30, 2026
00dcfd9
internal/wasmresume: lower direct resumable calls
cpunion Jul 30, 2026
39471a3
internal/wasmresume: exercise nested direct resumes
cpunion Jul 30, 2026
4e28c26
internal/wasmresume: link descriptors across packages
cpunion Jul 30, 2026
8c682ee
internal/wasmresume: lower indirect resumable calls
cpunion Jul 30, 2026
e04a1fe
ssa/wasm: route function values through resume entries
cpunion Jul 30, 2026
7be4d74
wasmresume: lower scheduler suspension points
cpunion Jul 30, 2026
70a2daa
runtime/wasm: scope resume frame storage per context
cpunion Jul 30, 2026
ca39b07
internal/wasmresume: execute required wasm profiles
cpunion Jul 30, 2026
a1263f4
wasmresume: preserve synchronous runtime boundaries
cpunion Jul 30, 2026
dd7d757
internal/build: separate wasm exception translation from Asyncify
cpunion Jul 30, 2026
a4aa6bb
wasmresume: complete persistent frame lifecycle
cpunion Jul 30, 2026
522dbde
ssa/wasm: emit resumable goroutine and defer state
cpunion Jul 30, 2026
215e8f2
runtime/wasm: schedule resumable goroutines
cpunion Jul 30, 2026
0b6ae00
ci/wasm: exercise resumable scheduler profiles
cpunion Jul 30, 2026
618ccf7
test/wasmresume: tolerate Node without table64
cpunion Jul 30, 2026
4375a39
compiler/wasm: integrate resumable GC roots
cpunion Aug 2, 2026
03a5f53
runtime/wasm: integrate resumable timers and roots
cpunion Aug 2, 2026
685e037
ci/wasm: exercise resumable runtime integration
cpunion Aug 2, 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
42 changes: 42 additions & 0 deletions .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,48 @@ jobs:
--env LLGO_WASM_BLOCKED_G=1000 \
"$RUNNER_TEMP/wasm-hardening-wasip1.wasm" hardening-p1 2>&1)
grep -Fxq "wasm hardening ok" <<<"$output"
LLGO_WASM_RESUME=1 GOOS=js GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-scheduler-go.mjs" ./internal/build/testdata/wasm-scheduler
run_wasm_scheduler "$RUNNER_TEMP/wasm-resume-scheduler-go.mjs"
LLGO_WASM_RESUME=1 llgo build -target wasm \
-o "$RUNNER_TEMP/wasm-resume-scheduler.mjs" ./internal/build/testdata/wasm-scheduler
run_wasm_scheduler "$RUNNER_TEMP/wasm-resume-scheduler.mjs"
LLGO_WASM_RESUME=1 GOOS=wasip1 GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-scheduler-wasip1.wasm" ./internal/build/testdata/wasm-scheduler
run_wasi_scheduler "$RUNNER_TEMP/wasm-resume-scheduler-wasip1.wasm"
LLGO_WASM_RESUME=1 GOOS=js GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-timers-go.mjs" ./internal/build/testdata/wasm-timers
run_wasm_timers "$RUNNER_TEMP/wasm-resume-timers-go.mjs"
LLGO_WASM_RESUME=1 llgo build -target wasm \
-o "$RUNNER_TEMP/wasm-resume-timers.mjs" ./internal/build/testdata/wasm-timers
run_wasm_timers "$RUNNER_TEMP/wasm-resume-timers.mjs"
LLGO_WASM_RESUME=1 GOOS=wasip1 GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-timers-wasip1.wasm" ./internal/build/testdata/wasm-timers
run_wasi_timers "$RUNNER_TEMP/wasm-resume-timers-wasip1.wasm"
LLGO_WASM_RESUME=1 GOOS=js GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-gc-go.mjs" ./internal/build/testdata/wasm-gc
run_wasm_workers "$RUNNER_TEMP/wasm-resume-gc-go.mjs" "wasm gc ok"
LLGO_WASM_RESUME=1 llgo build -target wasm \
-o "$RUNNER_TEMP/wasm-resume-gc.mjs" ./internal/build/testdata/wasm-gc
run_wasm_workers "$RUNNER_TEMP/wasm-resume-gc.mjs" "wasm gc ok"
LLGO_WASM_RESUME=1 GOOS=wasip1 GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc
wasm-tools validate --features all "$RUNNER_TEMP/wasm-resume-gc-wasip1.wasm"
test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/wasm-resume-gc-wasip1.wasm" 2>&1)" = "wasm gc ok"
LLGO_WASM_RESUME=1 GOOS=js GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-hardening-go.mjs" ./internal/build/testdata/wasm-hardening
run_wasm_hardening "$RUNNER_TEMP/wasm-resume-hardening-go.mjs" "resume-hardening-j64"
LLGO_WASM_RESUME=1 llgo build -target wasm \
-o "$RUNNER_TEMP/wasm-resume-hardening.mjs" ./internal/build/testdata/wasm-hardening
run_wasm_hardening "$RUNNER_TEMP/wasm-resume-hardening.mjs" "resume-hardening-j32"
LLGO_WASM_RESUME=1 GOOS=wasip1 GOARCH=wasm llgo build \
-o "$RUNNER_TEMP/wasm-resume-hardening-wasip1.wasm" ./internal/build/testdata/wasm-hardening
wasm-tools validate --features all "$RUNNER_TEMP/wasm-resume-hardening-wasip1.wasm"
output=$(wasmtime run -W exceptions=y \
--env LLGO_WASM_EXPECT_ARG=resume-hardening-p1 \
--env LLGO_WASM_BLOCKED_G=1000 \
"$RUNNER_TEMP/wasm-resume-hardening-wasip1.wasm" resume-hardening-p1 2>&1)
grep -Fxq "wasm hardening ok" <<<"$output"
file "$RUNNER_TEMP/runtime-js.wasm" \
"$RUNNER_TEMP/runtime-wasip1.wasm" \
"$RUNNER_TEMP/runtime-wasip1-threads.wasm" \
Expand Down
2 changes: 1 addition & 1 deletion cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -565,7 +565,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
if fn == nil {
fn = pkg.NewFuncEx(name, sig, llssa.Background(ftype), hasCtx, p.needsLinkOnce(f))
}
if target := p.prog.Target(); target.GOARCH == "wasm" {
if target := p.prog.Target(); target.GOARCH == "wasm" && len(f.Blocks) == 0 {
if decl, ok := f.Syntax().(*ast.FuncDecl); ok {
fullName, _ := astFuncName(llssa.PathOf(pkgTypes), decl)
if module, importName, ok := p.prog.WasmImport(fullName); ok {
Expand Down
22 changes: 22 additions & 0 deletions cl/wasm_import_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,3 +33,25 @@ func read(buf *byte) uint32 {
}
}
}

func TestWasmImportDirectiveIgnoredForDefinition(t *testing.T) {
const src = `package foo

//go:wasmimport gojs runtime.nanotime1
func nanotime1() int64 {
return 1
}
`
ir := cltest.CompileIREx(t, src, "foo.go", false, func(prog llssa.Program) {
prog.Target().GOOS = "js"
prog.Target().GOARCH = "wasm"
})
for _, unwanted := range []string{
`"wasm-import-module"="gojs"`,
`"wasm-import-name"="runtime.nanotime1"`,
} {
if strings.Contains(ir, unwanted) {
t.Fatalf("unexpected %s on WebAssembly function definition:\n%s", unwanted, ir)
}
}
}
15 changes: 15 additions & 0 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -387,6 +387,9 @@ func Build(inv Invocation) ([]Package, error) {
if conf.Target != "" && export.GOARCH != "" {
conf.Goarch = export.GOARCH
}
if err := configureWasmResume(conf, &export); err != nil {
return nil, err
}
wasmWorkers, err := configureWasmWorkers(conf, &export)
if err != nil {
return nil, err
Expand Down Expand Up @@ -451,6 +454,7 @@ func Build(inv Invocation) ([]Package, error) {
}

prog := llssa.NewProgram(target)
prog.EnableWasmResumeABI(IsWasmResumeEnabled())
prog.DisableBoundsChecks(conf.DisableBoundsChecks)
if conf.Mode != ModeGen {
// ModeGen callers (llgen and the golden suites) read LPkg.String()
Expand Down Expand Up @@ -1471,6 +1475,9 @@ func linkMainPkg(ctx *context, pkg *packages.Package, pkgs []*aPackage, outputPa
pcLineInfo: pcLineInfo,
funcInfoStubs: funcInfoStubs,
})
if err := lowerWasmResumeModule(ctx, entryPkg.LPkg.Module()); err != nil {
return fmt.Errorf("entry main: %w", err)
}
entryObjFile, err := exportObject(ctx, "entry_main", entryPkg.ExportFile, entryPkg.LPkg)
if err != nil {
return err
Expand Down Expand Up @@ -1874,6 +1881,9 @@ func buildPkg(ctx *context, aPkg *aPackage, verbose bool) error {
return nil
}

if err := lowerWasmResumeModule(ctx, ret.Module()); err != nil {
return fmt.Errorf("%s: %w", pkgPath, err)
}
ctx.cTransformer.SetSkipFuncs(cabiSkipFuncsForPlan9Asm(ctx, pkgPath, ret.Module()))
llabi.LowerLargeAggregates(ctx.prog.TargetData(), ret.Module())
ctx.cTransformer.TransformModule(ret.Path(), ret.Module())
Expand Down Expand Up @@ -2454,6 +2464,7 @@ const llgoFuncInfoSites = "LLGO_FUNCINFO_SITES"
const llgoTrace = "LLGO_TRACE"
const llgoOptimize = "LLGO_OPTIMIZE"
const llgoWasmRuntime = "LLGO_WASM_RUNTIME"
const llgoWasmResume = "LLGO_WASM_RESUME"
const llgoWasiThreads = "LLGO_WASI_THREADS"
const llgoWasmWorkers = "LLGO_WASM_WORKERS"
const llgoStdioNobuf = "LLGO_STDIO_NOBUF"
Expand Down Expand Up @@ -2536,6 +2547,10 @@ func IsWasiThreadsEnabled() bool {
return isEnvOn(llgoWasiThreads, false)
}

func IsWasmResumeEnabled() bool {
return isEnvOn(llgoWasmResume, false)
}

func IsFullRpathEnabled() bool {
return isEnvOn(llgoFullRpath, true)
}
Expand Down
1 change: 1 addition & 0 deletions internal/build/collect.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ func (c *context) collectEnvInputs(m *manifestBuilder) {
llgoTrace,
llgoOptimize,
llgoWasmRuntime,
llgoWasmResume,
llgoWasiThreads,
llgoWasmWorkers,
llgoStdioNobuf,
Expand Down
25 changes: 18 additions & 7 deletions internal/build/main_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,11 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g
pyFinalize = declareNoArgFunc(mainPkg, "Py_Finalize")
}

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

Expand All @@ -109,8 +112,12 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g
pkgPath = pkg.PkgPath
}

mainInit := declareNoArgFunc(mainPkg, pkgPath+".init")
mainMain := declareNoArgFunc(mainPkg, pkgPath+".main")
mainBackground := llssa.InC
if ctx.prog.WasmResumeABIEnabled() {
mainBackground = llssa.InGo
}
mainInit := mainPkg.NewFunc(pkgPath+".init", llssa.NoArgsNoRet, mainBackground)
mainMain := mainPkg.NewFunc(pkgPath+".main", llssa.NoArgsNoRet, mainBackground)

if ctx.buildConf.BuildMode != BuildModeExe {
initArraySection := ""
Expand All @@ -128,8 +135,8 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g
}

var wasmRunMain llssa.Function
if ctx.crossCompile.WasmPostLink.Asyncify || ctx.crossCompile.WasmRuntime.RunMainTask {
defineWasmMainTask(mainPkg, mainInit, mainMain)
if wasmScheduler {
defineWasmMainTask(mainPkg, mainInit, mainMain, ctx.prog.WasmResumeABIEnabled())
wasmRunMain = declareNoArgFunc(mainPkg, rtPkgPath+".RunWasmMain")
}
entryFn := defineEntryFunction(ctx, mainPkg, argcVar, argvVar, argvValueType, entryFunctions{
Expand Down Expand Up @@ -296,13 +303,17 @@ func defineEntryFunction(ctx *context, pkg llssa.Package, argcVar, argvVar llssa
return fn
}

func defineWasmMainTask(pkg llssa.Package, mainInit, mainMain llssa.Function) {
func defineWasmMainTask(pkg llssa.Package, mainInit, mainMain llssa.Function, resumable bool) {
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)
background := llssa.InC
if resumable {
background = llssa.InGo
}
fn := pkg.NewFunc("__llgo_wasm_main", sig, background)
fnVal := pkg.Module().NamedFunction("__llgo_wasm_main")
fnVal.SetVisibility(llvm.HiddenVisibility)
b := fn.MakeBody(1)
Expand Down
34 changes: 34 additions & 0 deletions internal/build/main_module_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,40 @@ func TestGenMainModuleWasmWorkerEntry(t *testing.T) {
}
}

func TestGenMainModuleWasmResumeEntry(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
prog := llssa.NewProgram(&llssa.Target{GOOS: "wasip1", GOARCH: "wasm"})
defer prog.Dispose()
prog.EnableWasmResumeABI(true)
ctx := &context{
prog: prog,
buildConf: &Config{
BuildMode: BuildModeExe,
Goos: "wasip1",
Goarch: "wasm",
},
}
pkg := &packages.Package{PkgPath: "example.com/foo", ExportFile: "foo.a"}
mod := genMainModule(ctx, llssa.PkgRuntime, pkg, &genConfig{})
if err := lowerWasmResumeModule(ctx, mod.LPkg.Module()); err != nil {
t.Fatal(err)
}
ir := mod.LPkg.String()
for _, want := range []string{
`define ptr @__llgo_wasm_start.__llgo_wasm_main`,
`define internal i8 @__llgo_wasm_resume.__llgo_wasm_main`,
`@"__llgo_wasm_resume_desc.example.com/foo.init" = external global`,
`@"__llgo_wasm_resume_desc.example.com/foo.main" = external global`,
`define hidden ptr @__llgo_wasm_main(ptr %0)`,
`call void @"github.com/goplus/llgo/runtime/internal/runtime.RunWasmMain"()`,
} {
if !strings.Contains(ir, want) {
t.Fatalf("resumable main module IR missing %q:\n%s", want, ir)
}
}
}

func TestGenMainModuleLibrary(t *testing.T) {
llvm.InitializeAllTargets()
t.Setenv(llgoStdioNobuf, "")
Expand Down
24 changes: 16 additions & 8 deletions internal/build/wasm_postlink.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,17 +29,25 @@ import (

func needsWasmPostLink(conf *Config, target *crosscompile.Export) bool {
return conf != nil && conf.BuildMode == BuildModeExe &&
target != nil && target.WasmPostLink.Asyncify
target != nil &&
(target.WasmPostLink.Asyncify || target.WasmPostLink.TranslateToExnref)
}

func wasmPostLinkArgs(target *crosscompile.Export, input, output string, debug bool) []string {
if target == nil || !target.WasmPostLink.Asyncify {
if target == nil ||
(!target.WasmPostLink.Asyncify && !target.WasmPostLink.TranslateToExnref) {
return nil
}
// LLVM 19 lowers Wasm SjLj through the legacy EH encoding. Asyncify
// understands that form; translate it only after instrumentation so the
// final module uses the standardized exnref-based EH instructions.
args := []string{"--asyncify", "--translate-to-exnref"}
var args []string
if target.WasmPostLink.Asyncify {
args = append(args, "--asyncify")
}
// LLVM 19 lowers Wasm SjLj through the legacy EH encoding. When Asyncify
// is enabled, translate only after instrumentation so the final module
// uses the standardized exnref-based EH instructions.
if target.WasmPostLink.TranslateToExnref {
args = append(args, "--translate-to-exnref")
}
if debug {
args = append(args, "-g")
}
Expand Down Expand Up @@ -89,7 +97,7 @@ func postLinkWasm(ctx *context, input, output string, verbose bool) error {
}
resolved, err := exec.LookPath(wasmOpt)
if err != nil {
return fmt.Errorf("WebAssembly Asyncify requires wasm-opt; install Binaryen or set WASMOPT: %w", err)
return fmt.Errorf("WebAssembly post-link requires wasm-opt; install Binaryen or set WASMOPT: %w", err)
}

tmpName, err := createClosedTemp(
Expand All @@ -114,7 +122,7 @@ func postLinkWasm(ctx *context, input, output string, verbose bool) error {
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
return fmt.Errorf("wasm-opt Asyncify failed: %w", err)
return fmt.Errorf("wasm-opt post-link failed: %w", err)
}
if err := os.Rename(tmpName, output); err != nil {
return err
Expand Down
24 changes: 20 additions & 4 deletions internal/build/wasm_postlink_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,10 @@ func wasmPostLinkTestContext() *context {
return &context{
buildConf: &Config{LinkOptions: LinkOptions{DWARF: DWARFOmit}},
crossCompile: crosscompile.Export{
WasmPostLink: crosscompile.WasmPostLink{Asyncify: true},
WasmPostLink: crosscompile.WasmPostLink{
Asyncify: true,
TranslateToExnref: true,
},
},
}
}
Expand All @@ -51,7 +54,10 @@ func writeWasmOptTestTool(t *testing.T, dir, script string) string {
}

func TestWasmPostLinkArgs(t *testing.T) {
target := &crosscompile.Export{WasmPostLink: crosscompile.WasmPostLink{Asyncify: true}}
target := &crosscompile.Export{WasmPostLink: crosscompile.WasmPostLink{
Asyncify: true,
TranslateToExnref: true,
}}
if got, want := wasmPostLinkArgs(target, "in.wasm", "out.wasm", false),
[]string{"--asyncify", "--translate-to-exnref", "in.wasm", "-o", "out.wasm"}; !reflect.DeepEqual(got, want) {
t.Fatalf("wasmPostLinkArgs() = %v, want %v", got, want)
Expand All @@ -63,6 +69,11 @@ func TestWasmPostLinkArgs(t *testing.T) {
if got := wasmPostLinkArgs(&crosscompile.Export{}, "in", "out", false); got != nil {
t.Fatalf("wasmPostLinkArgs(disabled) = %v, want nil", got)
}
target.WasmPostLink.Asyncify = false
if got, want := wasmPostLinkArgs(target, "in.wasm", "out.wasm", false),
[]string{"--translate-to-exnref", "in.wasm", "-o", "out.wasm"}; !reflect.DeepEqual(got, want) {
t.Fatalf("wasmPostLinkArgs(translate only) = %v, want %v", got, want)
}
}

func TestNeedsWasmPostLink(t *testing.T) {
Expand All @@ -87,6 +98,11 @@ func TestNeedsWasmPostLink(t *testing.T) {
if needsWasmPostLink(&Config{BuildMode: BuildModeExe}, nil) {
t.Fatal("needsWasmPostLink() enabled for a nil target")
}
target.WasmPostLink.Asyncify = false
target.WasmPostLink.TranslateToExnref = true
if !needsWasmPostLink(&Config{BuildMode: BuildModeExe}, target) {
t.Fatal("needsWasmPostLink() disabled for exnref translation")
}
}

func TestPrepareWasmLinkOutput(t *testing.T) {
Expand Down Expand Up @@ -196,7 +212,7 @@ func TestPostLinkWasmReportsToolFailure(t *testing.T) {

ctx := wasmPostLinkTestContext()
err := postLinkWasm(ctx, input, output, false)
if err == nil || !strings.Contains(err.Error(), "wasm-opt Asyncify failed") {
if err == nil || !strings.Contains(err.Error(), "wasm-opt post-link failed") {
t.Fatalf("postLinkWasm() error = %v", err)
}
if data, err := os.ReadFile(output); err != nil || string(data) != "old" {
Expand All @@ -223,7 +239,7 @@ func TestPostLinkWasmReportsPublishFailure(t *testing.T) {
if err == nil {
t.Fatal("postLinkWasm succeeded when the final output was a directory")
}
if strings.Contains(err.Error(), "wasm-opt Asyncify failed") {
if strings.Contains(err.Error(), "wasm-opt post-link failed") {
t.Fatalf("postLinkWasm failed before publishing output: %v", err)
}
}
Expand Down
Loading
Loading