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
10 changes: 10 additions & 0 deletions cl/compile.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,8 @@ type context struct {
debugDIVars map[*types.Var]llssa.DIVar
debugAllocVars map[*ssa.Alloc]*types.Var
runtimeCallerFuncs map[*ssa.Function]bool
gcRoots map[ssa.Value][]llssa.Expr
gcClosureRoot llssa.Expr
pcLineSeq uint64

patches Patches
Expand Down Expand Up @@ -606,6 +608,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
p.inits = append(p.inits, func() {
oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark
oldLocalityFunction := p.locality.function
oldGCRoots, oldGCClosureRoot := p.gcRoots, p.gcClosureRoot
p.fn = fn
p.goFn = f
p.callerFrameMark = llssa.Nil
Expand All @@ -614,6 +617,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun
defer func() {
p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark
p.locality.function = oldLocalityFunction
p.gcRoots, p.gcClosureRoot = oldGCRoots, oldGCClosureRoot
}()
p.phis = nil
if dbgSymsEnabled {
Expand All @@ -634,6 +638,8 @@ 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.prepareGCRoots(f, hasCtx)
p.initGCRoots(b, f)
off := make([]int, len(f.Blocks))
if isCgo {
p.cgoArgs = make([]llssa.Expr, len(f.Params))
Expand Down Expand Up @@ -1178,6 +1184,7 @@ func (p *context) compilePhis(b llssa.Builder, block *ssa.BasicBlock) int {
for i := 0; i < n; i++ {
iv := block.Instrs[i].(*ssa.Phi)
p.bvals[iv] = rets[i]
p.publishGCRoot(b, iv, rets[i])
}
return n
}
Expand Down Expand Up @@ -1210,6 +1217,9 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue
}
log.Panicln("unreachable:", iv)
}
defer func() {
p.publishGCRoot(b, iv, ret)
}()
switch v := iv.(type) {
case *ssa.Call:
ret = p.call(b, llssa.Call, &v.Call)
Expand Down
190 changes: 190 additions & 0 deletions cl/gcroot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,190 @@
/*
* 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/token"
"go/types"

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

func (p *context) prepareGCRoots(fn *ssa.Function, hasClosureContext bool) {
p.gcRoots = nil
p.gcClosureRoot = llssa.Nil
if !p.prog.GCRootsEnabled() {
return
}

planned := gcrootplan.Plan(fn, func(value ssa.Value) bool {
switch value.(type) {
case *ssa.FreeVar:
return false
}
typ := p.type_(value.Type(), llssa.InGo)
return p.prog.GCRootCount(typ) != 0
}, gcSafepoint)
counts := make(map[ssa.Value]int, len(planned))
total := 0
count := func(value ssa.Value) {
if _, ok := planned[value]; !ok {
return
}
typ := p.type_(value.Type(), llssa.InGo)
if n := p.prog.GCRootCount(typ); n != 0 {
counts[value] = n
total += n
}
}
for _, param := range fn.Params {
count(param)
}
for _, block := range fn.Blocks {
for _, instr := range block.Instrs {
if value, ok := instr.(ssa.Value); ok {
count(value)
}
}
}
hasClosureRoot := hasClosureContext && functionHasGCSafepoint(fn)
if hasClosureRoot {
total++
}
allSlots := p.fn.NewGCRoots(total)
next := 0
roots := make(map[ssa.Value][]llssa.Expr, len(counts))
assign := func(value ssa.Value) {
if n := counts[value]; n != 0 {
roots[value] = allSlots[next : next+n]
next += n
}
}
for _, param := range fn.Params {
assign(param)
}
for _, block := range fn.Blocks {
for _, instr := range block.Instrs {
if value, ok := instr.(ssa.Value); ok {
assign(value)
}
}
}
p.gcRoots = roots
if hasClosureRoot {
p.gcClosureRoot = allSlots[next]
}
}

func (p *context) initGCRoots(b llssa.Builder, fn *ssa.Function) {
if len(p.gcRoots) == 0 && p.gcClosureRoot.IsNil() {
return
}
b.SetBlockEx(p.fn.Block(0), llssa.AtEnd, true)
for i, param := range fn.Params {
if _, ok := p.gcRoots[param]; ok {
p.publishGCRoot(b, param, b.Param(i))
}
}
if !p.gcClosureRoot.IsNil() {
b.SetGCRoot(p.gcClosureRoot, p.fn.ClosureContextParam())
}
}

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

// gcSafepoint mirrors the operations whose LLGo lowering can call the runtime.
// Unknown instructions stay conservative.
func gcSafepoint(instr ssa.Instruction) bool {
switch instr := instr.(type) {
case *ssa.Phi, *ssa.DebugRef, *ssa.Extract, *ssa.Field, *ssa.FieldAddr,
*ssa.Index, *ssa.IndexAddr, *ssa.If, *ssa.Jump, *ssa.Return,
*ssa.Slice, *ssa.SliceToArrayPointer, *ssa.Store, *ssa.ChangeType:
return false
case *ssa.BinOp:
return gcBinOpSafepoint(instr)
case *ssa.UnOp:
return instr.Op == token.ARROW
case *ssa.Convert:
return gcConversionSafepoint(instr.X.Type(), instr.Type())
case *ssa.Call:
if builtin, ok := instr.Call.Value.(*ssa.Builtin); ok {
switch builtin.Name() {
case "cap", "complex", "imag", "len", "real":
return false
}
}
return true
default:
return true
}
}

func gcBinOpSafepoint(instr *ssa.BinOp) bool {
switch basicKind(instr.X.Type()) {
case types.String, types.UntypedString:
return true
}
_, isInterface := types.Unalias(instr.X.Type()).Underlying().(*types.Interface)
return isInterface
}

func gcConversionSafepoint(src, dst types.Type) bool {
return isStringOrSlice(src) || isStringOrSlice(dst)
}

func isStringOrSlice(typ types.Type) bool {
switch typ := types.Unalias(typ).Underlying().(type) {
case *types.Slice:
return true
case *types.Basic:
return typ.Info()&types.IsString != 0
default:
return false
}
}

func basicKind(typ types.Type) types.BasicKind {
if basic, ok := types.Unalias(typ).Underlying().(*types.Basic); ok {
return basic.Kind()
}
return types.Invalid
}

func (p *context) publishGCRoot(b llssa.Builder, value ssa.Value, expr llssa.Expr) {
slots, ok := p.gcRoots[value]
if !ok || expr.IsNil() {
return
}
roots := b.GCRootPointers(expr)
if len(roots) != len(slots) {
panic("cl: inconsistent GC root layout")
}
for i, root := range roots {
b.SetGCRoot(slots[i], root)
}
}
153 changes: 153 additions & 0 deletions cl/gcroot_internal_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
//go:build !llgo

package cl

import (
"go/ast"
"go/importer"
"go/parser"
"go/token"
"go/types"
"testing"

"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
)

func TestGCSafepointClassification(t *testing.T) {
fn := buildGCRootSSAFunction(t, `package p
func helper()
func classify(p *int, text string, bytes []byte, ch chan int, m map[string]int, value any) {
_ = *p
_ = len(bytes)
_ = text + text
_ = string(bytes)
_ = value == value
helper()
_ = <-ch
m[text] = 1
}`)
seen := make(map[string]bool)
for _, block := range fn.Blocks {
for _, instr := range block.Instrs {
switch instr := instr.(type) {
case *ssa.UnOp:
switch instr.Op {
case token.MUL:
seen["deref"] = true
if gcSafepoint(instr) {
t.Error("pointer dereference classified as a safepoint")
}
case token.ARROW:
seen["receive"] = true
if !gcSafepoint(instr) {
t.Error("channel receive not classified as a safepoint")
}
}
case *ssa.BinOp:
switch basicKind(instr.X.Type()) {
case types.String:
seen["string operation"] = true
if !gcSafepoint(instr) {
t.Error("string operation not classified as a safepoint")
}
default:
if _, ok := instr.X.Type().Underlying().(*types.Interface); ok {
seen["interface comparison"] = true
if !gcSafepoint(instr) {
t.Error("interface comparison not classified as a safepoint")
}
}
}
case *ssa.Convert:
seen["string conversion"] = true
if !gcSafepoint(instr) {
t.Error("string conversion not classified as a safepoint")
}
case *ssa.Call:
if builtin, ok := instr.Call.Value.(*ssa.Builtin); ok && builtin.Name() == "len" {
seen["pure builtin"] = true
if gcSafepoint(instr) {
t.Error("len classified as a safepoint")
}
} else {
seen["call"] = true
if !gcSafepoint(instr) {
t.Error("call not classified as a safepoint")
}
}
case *ssa.MapUpdate:
seen["map update"] = true
if !gcSafepoint(instr) {
t.Error("map update not classified as a safepoint")
}
}
}
}
for _, want := range []string{
"deref", "receive", "string operation", "string conversion",
"interface comparison", "pure builtin", "call", "map update",
} {
if !seen[want] {
t.Errorf("%s instruction was not generated", want)
}
}
if !functionHasGCSafepoint(fn) {
t.Error("function with runtime operations has no GC safepoint")
}
}

func TestGCSafepointPureInstructions(t *testing.T) {
for _, instr := range []ssa.Instruction{
new(ssa.DebugRef),
new(ssa.Extract),
new(ssa.Field),
new(ssa.FieldAddr),
new(ssa.If),
new(ssa.Index),
new(ssa.IndexAddr),
new(ssa.Jump),
new(ssa.Phi),
new(ssa.Return),
new(ssa.Slice),
new(ssa.SliceToArrayPointer),
new(ssa.Store),
new(ssa.ChangeType),
} {
if gcSafepoint(instr) {
t.Errorf("%T classified as a safepoint", instr)
}
}
if !gcSafepoint(new(ssa.MakeSlice)) {
t.Error("unknown runtime-lowered instruction must stay conservative")
}
if gcConversionSafepoint(types.Typ[types.Int], types.Typ[types.Uint]) {
t.Error("numeric conversion classified as a safepoint")
}
pure := buildGCRootSSAFunction(t, `package p
func classify(p *int) *int { return p }
`)
if functionHasGCSafepoint(pure) {
t.Error("pure function has a GC safepoint")
}
}

func buildGCRootSSAFunction(t *testing.T, src string) *ssa.Function {
t.Helper()
fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "gcroot.go", src, 0)
if err != nil {
t.Fatal(err)
}
pkg, _, err := ssautil.BuildPackage(
&types.Config{Importer: importer.Default()},
fset,
types.NewPackage("gcroot", "p"),
[]*ast.File{file},
ssa.InstantiateGenerics,
)
if err != nil {
t.Fatal(err)
}
return pkg.Func("classify")
}
Loading
Loading