Skip to content
Closed
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
6 changes: 3 additions & 3 deletions .github/workflows/llgo.yml
Original file line number Diff line number Diff line change
Expand Up @@ -508,11 +508,11 @@ jobs:
run_wasm_timers "$RUNNER_TEMP/wasm-timers.mjs"
GOOS=wasip1 GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-timers-wasip1.wasm" ./internal/build/testdata/wasm-timers
run_wasi_timers "$RUNNER_TEMP/wasm-timers-wasip1.wasm"
GOOS=js GOARCH=wasm llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-go.mjs" ./internal/build/testdata/wasm-gc
GOOS=js GOARCH=wasm llgo build -o "$RUNNER_TEMP/wasm-gc-go.mjs" ./internal/build/testdata/wasm-gc
node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc-go.mjs'; await Module();"
llgo build -target wasm -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc.mjs" ./internal/build/testdata/wasm-gc
llgo build -target wasm -o "$RUNNER_TEMP/wasm-gc.mjs" ./internal/build/testdata/wasm-gc
node --input-type=module -e "import Module from '$RUNNER_TEMP/wasm-gc.mjs'; await Module();"
GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 llgo build -tags=llgo_wasm_gc -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc
GOOS=wasip1 GOARCH=wasm LLGO_WASI_THREADS=0 llgo build -o "$RUNNER_TEMP/wasm-gc-wasip1.wasm" ./internal/build/testdata/wasm-gc
wasm-tools validate --features all "$RUNNER_TEMP/wasm-gc-wasip1.wasm"
test "$(wasmtime run -W exceptions=y "$RUNNER_TEMP/wasm-gc-wasip1.wasm" 2>&1)" = "wasm gc ok"
file "$RUNNER_TEMP/runtime-js.wasm" \
Expand Down
20 changes: 18 additions & 2 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (

"github.com/goplus/llgo/cl/blocks"
"github.com/goplus/llgo/cl/ssawrap"
"github.com/goplus/llgo/internal/directive"
"github.com/goplus/llgo/internal/goembed"
"github.com/goplus/llgo/internal/typepatch"
"golang.org/x/tools/go/ssa"
Expand Down Expand Up @@ -181,6 +182,8 @@ type context struct {
runtimeCallerFuncs map[*ssa.Function]bool
gcRoots map[ssa.Value][]llssa.Expr
gcClosureRoot llssa.Expr
safepointEntry bool
safepoints map[ssa.Instruction]struct{}
pcLineSeq uint64

patches Patches
Expand Down Expand Up @@ -617,6 +620,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark
oldLocalityFunction := p.locality.function
oldGCRoots, oldGCClosureRoot := p.gcRoots, p.gcClosureRoot
oldSafepointEntry, oldSafepoints := p.safepointEntry, p.safepoints
p.fn = fn
p.goFn = f
p.callerFrameMark = llssa.Nil
Expand All @@ -626,6 +630,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark
p.locality.function = oldLocalityFunction
p.gcRoots, p.gcClosureRoot = oldGCRoots, oldGCClosureRoot
p.safepointEntry, p.safepoints = oldSafepointEntry, oldSafepoints
}()
p.phis = nil
if dbgSymsEnabled {
Expand All @@ -646,6 +651,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
p.prepareExportedLocalContext(f)
p.bvals = make(map[ssa.Value]llssa.Expr)
p.methodNilDerefChecks = collectMethodNilDerefChecks(f)
p.prepareCooperativeSafepoints(f, isCgo)
p.prepareGCRoots(f, hasCtx)
p.initGCRoots(b, f)
off := make([]int, len(f.Blocks))
Expand Down Expand Up @@ -693,12 +699,16 @@ func funcInfoDisplayName(goName string) string {
}

func hasNoInlineDirective(f *ssa.Function) bool {
return hasFuncDirective(f, "go:noinline")
}

func hasFuncDirective(f *ssa.Function, name string) bool {
decl, _ := f.Syntax().(*ast.FuncDecl)
if decl == nil || decl.Doc == nil {
return false
}
for _, c := range decl.Doc.List {
if c.Text == "//go:noinline" {
for _, item := range directive.ParseGroup(decl.Doc) {
if item.Name == name {
return true
}
}
Expand Down Expand Up @@ -871,6 +881,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do
if enableDbgSyms && block.Parent().Origin() == nil && block.Index == 0 {
p.debugParams(b, block.Parent())
}
if block.Index == 0 && p.safepointEntry {
p.emitCooperativeSafepoint(b)
}

if doModInit {
p.initializeLocalGuards(b)
Expand All @@ -894,6 +907,9 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do
isCgoC2 := isCgoC2func(fnName)
isCgoCmacro := isCgoCmacro(fnName)
for i, instr := range instrs {
if p.isCooperativeSafepoint(instr) {
p.emitCooperativeSafepoint(b)
}
if i == 1 && doModInit && p.state == pkgInPatch { // in patch package but no pkgFNoOldInit
initFnNameOld := initFnNameOfHasPatch(p.fn.Name())
fnOld := pkg.NewFunc(initFnNameOld, llssa.NoArgsNoRet, llssa.InC)
Expand Down
30 changes: 28 additions & 2 deletions cl/gcroot.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,15 @@ func (p *context) prepareGCRoots(fn *ssa.Function, hasClosureContext bool) {
}
typ := p.type_(value.Type(), llssa.InGo)
return p.prog.GCRootCount(typ) != 0
}, gcSafepoint)
}, p.isGCSafepoint)
if p.safepointEntry {
for _, param := range fn.Params {
typ := p.type_(param.Type(), llssa.InGo)
if p.prog.GCRootCount(typ) != 0 {
planned[param] = struct{}{}
}
}
}
counts := make(map[ssa.Value]int, len(planned))
total := 0
count := func(value ssa.Value) {
Expand All @@ -62,7 +70,7 @@ func (p *context) prepareGCRoots(fn *ssa.Function, hasClosureContext bool) {
}
}
}
hasClosureRoot := hasClosureContext && functionHasGCSafepoint(fn)
hasClosureRoot := hasClosureContext && p.functionHasGCSafepoint(fn)
if hasClosureRoot {
total++
}
Expand Down Expand Up @@ -117,6 +125,24 @@ func functionHasGCSafepoint(fn *ssa.Function) bool {
return false
}

func (p *context) functionHasGCSafepoint(fn *ssa.Function) bool {
if p.safepointEntry {
return true
}
for _, block := range fn.Blocks {
for _, instr := range block.Instrs {
if p.isGCSafepoint(instr) {
return true
}
}
}
return false
}

func (p *context) isGCSafepoint(instr ssa.Instruction) bool {
return gcSafepoint(instr) || p.isCooperativeSafepoint(instr)
}

// gcSafepoint mirrors the operations whose LLGo lowering can call the runtime.
// Unknown instructions stay conservative.
func gcSafepoint(instr ssa.Instruction) bool {
Expand Down
72 changes: 72 additions & 0 deletions cl/safepoint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/*
* 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 cl

import (
"strings"

"github.com/goplus/llgo/internal/safepointplan"
llssa "github.com/goplus/llgo/ssa"
"golang.org/x/tools/go/ssa"
)

func (p *context) prepareCooperativeSafepoints(fn *ssa.Function, isCgo bool) {
p.safepointEntry = false
p.safepoints = nil
if !p.prog.CooperativeSafepointsEnabled() || fn == nil || len(fn.Blocks) == 0 ||
isCgo || hasFuncDirective(fn, "go:nosplit") {
return
}
// Package-less SSA wrappers only forward into a declared function and may
// represent runtime helpers, so the declared function owns the poll.
if path := safepointPackagePath(fn); path == "" || excludeSafepointPackage(path) {
return
}
p.safepointEntry = true
p.safepoints = safepointplan.Backedges(fn)
}

func safepointPackagePath(fn *ssa.Function) string {
for current := fn; current != nil; current = current.Parent() {
if pkg := current.Package(); pkg != nil {
return pkg.Pkg.Path()
}
if origin := current.Origin(); origin != nil {
if pkg := origin.Package(); pkg != nil {
return pkg.Pkg.Path()
}
}
}
return ""
}

func excludeSafepointPackage(path string) bool {
if path == "runtime" || strings.HasPrefix(path, "internal/runtime/") {
return true
}
runtimeModule := strings.TrimSuffix(llssa.PkgRuntime, "/internal/runtime")
return path == runtimeModule || strings.HasPrefix(path, runtimeModule+"/")
}

func (p *context) isCooperativeSafepoint(instr ssa.Instruction) bool {
_, ok := p.safepoints[instr]
return ok
}

func (p *context) emitCooperativeSafepoint(b llssa.Builder) {
b.Call(p.pkg.RuntimeFunc("CooperativeSafepoint"))
}
78 changes: 78 additions & 0 deletions cl/safepoint_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
//go:build !llgo

/*
* 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 cl

import (
"go/types"
"testing"
)

func TestExcludeSafepointPackage(t *testing.T) {
tests := []struct {
path string
want bool
}{
{path: "runtime", want: true},
{path: "internal/runtime/atomic", want: true},
{path: "github.com/goplus/llgo/runtime", want: true},
{path: "github.com/goplus/llgo/runtime/internal/wasmevent", want: true},
{path: "github.com/goplus/llgo/runtimeextra"},
{path: "example.com/app/runtime"},
}
for _, test := range tests {
if got := excludeSafepointPackage(test.path); got != test.want {
t.Errorf("excludeSafepointPackage(%q) = %v, want %v", test.path, got, test.want)
}
}
}

func TestSafepointPackagePathUsesGenericOrigin(t *testing.T) {
pkg := buildLinkOnceSSAPackage(t, `package p
type Box[T any] struct{}
func (Box[T]) M() {}
`)
box := pkg.Pkg.Scope().Lookup("Box").(*types.TypeName).Type().(*types.Named)
boxInt, err := types.Instantiate(nil, box, []types.Type{types.Typ[types.Int]}, true)
if err != nil {
t.Fatal(err)
}
_, fn := linkOnceTestMethodValue(t, pkg, boxInt, "M")
if fn.Package() != nil || fn.Origin() == nil {
t.Fatalf("expected a package-less generic instance, got package=%v origin=%v", fn.Package(), fn.Origin())
}
if got := safepointPackagePath(fn); got != "p" {
t.Fatalf("safepointPackagePath(%s) = %q, want p", fn, got)
}
}

func TestSafepointPackagePathLeavesSyntheticWrapperUnowned(t *testing.T) {
pkg := buildLinkOnceSSAPackage(t, `package p
type Inner struct{}
func (Inner) M() {}
type Outer struct{ Inner }
`)
outer := pkg.Pkg.Scope().Lookup("Outer").(*types.TypeName).Type()
_, fn := linkOnceTestMethodValue(t, pkg, types.NewPointer(outer), "M")
if fn.Package() != nil || fn.Origin() != nil {
t.Fatalf("expected an unowned synthetic wrapper, got package=%v origin=%v", fn.Package(), fn.Origin())
}
if got := safepointPackagePath(fn); got != "" {
t.Fatalf("safepointPackagePath(%s) = %q, want empty", fn, got)
}
}
83 changes: 83 additions & 0 deletions cl/safepoint_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
//go:build !llgo

package cl_test

import (
"regexp"
"strings"
"testing"

"github.com/goplus/llgo/cl/cltest"
llssa "github.com/goplus/llgo/ssa"
)

func TestCompileCooperativeSafepoints(t *testing.T) {
const src = `package main

func leaf(p *int) *int {
return p
}

func loop(p *int, n int) *int {
for n > 0 {
n--
}
return p
}

//go:nosplit
func noPoll(p *int) *int {
return p
}
`
ir := cltest.CompileIREx(t, src, "safepoint.go", false, func(prog llssa.Program) {
prog.EnableGCRoots(true)
prog.EnableCooperativeSafepoints(true)
})

leaf := findLLVMFunction(t, ir, "leaf")
if got := strings.Count(leaf, "CooperativeSafepoint"); got != 1 {
t.Fatalf("leaf has %d safepoints, want 1:\n%s", got, leaf)
}
if !strings.Contains(leaf, `[1 x ptr]`) {
t.Fatalf("leaf parameter is not rooted across the entry safepoint:\n%s", leaf)
}

loop := findLLVMFunction(t, ir, "loop")
if got := strings.Count(loop, "CooperativeSafepoint"); got != 2 {
t.Fatalf("loop has %d safepoints, want entry plus backedge:\n%s", got, loop)
}
if !strings.Contains(loop, `[1 x ptr]`) {
t.Fatalf("loop parameter is not rooted across safepoints:\n%s", loop)
}

noPoll := findLLVMFunction(t, ir, "noPoll")
if strings.Contains(noPoll, "CooperativeSafepoint") ||
strings.Contains(noPoll, "llvm_gc_root_chain") {
t.Fatalf("//go:nosplit function contains a safepoint or root frame:\n%s", noPoll)
}
}

func TestCompileCooperativeSafepointsDisabled(t *testing.T) {
const src = `package main
func loop(n int) {
for n > 0 {
n--
}
}
`
ir := cltest.CompileIREx(t, src, "safepoint_disabled.go", false, nil)
if strings.Contains(ir, "CooperativeSafepoint") {
t.Fatalf("disabled cooperative safepoints changed ordinary code:\n%s", ir)
}
}

func findLLVMFunction(t *testing.T, ir, name string) string {
t.Helper()
pattern := regexp.MustCompile(`(?ms)^define [^{]*\.` + regexp.QuoteMeta(name) + `"?\([^)]*\).*?^\}`)
body := pattern.FindString(ir)
if body == "" {
t.Fatalf("LLVM function %s not found:\n%s", name, ir)
}
return body
}
Loading
Loading