Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
3f420eb
runtime/wasm: define resumable frame dispatch contract
cpunion Jul 30, 2026
ab1413f
ssa/wasm: inventory resumable Go calls
cpunion Jul 30, 2026
8202b20
internal/wasmresume: number generated Go calls
cpunion Jul 30, 2026
9c7933d
internal/wasmresume: plan persistent frame values
cpunion Jul 30, 2026
3bfb3a7
runtime/wasm: retain completed resume frames
cpunion Jul 30, 2026
ffbf415
internal/wasmresume: lay out persistent frames
cpunion Jul 30, 2026
6732d34
runtime/wasm: define generated frame descriptors
cpunion Jul 30, 2026
177402b
internal/wasmresume: emit leaf resume entries
cpunion Jul 30, 2026
bfadd75
internal/wasmresume: split call continuations safely
cpunion Jul 30, 2026
2ddf803
internal/wasmresume: canonicalize live values in frames
cpunion Jul 30, 2026
2fa123f
internal/wasmresume: lower direct resumable calls
cpunion Jul 30, 2026
1e54d32
internal/wasmresume: exercise nested direct resumes
cpunion Jul 30, 2026
46488b5
internal/wasmresume: link descriptors across packages
cpunion Jul 30, 2026
cb6a2b3
internal/wasmresume: lower indirect resumable calls
cpunion Jul 30, 2026
ca42b6a
ssa/wasm: route function values through resume entries
cpunion Jul 30, 2026
a8350d9
wasmresume: lower scheduler suspension points
cpunion Jul 30, 2026
3de39f7
runtime/wasm: scope resume frame storage per context
cpunion Jul 30, 2026
ba20ec9
internal/wasmresume: execute required wasm profiles
cpunion Jul 30, 2026
37d53ec
wasmresume: preserve synchronous runtime boundaries
cpunion Jul 30, 2026
936e3ad
internal/build: separate wasm exception translation from Asyncify
cpunion Jul 30, 2026
171c322
wasmresume: complete persistent frame lifecycle
cpunion Jul 30, 2026
2b6933c
ssa/wasm: emit resumable goroutine and defer state
cpunion Jul 30, 2026
cd576bf
runtime/wasm: schedule resumable goroutines
cpunion Jul 30, 2026
b2fa104
ci/wasm: exercise resumable scheduler profiles
cpunion Jul 30, 2026
49cb0c7
test/wasmresume: tolerate Node without table64
cpunion Jul 30, 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
9 changes: 9 additions & 0 deletions .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -489,6 +489,15 @@ jobs:
run_wasm_scheduler "$RUNNER_TEMP/wasm-scheduler.mjs"
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"
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"
file "$RUNNER_TEMP/runtime-js.wasm" \
"$RUNNER_TEMP/runtime-wasip1.wasm" \
"$RUNNER_TEMP/runtime-wasip1-threads.wasm"
15 changes: 15 additions & 0 deletions internal/build/build.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,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
}
if conf.AppExt == "" {
conf.AppExt = defaultAppExt(conf)
}
Expand Down Expand Up @@ -441,6 +444,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 @@ -1382,6 +1386,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 @@ -1785,6 +1792,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 @@ -2365,6 +2375,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 llgoStdioNobuf = "LLGO_STDIO_NOBUF"
const llgoFullRpath = "LLGO_FULL_RPATH"
Expand Down Expand Up @@ -2446,6 +2457,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,
llgoStdioNobuf,
llgoFullRpath,
Expand Down
23 changes: 16 additions & 7 deletions internal/build/main_module.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,9 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g
pyFinalize = declareNoArgFunc(mainPkg, "Py_Finalize")
}

wasmScheduler := ctx.crossCompile.WasmPostLink.Asyncify || 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 +110,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 +133,8 @@ func genMainModule(ctx *context, rtPkgPath string, pkg *packages.Package, cfg *g
}

var wasmRunMain llssa.Function
if ctx.crossCompile.WasmPostLink.Asyncify {
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 @@ -295,13 +300,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 @@ -99,6 +99,40 @@ func TestGenMainModuleWASIAsyncifyEntry(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
61 changes: 61 additions & 0 deletions internal/build/wasm_resume.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2026 The XGo Authors (xgo.dev). All rights reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package build

import (
"fmt"
"slices"

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

const wasmResumeBuildTag = "llgo.wasm_resume"

func configureWasmResume(conf *Config, export *crosscompile.Export) error {
if !IsWasmResumeEnabled() {
return nil
}
if conf == nil || conf.Goarch != "wasm" {
return fmt.Errorf("%s requires GOARCH=wasm", llgoWasmResume)
}
if conf.Goos != "js" && conf.Goos != "wasip1" {
return fmt.Errorf("%s does not support GOOS=%s", llgoWasmResume, conf.Goos)
}
if IsWasiThreadsEnabled() {
return fmt.Errorf("%s is incompatible with %s", llgoWasmResume, llgoWasiThreads)
}
if !slices.Contains(export.BuildTags, wasmResumeBuildTag) {
export.BuildTags = append(export.BuildTags, wasmResumeBuildTag)
}
export.WasmPostLink.Asyncify = false
export.LDFLAGS = slices.DeleteFunc(export.LDFLAGS, func(flag string) bool {
return flag == "-sASYNCIFY=1"
})
return nil
}

func lowerWasmResumeModule(ctx *context, mod llvm.Module) error {
if ctx == nil || !ctx.prog.WasmResumeABIEnabled() {
return nil
}
if err := wasmresume.Lower(mod, ctx.prog.TargetData()); err != nil {
return fmt.Errorf("lower WebAssembly resumable ABI: %w", err)
}
return nil
}
Loading
Loading