Skip to content
Merged
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
102 changes: 102 additions & 0 deletions cl/coro_call_site_plan.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,103 @@ func isCoroPythonIntrinsicOpcode(opcode int) bool {
}
}

type coroCallArgumentLifetimeUniverse interface {
canonicalAlias(*ssa.Function) *ssa.Function
sortedUseOwners(*ssa.Function) []*preparedEmissionPackage
functionABIContext(*ssa.Function, *preparedEmissionPackage) (*context, error)
freezeCoroCallableShape(*ssa.Function) (coroCallableFrozenShape, error)
}

func classifyCoroCallArgumentsBorrowedUntilCompletion(
universe coroCallArgumentLifetimeUniverse,
ctx *context,
caller *ssa.Function,
call ssa.CallInstruction,
) (bool, error) {
direct, ok := call.(*ssa.Call)
if !ok || direct.Common() == nil || direct.Common().IsInvoke() ||
direct.Common().Method != nil || ctx == nil || ctx.prog == nil || caller == nil {
return false, nil
}
callee := universe.canonicalAlias(direct.Common().StaticCallee())
if callee == nil || callee.Pkg == nil || caller.Pkg == nil ||
callee.Pkg != caller.Pkg || callee.Pkg != ctx.goPkg {
return false, nil
}
// A source stub may remain visible to x/tools SSA even though patch
// selection or reachability omitted its physical declaration. Such a call
// has no frozen ABI owner and therefore cannot receive an occurrence-level
// lifetime certificate; it must not make ProgramIR construction fail either.
if len(universe.sortedUseOwners(callee)) == 0 {
return false, nil
}
shape, err := universe.freezeCoroCallableShape(callee)
if err != nil {
return false, fmt.Errorf("freeze C argument lifetime for %q: %w", callee.Name(), err)
}
if shape.kind != cFunc {
return false, nil
}
parsed, present, err := coroCallableContractCertificateFor(callee)
if err != nil {
return false, fmt.Errorf("freeze C argument lifetime for %q: %w", callee.Name(), err)
}
contract := defaultCoroForeignDeclarationContract().Contract
if present {
if parsed.Scope != coroCallableContractScopeDeclaration {
return false, nil
}
contract = parsed.Contract
}
switch contract.Memory {
case coro.MemoryByValue, coro.MemoryBorrowUntilReturn:
return true, nil
case coro.MemoryBorrowUntilComplete:
return contract.Progress == coro.ProgressExecutorSafe ||
contract.Progress == coro.ProgressMayBlock, nil
default:
return false, nil
}
}

func coroCallArgumentsBorrowedUntilCompletion(
universe coroCallArgumentLifetimeUniverse,
call *ssa.Call,
) (bool, error) {
if universe == nil || call == nil || call.Parent() == nil {
return false, nil
}
caller := universe.canonicalAlias(call.Parent())
if caller == nil || caller != call.Parent() {
return false, nil
}
owners := universe.sortedUseOwners(caller)
if len(owners) == 0 {
return false, nil
}
borrowed, classified := false, false
for _, owner := range owners {
ctx, err := universe.functionABIContext(caller, owner)
if err != nil {
return false, err
}
candidate, err := classifyCoroCallArgumentsBorrowedUntilCompletion(universe, ctx, caller, call)
if err != nil {
return false, err
}
if classified && candidate != borrowed {
return false, fmt.Errorf(
"call %q has owner-dependent C argument lifetime", call.String(),
)
}
borrowed, classified = candidate, true
}
if !classified {
return false, nil
}
return borrowed, nil
}

// freezeCallSites is the final pre-SSAPlan ProgramIR builder stage. Runtime
// helper closure, patch redirects, physical identities, and worker
// certificates must already be immutable. The stage validates raw SSA exactly
Expand Down Expand Up @@ -392,6 +489,11 @@ func (ir *coroProgramIR) freezeCallSites(u *EmissionUniverse) error {
case intrinsic && classifyErr == nil && semantics.ElidesManagedCall() && plan.StaticSpawnTarget == nil:
plan.Elision = CoroCallElidedIntrinsic
}
if classifyErr == nil {
direct, _ := call.(*ssa.Call)
plan.ArgumentsBorrowedUntilCompletion, classifyErr =
coroCallArgumentsBorrowedUntilCompletion(u, direct)
}
if plan.ElidesCall() && intrinsic && classifyErr == nil && workerCertified {
if workerCertificate.ID == "" {
classifyErr = fmt.Errorf("freeze intrinsic elision certificate: certified call has an empty identity")
Expand Down
114 changes: 114 additions & 0 deletions cl/coro_frame_retention_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,26 @@ func Root(limit uint32) bool {
}
`

const coroForeignBorrowedAllocationFixture = `package foo

import "unsafe"

type result struct {
value uintptr
}

//go:linkname call C.coro_borrowed_allocation_fixture
func call(resultAddress uintptr) bool

func Root() uintptr {
var value result
if !call(uintptr(unsafe.Pointer(&value))) {
return 0
}
return value.value
}
`

func TestCoroBorrowedAllocationUsesPhysicalLocalStorage(t *testing.T) {
prog, ssaPkg, files, universe, plan := prepareCoroPreemptTestPlan(
t,
Expand Down Expand Up @@ -154,6 +174,100 @@ func TestCoroBorrowedAllocationUsesPhysicalLocalStorage(t *testing.T) {
}
}

func TestCoroForeignBorrowContractUsesPhysicalLocalStorage(t *testing.T) {
prog, ssaPkg, _, universe, plan := prepareCoroPreemptTestPlan(
t,
coroForeignBorrowedAllocationFixture,
[]coroRootFactoryTestRoot{{name: "Root", demand: coro.AsyncDemand}},
nil,
-1,
)
defer prog.Dispose()
root := ssaPkg.Func("Root")
var foreignCall *ssa.Call
for _, block := range root.Blocks {
for _, instruction := range block.Instrs {
call, ok := instruction.(*ssa.Call)
if ok && call.Common() != nil && call.Common().StaticCallee() != nil &&
call.Common().StaticCallee().Name() == "call" {
foreignCall = call
}
}
}
if foreignCall == nil {
t.Fatal("foreign-borrow fixture has no direct C call")
}
site, frozen, err := universe.CoroCallSitePlan(foreignCall)
if err != nil || !frozen || !site.ArgumentsBorrowedUntilCompletion {
t.Fatalf("foreign-borrow call SitePlan = %+v, frozen=%t, err=%v, samePkg=%t",
site, frozen, err, foreignCall.Common().StaticCallee().Pkg == ssaPkg)
}
audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "")
if err != nil {
t.Fatal(err)
}
proof := audit.currentFrameRetentionProof()
allocations := coroFrameRetentionHeapAllocs(root)
if len(allocations) != 1 || len(proof.borrowedAllocations) != 1 ||
len(proof.managedHeapAllocations) != 0 {
t.Fatalf("foreign-borrow fixture SSA/borrowed/managed allocations = %d/%d/%d, want 1/1/0",
len(allocations), len(proof.borrowedAllocations), len(proof.managedHeapAllocations))
}
borrow, retained := proof.borrowedAllocations[allocations[0]]
if !retained || borrow.FunctionsVisited == 0 {
t.Fatalf("foreign declaration did not retain an exact borrow proof: %+v", borrow)
}
if reason := audit.validateAlloc(allocations[0]); reason != "" {
t.Fatalf("foreign borrowed allocation rejected: %s", reason)
}
}

func TestCoroForeignRetainedContractRejectsBorrowedStorage(t *testing.T) {
source := strings.Replace(
coroForeignBorrowedAllocationFixture,
"//go:linkname call C.coro_borrowed_allocation_fixture",
"//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=retained\n//go:linkname call C.coro_borrowed_allocation_fixture",
1,
)
prog, ssaPkg, _, universe, plan := prepareCoroPreemptTestPlan(
t,
source,
[]coroRootFactoryTestRoot{{name: "Root", demand: coro.AsyncDemand}},
nil,
-1,
)
defer prog.Dispose()
root := ssaPkg.Func("Root")
var foreignCall *ssa.Call
for _, block := range root.Blocks {
for _, instruction := range block.Instrs {
call, ok := instruction.(*ssa.Call)
if ok && call.Common() != nil && call.Common().StaticCallee() != nil &&
call.Common().StaticCallee().Name() == "call" {
foreignCall = call
}
}
}
if foreignCall == nil {
t.Fatal("foreign-retained fixture has no direct C call")
}
site, frozen, err := universe.CoroCallSitePlan(foreignCall)
if err != nil || !frozen || site.ArgumentsBorrowedUntilCompletion {
t.Fatalf("foreign-retained call SitePlan = %+v, frozen=%t, err=%v; want conservative lifetime",
site, frozen, err)
}
audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "")
if err != nil {
t.Fatal(err)
}
proof := audit.currentFrameRetentionProof()
allocations := coroFrameRetentionHeapAllocs(root)
if len(allocations) != 1 || len(proof.borrowedAllocations) != 0 {
t.Fatalf("foreign-retained fixture SSA/borrowed/managed allocations = %d/%d/%d, want one SSA allocation and no borrow proof",
len(allocations), len(proof.borrowedAllocations), len(proof.managedHeapAllocations))
}
}

func TestCoroGenericParkStateRetentionIsSourceIndependent(t *testing.T) {
for _, symbol := range []string{"__llgo_coro_fixture_prepare", "__llgo_coro_another_source_prepare"} {
t.Run(symbol, func(t *testing.T) {
Expand Down
15 changes: 11 additions & 4 deletions cl/emission_runtime_helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -104,17 +104,20 @@ func planCoroPlainAllocation(ctx *context, allocation *ssa.Alloc) coroPlainAlloc
}

// coroBorrowedAllocationUniverse is the complete read-only authority needed by
// the interprocedural lifetime proof. Keeping this as a two-method view avoids
// the interprocedural lifetime proof. Keeping this as a narrow view avoids
// turning a local allocation classifier into another whole-program plan owner.
type coroBorrowedAllocationUniverse interface {
coroCallArgumentLifetimeUniverse
Resolve(*ssa.Function) (*ssa.Function, bool)
FunctionBackground(*ssa.Function) (llssa.Background, bool, error)
}

// proveCoroBorrowedAllocation admits only bodies which the frozen emission
// universe will actually compile as managed Go. Source bodies attached to C,
// Python, or LLVM intrinsic declarations are type-checking stubs rather than
// memory-semantics evidence and must never justify local storage.
// universe will actually compile as managed Go, or exact call-site plans which
// prove that a foreign call stops borrowing an argument no later than its
// logical completion. Source bodies attached to C, Python, or LLVM intrinsic
// declarations remain type-checking stubs and never constitute memory-
// semantics evidence themselves.
func proveCoroBorrowedAllocation(
universe coroBorrowedAllocationUniverse,
allocation *ssa.Alloc,
Expand All @@ -123,6 +126,10 @@ func proveCoroBorrowedAllocation(
return coro.SSABorrowedAllocationProof{Allocation: allocation}, false
}
return coro.ProveSSABorrowedAllocationWithConfig(allocation, coro.SSABorrowedAllocationConfig{
BorrowedCallArgument: func(call *ssa.Call, _ int) bool {
borrowed, err := coroCallArgumentsBorrowedUntilCompletion(universe, call)
return err == nil && borrowed
},
ResolveCalleeBody: func(function *ssa.Function) (*ssa.Function, bool) {
canonical, resolved := universe.Resolve(function)
if !resolved || canonical == nil || len(canonical.Blocks) == 0 {
Expand Down
7 changes: 7 additions & 0 deletions cl/emission_universe.go
Original file line number Diff line number Diff line change
Expand Up @@ -546,6 +546,13 @@ type CoroCallSitePlan struct {
// construction and is the only authority consumed by analysis, physical
// planning, lowering facts, and codegen.
ControlOperation CoroControlOperation
// ArgumentsBorrowedUntilCompletion is an occurrence-level lifetime fact for
// one exact ordinary call to a source-visible C declaration in the caller's
// package. It permits caller-owned storage to live in the native/coroutine
// frame only when the frozen callable contract ends every argument borrow no
// later than this logical call completes. Dynamic, deferred, spawned,
// cross-package, async-completion, and retained-memory calls keep this false.
ArgumentsBorrowedUntilCompletion bool
// RawPlainSynchronousIntrinsic proves that this exact direct llgo.syscall
// occurrence has the uintptr word ABI accepted by ordinary synchronous
// intrinsic lowering. It authorizes only a separately proven raw/plain
Expand Down
75 changes: 75 additions & 0 deletions doc/coro-performance-baseline.md
Original file line number Diff line number Diff line change
Expand Up @@ -1888,3 +1888,78 @@ file I/O, direct file syscalls, a sole-M blocking pipe whose writer first parks
on a real timer, and loopback TCP. Runtime-core tests cover allocation-elided
and dynamic destruction, panic/recover compatibility, slow yield, nesting, and
the depth-bound scheduler fallback.

### Escape-free scalar synchronous native-call checkpoint

The 2026-08-15 follow-up starts at merge
`b48265391044cf8e20f160e550f1abfcf4fa8b98`; the measured implementation is
`9f0470cd1`. Profiling the standard-file fixture showed that every
compiler-certified native syscall created two
collector allocations before entering the kernel: one for the nine-word
argument record and one for the six-word result record. Their addresses crossed
the Go-to-C ABI in `coroworker.Call`, so LLGo's conservative escape decision was
correct; wrapping the pointers in a runtime `noescape` helper did not change the
SSA allocation class.

The retained implementation replaces that Go-visible pointer ABI with a local
runtime declaration of `__llgo_coro_worker_call_words_v2`. Its nine arguments
and the address of one caller-owned result record cross as scalar words. The C
wrapper owns the argument array and calls the old pointer-taking leaf entirely
inside the native worker island; no Go declaration can select that leaf.

Scalar encoding alone is not an escape proof: x/tools SSA still conservatively
marks the result record as heap storage because its address reaches a bodyless
C declaration. The compiler now freezes an occurrence-level argument-lifetime
fact and feeds it into the generic interprocedural borrowed-allocation proof.
Only an ordinary static call to a same-package, owner-backed C declaration can
qualify, and its callable contract must end the borrow by return or synchronous
completion. Retained-memory, asynchronous-completion, cross-package, dynamic,
deferred, spawned, ownerless, and unresolved calls fail closed. Unit tests cover
the positive call and an explicit `memory=retained` rejection; no new source
annotation is needed for this runtime leaf.

A seven-word C struct return was also tested and rejected before retention:
separately compiled Mach-O objects disagreed about the hidden `sret` ABI and
faulted despite a full-LTO fixture appearing to work. Keeping the result address
as an explicit `uintptr` gives the cross-object boundary an all-scalar ABI and
the independent-link E2Es guard it. Fault attribution, native handoff, lazy
compensation, and result/fault interpretation are otherwise unchanged.

The parent and candidate were built from the same `io_workload` source with
independent caches, full LTO, stripped output, Go 1.26.5, and LLVM 22.1.8 on
Darwin arm64. Nine AB/BA-interleaved 5,000-operation standard-file runs produced
these hardware-counter medians:

| Metric | fused parent | scalar-ABI candidate | delta |
| --- | ---: | ---: | ---: |
| retired instructions | 1,409,175,860 | 1,256,015,357 | -10.87% |
| cycles | 471,775,229 | 226,316,522 | -52.03% |
| stripped file bytes | 7,014,704 | 7,014,768 | +64 (+0.001%) |
| Mach-O `__text` bytes | 3,137,408 | 3,137,244 | -164 (-0.005%) |

These are fresh-cache rebuilds of the exact parent and candidate; earlier
temporary binaries were discarded after their runtime archives were found not
to be comparable. Retired-instruction ranges were
1,276,319,938--1,417,813,058 for the parent and
1,252,119,179--1,257,990,658 for the candidate. Candidate cycles were tightly
grouped at 221,952,862--229,185,984, while parent cycles ranged from
257,958,534 to 515,229,856 as its allocation and collection work varied. The
cycle reduction is therefore a strong same-host observation, not a portable
latency claim.

An independent 15-process AB/BA wall-time gate measured the standard-file
fixture at 42.658 ms [41.355, 44.624] versus 53.021 ms [44.064, 53.907] for
the parent (-19.54%) and 7.740 ms [7.644, 8.172] for Go (5.51x Go). Direct
`syscall` file I/O improved from 21.684 ms [21.102, 22.030] to 18.890 ms
[18.223, 19.222] (-12.88%, 2.44x Go). Loopback TCP remained within noise at
19.741 ms versus 19.729 ms for the parent because it already uses the
nonblocking poll/event path; Go measured 9.561 ms in the same run.

Disassembly of `coroNativeForeignWordCallV1` changes two runtime GC allocation
calls into one stack result area plus `__llgo_coro_worker_call_words_v2`. The
reentry-capable same-M path changes three allocations to one; its remaining
104-byte boundary is temporarily published in TLS for C-to-Go callbacks and is
not on the file/syscall path. Small standard-file, direct-syscall, sole-M
blocking-pipe/timer, and loopback-TCP execution gates pass with
`GOMAXPROCS=1`, as do separately linked same-M callback, locked-thread
compensation, deferred replacement, and worker-capability E2Es.
2 changes: 1 addition & 1 deletion internal/build/coro_native_target_plan_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -633,7 +633,7 @@ func TestRealNativeCoroTargetIsTrustedPlainSchedulerIsland(t *testing.T) {
"__llgo_coro_worker_queue_init_v1": false,
"__llgo_coro_worker_queue_stop_v1": false,
"__llgo_coro_worker_queue_destroy_after_join_v1": false,
"__llgo_coro_worker_call_v1": false,
"__llgo_coro_worker_call_words_v2": false,
"__llgo_coro_worker_queue_reserve_v2": false,
"__llgo_coro_worker_queue_cancel_reservation_v2": false,
"__llgo_coro_worker_queue_submit_reserved_v4": false,
Expand Down
Loading