diff --git a/.github/workflows/coroutine.yml b/.github/workflows/coroutine.yml new file mode 100644 index 0000000000..61ddfdf44f --- /dev/null +++ b/.github/workflows/coroutine.yml @@ -0,0 +1,255 @@ +name: Coroutine + +on: + pull_request: + branches: + - main + - llvm-coro + - "coro/**" + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + test: + name: ${{ matrix.check }} + runs-on: ubuntu-22.04 + timeout-minutes: 20 + strategy: + fail-fast: false + matrix: + include: + - { llvm: 19, go: "1.26.5", tags: "llvm19", lane: "compat", check: "test (19, 1.26.5, llvm19)" } + - { llvm: 20, go: "1.26.5", tags: "llvm20", lane: "compat", check: "test (20, 1.26.5, llvm20)" } + - { llvm: 21, go: "1.26.5", tags: "llvm21", lane: "compat", check: "test (21, 1.26.5, llvm21)" } + - { llvm: 22, go: "1.26.5", tags: "llvm22", lane: "compat", check: "test (22, 1.26.5, llvm22)" } + - { llvm: 19, go: "1.26.5", tags: "llvm19", lane: "integration", check: "test integration (19, 1.26.5, llvm19)" } + - { llvm: 19, go: "1.26.5", tags: "llvm19", lane: "targets", check: "test targets (19, 1.26.5, llvm19)" } + steps: + - uses: actions/checkout@v7 + + - name: Install LLVM + run: | + echo 'deb http://apt.llvm.org/jammy/ llvm-toolchain-jammy-${{ matrix.llvm }} main' | sudo tee /etc/apt/sources.list.d/llvm.list + wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - + sudo apt-get update + sudo apt-get install --no-install-recommends llvm-${{ matrix.llvm }}-dev clang-${{ matrix.llvm }} lld-${{ matrix.llvm }} libgc-dev + echo '/usr/lib/llvm-${{ matrix.llvm }}/bin' >> "$GITHUB_PATH" + + - name: Set up Go + uses: ./.github/actions/setup-go + with: + go-version: ${{ matrix.go }} + + # This focused LLVM/runtime matrix complements the regular upstream Go, + # cache, LLGo and target workflows, including their macOS lanes. + - name: Test coroutine analysis + if: matrix.lane == 'integration' + run: go test -race -shuffle=on ./internal/coro + + - name: Test target-neutral coroutine runtime core + if: matrix.lane == 'compat' + run: | + cd runtime + go test -race -shuffle=on ./internal/coroalloc -count=1 + go test -race -shuffle=on ./internal/coro -count=1 + go test -race -shuffle=on ./internal/corodoorbell -count=1 + # The run-decision wrapper needs the production coro package but a + # test-local abort shim, so select its two sources instead of loading + # the complete LLGo runtime package into the host Go runtime. + go test -race -shuffle=on -tags=coro_run_decision_abi_test \ + ./internal/runtime/coro_run_decision.go \ + ./internal/runtime/coro_run_decision_test.go \ + -run '^Test(CoroRunDecisionOutputModeV1|NormalCoroRunDecisionWordsV1|CoroRunDecisionWrapperRejectsMalformedNormalOnlyMode)$' -count=1 + go test . -run '^(TestCoroNativeTargetBuildSelection|TestCoroTimerOwnerV2SourceABI|TestTimeSleep)' -count=1 + # The complete LLGo runtime package intentionally owns symbols that + # collide with the host Go runtime. Use the real production adapter + # sources plus test-only definitions of the compiler-owned C wrappers + # to exercise begin/run/destroy without adding production callbacks. + # Compiler tests separately cover the LLVM/LLGo side; this is not a + # cross-language linked smoke test. + go test -race -shuffle=on -tags=coro_runtime_adapter_test \ + ./internal/runtime/coro_run_slice.go \ + ./internal/runtime/coro_program.go \ + ./internal/runtime/coro_sched.go \ + ./internal/runtime/coro_executor.go \ + ./internal/runtime/coro_panic_payload.go \ + ./internal/runtime/coro_executor_driver_legacy.go \ + ./internal/runtime/coro_ready_distribution_default.go \ + ./internal/runtime/coro_target_executor_retired_default.go \ + ./internal/runtime/coro_target_test_adapter.go \ + ./internal/runtime/coro_program_test.go \ + -run '^TestCoroProgram' -count=1 + # The native fleet is a deliberately isolated production adapter: + # select only its real source plus its host test so LLGo runtime + # symbols do not collide with the host Go runtime. + go test -race -shuffle=on -tags=coro_native_fleet_test \ + ./internal/runtime/coro_run_slice.go \ + ./internal/runtime/coro_ready_distribution_default.go \ + ./internal/runtime/coro_native_fleet.go \ + ./internal/runtime/coro_native_fleet_reactor.go \ + ./internal/runtime/coro_native_fleet_test.go \ + -run '^TestCoroNativeFleet' -count=1 + GOOS=js GOARCH=wasm CGO_ENABLED=0 go test \ + -tags=coro_runtime_adapter_test \ + -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" \ + ./internal/runtime/coro_run_slice.go \ + ./internal/runtime/coro_program.go \ + ./internal/runtime/coro_sched.go \ + ./internal/runtime/coro_executor.go \ + ./internal/runtime/coro_panic_payload.go \ + ./internal/runtime/coro_executor_driver_legacy.go \ + ./internal/runtime/coro_ready_distribution_default.go \ + ./internal/runtime/coro_target_executor_retired_default.go \ + ./internal/runtime/coro_target_test_adapter.go \ + ./internal/runtime/coro_program_test.go \ + -run '^TestCoroProgram' -count=1 + # Exercise the production typed hchan queue and exact coroutine + # source transaction together. The host atomic shim exercises the + # portable no-suspend channel gate without an OS mutex dependency. + go test -race -shuffle=on -tags='coro_channel_adapter_test,coro_channel_owner_test' \ + ./internal/runtime/z_chan.go \ + ./internal/runtime/z_chan_coro.go \ + ./internal/runtime/z_chan_lock_coro.go \ + ./internal/runtime/z_chan_lock_coro_atomic_host.go \ + ./internal/runtime/coro_channel_adapter_test.go \ + ./internal/runtime/coro_channel_owner_lock_test.go \ + -run '^TestCoroChannelAdapter' -count=1 + GOOS=js GOARCH=wasm CGO_ENABLED=0 go test \ + -tags='coro_channel_adapter_test,coro_channel_owner_test' \ + -exec="$(go env GOROOT)/lib/wasm/go_js_wasm_exec" \ + ./internal/runtime/z_chan.go \ + ./internal/runtime/z_chan_coro.go \ + ./internal/runtime/z_chan_lock_coro.go \ + ./internal/runtime/z_chan_lock_coro_atomic_host.go \ + ./internal/runtime/coro_channel_adapter_test.go \ + ./internal/runtime/coro_channel_owner_lock_test.go \ + -run '^TestCoroChannelAdapter' -count=1 + # Semaphore and notify waits share one POD-only keyed registry. Race + # the exact production registry source to lock FIFO selection, + # generation reuse, cancellation, and single-owner posting. + go test -race -shuffle=on -tags=coro_sema_owner_test \ + ./internal/runtime/z_chan_lock_coro.go \ + ./internal/runtime/z_chan_lock_coro_atomic_host.go \ + ./internal/runtime/coro_keyed_park.go \ + ./internal/runtime/coro_keyed_post_test.go \ + ./internal/runtime/coro_keyed_registry_test.go \ + -run '^TestCoroKeyed' -count=1 + go test ./internal/corotimer -run '^TestDeadlineAfter$' -count=1 + + - name: Link named freestanding WebAssembly targets + if: matrix.lane == 'targets' + env: + LLGO_WASM_TARGET_SMOKE: "1" + run: | + go test -v ./internal/crosscompile -run '^TestFreestandingWasmTargetToolchainSmoke$' -count=1 + go build -o /tmp/llgo-wasm-target ./cmd/llgo + for target in wasip2 wasm-unknown; do + output="/tmp/llgo-${target}.wasm" + LLGO_BUILD_CACHE=off LDFLAGS='--export=main' \ + /tmp/llgo-wasm-target build -target="$target" -o "$output" \ + ./internal/crosscompile/testdata/wasm_allocator + test "$(od -An -t x1 -N4 "$output" | tr -d ' \n')" = '0061736d' + symbols="$(llvm-nm --defined-only --format=just-symbols "$output")" + for symbol in main malloc free sbrk abort; do + grep -Fx "$symbol" <<<"$symbols" + done + ! grep -E '^GC_' <<<"$symbols" + test -z "$(llvm-nm --undefined-only --format=just-symbols "$output")" + if command -v wasmtime >/dev/null; then + result="$(wasmtime run --invoke main "$output" 0 0)" + test "$result" = '0' + fi + done + + - name: Compile coroutine runtime adapter across targets + if: matrix.lane == 'targets' + run: | + cd runtime + GOOS=js GOARCH=wasm CGO_ENABLED=0 go test -c -o /tmp/coro-js-wasm.test ./internal/runtime + GOOS=wasip1 GOARCH=wasm CGO_ENABLED=0 go test -c -o /tmp/coro-wasip1-wasm.test ./internal/runtime + GOOS=linux GOARCH=arm CGO_ENABLED=0 go test -c -o /tmp/coro-linux-arm.test ./internal/runtime + GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 go test -c -o /tmp/coro-linux-riscv64.test ./internal/runtime + GOOS=linux GOARCH=arm64 CGO_ENABLED=0 go test -c -o /tmp/coro-doorbell-linux-arm64.test ./internal/corodoorbell + GOOS=linux GOARCH=riscv64 CGO_ENABLED=0 go test -c -o /tmp/coro-doorbell-linux-riscv64.test ./internal/corodoorbell + GOOS=linux GOARCH=arm CGO_ENABLED=0 go test -c -tags='baremetal cortexm' -o /tmp/coro-cortexm-baremetal.test ./internal/runtime + + - name: Test coroutine build integration + if: matrix.lane == 'integration' + # Keep the focused workflow exhaustive for the build-side coroutine + # contract. This includes park effect seeding, frozen foreign noblock + # certificates, IRQUnsafe handling, the explicit-status panic stop, and + # the native linked static-spawn scheduler-island execution smoke. The + # timer checks have their own LLVM 19-22 steps below, so do not run them + # twice in the LLVM 19 job. + run: go test ./internal/build -run 'Coro|Coroutine' -skip '^TestCoroNativeTimeSleepProductionPlanAndCodegen$' -timeout=10m -count=1 + + - name: Verify production time.Sleep coroutine plan + if: matrix.lane == 'compat' + run: go test -tags='${{ matrix.tags }}' -v ./internal/build -run '^TestCoroNativeTimeSleepProductionPlanAndCodegen$' -timeout=10m -count=1 + + - name: Test coroutine compiler integration + if: matrix.lane == 'integration' + run: | + go test -race ./cl/ssawrap -count=1 + go test -race ./cl -run '^Test(CompilationCoroPlanObservationAndCacheRegistration|CoroEntryResolutionPlainPrimaryPreservesIR|ResolveFunctionSymbolUsesPrimaryAndExactPlan|CoroEntryRejectsUnsupportedBeforeCreatingSymbol|CoroEntryResolutionPreflightRejectsWholePlanBeforeCodegen|CoroEntryResolutionPreflightRejectsMissingPlanAndCache|Emission.*)$' -count=1 + # Compile and execute one ordinary program through LLGo so the new + # runtime glue is checked by LLGo itself, not only by the host Go compiler. + go test ./cl -run '^TestRunAndTestFromTestgo/print$' -count=1 + + - name: Test structured LLVM coroutine builder + if: matrix.lane == 'compat' + run: go test -tags='${{ matrix.tags }}' -v ./ssa -run '^TestCoro' -count=1 + + - name: Test canonical coroutine plan digest and cache identity + if: matrix.lane == 'compat' + run: | + go test -tags='${{ matrix.tags }}' ./internal/coro -run '^TestCoroPlanDigest' -count=1 + go test -tags='${{ matrix.tags }}' ./internal/build -run '^Test(BuildCoroPlanInstallsArchiveDigest|CoroutinePlanInputsAffectFingerprint|CoroEntryResolutionUsesPlanMatchedPackageCache|CoroPlanDigestMetadataUsesEffectiveLLVMTarget|CoroPhysicalABICacheRegistrationPreservesCollectedFuncInfo)$' -count=1 + go test -tags='${{ matrix.tags }}' ./cl -run '^Test(CompilationCoroABIIdentityValidation|CoroEntryResolutionCacheRegistrationWithDigest|CoroPhysicalABICacheRegistrationPreservesPhysicalMetadata)$' -count=1 + + - name: Test coroutine physical ABI and function dispatch lowering + if: matrix.lane == 'compat' + # Run every compiler test whose name is part of the coroutine contract; + # in particular this covers pure SSA aggregates/PHI and caller-frame + # park lowering on native64 and wasm32 before and after CoroSplit. + run: go test -tags='${{ matrix.tags }}' -v ./cl -run '^Test(Coro|EmissionUniverse(ActiveABIMethodTablesUseFrozenWrapperSymbols|ABIMethodDemandReferencesAreExactRecursiveAndOwnerScoped))' -count=1 + + - name: Test coroutine TLS function dispatch proof + if: matrix.lane == 'compat' + run: go test -tags='${{ matrix.tags }}' -v ./internal/build -run '^TestCoroTLS' -count=1 + + - name: Test coroutine registry and control integration + if: matrix.lane == 'compat' + run: go test -tags='${{ matrix.tags }}' -v ./internal/build -run '^Test(CollectLinkedCoroRootAnchors|ActiveCoroABIVersions|BuildCoroPlanErrors|CoroProgramManifest.*|CoroProgramBootstrap.*|SelectCoroProgramBootstrap.*|GenMainModule.*Coro.*)$' -count=1 + + - name: Test LLVM 22 tool configuration + if: matrix.lane == 'compat' && matrix.llvm == 22 + run: go test -tags=llvm22 ./xtool/env/llvm ./internal/xtool/llvm + + - name: Test resolved LLVM target configuration + if: matrix.lane == 'targets' + run: | + go test ./internal/xtool/llvm -run '^TestGetTarget(Spec|Triple)$' -count=1 + go test ./internal/crosscompile -run '^Test(UseTarget|UseExportsResolvedLLVMConfig|ResolvedLLVMTargetSpecWASIThreads)$' -count=1 + go test ./internal/cabi -run '^TestDevLTOGlobalDCETargetArchAndNewTransformerArchSelection$' -count=1 + go test -v ./ssa -run '^Test(ResolvedTargetConfig(|IsAuthoritativeAndFrozen)|TargetDataLegacyLayoutCompatibility|ResolvedPointerWidthMismatchFallsBack|NewProgramDefaultTargetCompatibility|ResolvedExternalBackendCompatibilityFallback|ResolvedTargetABINameControlsRISCVObject)$' -count=1 + go test ./internal/build -run '^Test(NewLLSSATargetUsesResolvedLLVMConfig|LLVMCPUAndFeaturesAffectBuildFingerprint|DefaultTargetKeepsLegacyCacheIdentity|NonDefaultLLVMFeaturesEnterCacheIdentity|ResolvedTargetCompatibilityAudit)$' -count=1 + + - name: Check llgo-tag build + if: matrix.lane == 'integration' + run: go test -tags=llgo ./internal/coro + + - name: Vet coroutine analysis + if: matrix.lane == 'integration' + run: | + go vet ./internal/coro ./internal/build ./cl/ssawrap ./internal/xtool/llvm ./internal/crosscompile ./internal/cabi + # The runtime package has pre-existing unsafe.Pointer findings. + (cd runtime && go vet ./internal/coro && go vet -unsafeptr=false ./internal/runtime) + # The compiler package has a pre-existing unsafe.Pointer finding. + # Disable only that analyzer and keep all other checks enabled. + go vet -unsafeptr=false ./cl + # The SSA package has pre-existing sync.Map copylocks findings. Keep + # every other analyzer active while coroutine slices are integrated. + go vet -copylocks=false ./ssa diff --git a/.github/workflows/fmt.yml b/.github/workflows/fmt.yml index bde48ad92f..10c6acafe1 100644 --- a/.github/workflows/fmt.yml +++ b/.github/workflows/fmt.yml @@ -32,8 +32,13 @@ jobs: pushd "$dir" >/dev/null # gofmt won't traverse directories that start with '_' or 'testdata', # so mirror dev/local_ci.sh and scan every Go file explicitly. + # pthread.go intentionally keeps c2go's spaced `// llgo:link` + # directive adjacent to `//llgo:coro`; gofmt splits that single + # FuncDecl doc group and changes the compiler-visible contract. fmt_output="$( - find . -name '*.go' -type f ! -name 'xgo_autogen.go' -print0 \ + find . -name '*.go' -type f ! -name 'xgo_autogen.go' \ + ! -path './runtime/internal/clite/pthread/pthread.go' \ + ! -path './internal/clite/pthread/pthread.go' -print0 \ | xargs -0 gofmt -l \ | sed 's|^\\./||' \ || true diff --git a/.github/workflows/targets.yml b/.github/workflows/targets.yml index 6eed98a333..b7cfb6c01d 100644 --- a/.github/workflows/targets.yml +++ b/.github/workflows/targets.yml @@ -15,7 +15,10 @@ concurrency: jobs: llgo: - timeout-minutes: 30 + # The empty and defer probes compile the complete embedded target catalog + # sequentially. Coroutine planning makes the combined lane exceed the old + # 30-minute budget on hosted runners even while targets keep succeeding. + timeout-minutes: 45 strategy: matrix: os: diff --git a/_demo/embed/targetsbuild/build.sh b/_demo/embed/targetsbuild/build.sh index 23171f9014..d97d30d4b7 100755 --- a/_demo/embed/targetsbuild/build.sh +++ b/_demo/embed/targetsbuild/build.sh @@ -215,9 +215,27 @@ for target in "${targets_to_build[@]}"; do continue fi - output=$(../../../dev/llgo.sh build -target $target -o hello.elf "./$test_dir" 2>&1) - if [ $? -eq 0 ]; then - echo ✅ $target `file hello.elf` + # The named wasm target is the Emscripten environment, not the freestanding + # wasm-unknown or WASI target. Keep target resolution honest and report a + # missing optional SDK as a capability warning; dedicated WebAssembly jobs + # exercise the toolchain-independent wasm targets. + if [[ "$target" == "wasm" ]] && ! command -v emcc >/dev/null 2>&1; then + echo ⚠️ $target "(Emscripten SDK is not installed; wasm-unknown and WASI are tested separately)" + warned_targets+=("$target") + continue + fi + + if output=$(../../../dev/llgo.sh build -target "$target" -o hello.elf "./$test_dir" 2>&1); then + artifact=hello.elf + if [[ "$target" == "wasm" ]]; then + artifact=hello.elf.wasm + fi + if [ ! -f "$artifact" ]; then + echo ❌ "$target (build succeeded without expected artifact $artifact)" + failed_targets+=("$target") + continue + fi + echo ✅ "$target $(file "$artifact")" successful_targets+=("$target") else # Check if output contains warning messages diff --git a/cl/_testdata/llgosyscall/in.go b/cl/_testdata/llgosyscall/in.go index ccb613354b..2cdf867752 100644 --- a/cl/_testdata/llgosyscall/in.go +++ b/cl/_testdata/llgosyscall/in.go @@ -12,10 +12,13 @@ func syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) //go:linkname syscall6X llgo.syscall func syscall6X(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2, err uintptr) +//go:linkname syscall32 llgo.syscall32 +func syscall32(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) + //go:linkname syscall5f64 llgo.syscall func syscall5f64(fn, a1, a2, a3, a4, a5 uintptr, f1 float64) (r1, r2, err uintptr) -//go:linkname syscallPtr llgo.syscall +//go:linkname syscallPtr llgo.syscallPtr func syscallPtr(fn, a1, a2, a3 uintptr) (r1, r2, err uintptr) //go:linkname rawSyscall llgo.syscall @@ -36,6 +39,15 @@ func Use() uintptr { return r1 } +// CHECK-LABEL: define i64 @"{{.*}}/llgosyscall.Use32"(){{.*}} { +// CHECK: %[[R:[0-9]+]] = call i64 null(i64 1, i64 2, i64 3) +// CHECK: %[[LOW:[0-9]+]] = trunc i64 %[[R]] to i32 +// CHECK: %{{[0-9]+}} = icmp eq i32 %[[LOW]], -1 +func Use32() uintptr { + r1, _, _ := syscall32(0, 1, 2, 3) + return r1 +} + // CHECK-LABEL: define i64 @"{{.*}}/llgosyscall.Use5F64"(i64 %0, double %1){{.*}} { // CHECK: %{{[0-9]+}} = inttoptr i64 %0 to ptr // CHECK: %{{[0-9]+}} = call i64 %{{[0-9]+}}(i64 1, i64 2, i64 3, i64 4, i64 5, double %1) @@ -79,7 +91,7 @@ func Use6X() uintptr { // CHECK-LABEL: define i64 @"{{.*}}/llgosyscall.UsePtr"(){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: %0 = call i64 null(i64 1, i64 2, i64 3) -// CHECK-NEXT: %1 = icmp eq i64 %0, -1 +// CHECK-NEXT: %1 = icmp eq i64 %0, 0 // CHECK-NEXT: %2 = call i32 @cliteErrno() // CHECK-NEXT: %3 = sext i32 %2 to i64 // CHECK-NEXT: %4 = select i1 %1, i64 %3, i64 0 diff --git a/cl/_testgo/selects/in.go b/cl/_testgo/selects/in.go index 5f69de4d6a..33f655ec6b 100644 --- a/cl/_testgo/selects/in.go +++ b/cl/_testgo/selects/in.go @@ -102,7 +102,7 @@ func main() { // CHECK-NEXT: %32 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %31, ptr %30, 1 // CHECK-NEXT: %33 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %32, i32 0, 2 // CHECK-NEXT: %34 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %33, i1 false, 3 -// CHECK-NEXT: %35 = alloca i8, i64 48, align 1 +// CHECK-NEXT: %35 = alloca %"{{.*}}/runtime/internal/runtime.ChanOp", i64 2, align 8 // CHECK-NEXT: %36 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %35, i64 0 // CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.ChanOp" %29, ptr %36, align 8 // CHECK-NEXT: %37 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %35, i64 1 @@ -185,7 +185,7 @@ func main() { // CHECK-NEXT: %20 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %19, ptr %18, 1 // CHECK-NEXT: %21 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %20, i32 0, 2 // CHECK-NEXT: %22 = insertvalue %"{{.*}}/runtime/internal/runtime.ChanOp" %21, i1 false, 3 -// CHECK-NEXT: %23 = alloca i8, i64 48, align 1 +// CHECK-NEXT: %23 = alloca %"{{.*}}/runtime/internal/runtime.ChanOp", i64 2, align 8 // CHECK-NEXT: %24 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %23, i64 0 // CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.ChanOp" %17, ptr %24, align 8 // CHECK-NEXT: %25 = getelementptr %"{{.*}}/runtime/internal/runtime.ChanOp", ptr %23, i64 1 diff --git a/cl/_testrt/tpunsafe/in.go b/cl/_testrt/tpunsafe/in.go index 7ac3230544..ba36f2d611 100644 --- a/cl/_testrt/tpunsafe/in.go +++ b/cl/_testrt/tpunsafe/in.go @@ -33,31 +33,29 @@ func main() { // CHECK-LABEL: define linkonce void @"{{.*}}/cl/_testrt/tpunsafe.(*M[bool]).check"(ptr %0, i64 %1, i64 %2, i64 %3){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %4 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[bool]", ptr %0, i32 0, i32 2 -// CHECK-NEXT: %5 = load %"{{.*}}/cl/_testrt/tpunsafe.N[bool]", ptr %4, align 1 -// CHECK-NEXT: %6 = icmp ne i64 1, %1 -// CHECK-NEXT: br i1 %6, label %_llgo_1, label %_llgo_2 +// CHECK-NEXT: %4 = icmp ne i64 1, %1 +// CHECK-NEXT: br i1 %4, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 1) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 1) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @1, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 %1) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %7 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }, ptr %7, align 8 -// CHECK-NEXT: %8 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %7, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %8) +// CHECK-NEXT: %5 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }, ptr %5, align 8 +// CHECK-NEXT: %6 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %5, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %6) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[bool]", ptr %0, i32 0, i32 2 -// CHECK-NEXT: %10 = load %"{{.*}}/cl/_testrt/tpunsafe.N[bool]", ptr %9, align 1 -// CHECK-NEXT: %11 = icmp ne i64 8, %2 -// CHECK-NEXT: br i1 %11, label %_llgo_3, label %_llgo_4 +// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[bool]", ptr %0, i32 0, i32 2 +// CHECK-NEXT: %8 = load %"{{.*}}/cl/_testrt/tpunsafe.N[bool]", ptr %7, align 1 +// CHECK-NEXT: %9 = icmp ne i64 8, %2 +// CHECK-NEXT: br i1 %9, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 4 }) @@ -68,18 +66,18 @@ func main() { // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 %2) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %12 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %12, align 8 -// CHECK-NEXT: %13 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %12, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %13) +// CHECK-NEXT: %10 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %10, align 8 +// CHECK-NEXT: %11 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %10, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %11) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_2 -// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[bool]", ptr %0, i32 0, i32 2 -// CHECK-NEXT: %15 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.N[bool]", ptr %14, i32 0, i32 1 -// CHECK-NEXT: %16 = load i1, ptr %15, align 1 -// CHECK-NEXT: %17 = icmp ne i64 1, %3 -// CHECK-NEXT: br i1 %17, label %_llgo_5, label %_llgo_6 +// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[bool]", ptr %0, i32 0, i32 2 +// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.N[bool]", ptr %12, i32 0, i32 1 +// CHECK-NEXT: %14 = load i1, ptr %13, align 1 +// CHECK-NEXT: %15 = icmp ne i64 1, %3 +// CHECK-NEXT: br i1 %15, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_4 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 4 }) @@ -90,10 +88,10 @@ func main() { // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 %3) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %18 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %18, align 8 -// CHECK-NEXT: %19 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %18, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %19) +// CHECK-NEXT: %16 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %16, align 8 +// CHECK-NEXT: %17 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %16, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %17) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_4 @@ -116,31 +114,29 @@ func (m *M[T]) check(align, offset1, offset2 uintptr) { // CHECK-LABEL: define linkonce void @"{{.*}}/cl/_testrt/tpunsafe.(*M[int64]).check"(ptr %0, i64 %1, i64 %2, i64 %3){{.*}} { // CHECK-NEXT: _llgo_0: -// CHECK-NEXT: %4 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[int64]", ptr %0, i32 0, i32 2 -// CHECK-NEXT: %5 = load %"{{.*}}/cl/_testrt/tpunsafe.N[int64]", ptr %4, align 8 -// CHECK-NEXT: %6 = icmp ne i64 8, %1 -// CHECK-NEXT: br i1 %6, label %_llgo_1, label %_llgo_2 +// CHECK-NEXT: %4 = icmp ne i64 8, %1 +// CHECK-NEXT: br i1 %4, label %_llgo_1, label %_llgo_2 // CHECK-EMPTY: // CHECK-NEXT: _llgo_1: ; preds = %_llgo_0 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintInt"(i64 8) +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 8) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @1, i64 4 }) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 %1) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %7 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }, ptr %7, align 8 -// CHECK-NEXT: %8 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %7, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %8) +// CHECK-NEXT: %5 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @2, i64 20 }, ptr %5, align 8 +// CHECK-NEXT: %6 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %5, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %6) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_2: ; preds = %_llgo_0 -// CHECK-NEXT: %9 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[int64]", ptr %0, i32 0, i32 2 -// CHECK-NEXT: %10 = load %"{{.*}}/cl/_testrt/tpunsafe.N[int64]", ptr %9, align 8 -// CHECK-NEXT: %11 = icmp ne i64 16, %2 -// CHECK-NEXT: br i1 %11, label %_llgo_3, label %_llgo_4 +// CHECK-NEXT: %7 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[int64]", ptr %0, i32 0, i32 2 +// CHECK-NEXT: %8 = load %"{{.*}}/cl/_testrt/tpunsafe.N[int64]", ptr %7, align 8 +// CHECK-NEXT: %9 = icmp ne i64 16, %2 +// CHECK-NEXT: br i1 %9, label %_llgo_3, label %_llgo_4 // CHECK-EMPTY: // CHECK-NEXT: _llgo_3: ; preds = %_llgo_2 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 4 }) @@ -151,18 +147,18 @@ func (m *M[T]) check(align, offset1, offset2 uintptr) { // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 %2) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %12 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %12, align 8 -// CHECK-NEXT: %13 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %12, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %13) +// CHECK-NEXT: %10 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %10, align 8 +// CHECK-NEXT: %11 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %10, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %11) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_4: ; preds = %_llgo_2 -// CHECK-NEXT: %14 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[int64]", ptr %0, i32 0, i32 2 -// CHECK-NEXT: %15 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.N[int64]", ptr %14, i32 0, i32 1 -// CHECK-NEXT: %16 = load i64, ptr %15, align 8 -// CHECK-NEXT: %17 = icmp ne i64 8, %3 -// CHECK-NEXT: br i1 %17, label %_llgo_5, label %_llgo_6 +// CHECK-NEXT: %12 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.M[int64]", ptr %0, i32 0, i32 2 +// CHECK-NEXT: %13 = getelementptr inbounds %"{{.*}}/cl/_testrt/tpunsafe.N[int64]", ptr %12, i32 0, i32 1 +// CHECK-NEXT: %14 = load i64, ptr %13, align 8 +// CHECK-NEXT: %15 = icmp ne i64 8, %3 +// CHECK-NEXT: br i1 %15, label %_llgo_5, label %_llgo_6 // CHECK-EMPTY: // CHECK-NEXT: _llgo_5: ; preds = %_llgo_4 // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintString"(%"{{.*}}/runtime/internal/runtime.String" { ptr @0, i64 4 }) @@ -173,10 +169,10 @@ func (m *M[T]) check(align, offset1, offset2 uintptr) { // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 32) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintUint"(i64 %3) // CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.PrintByte"(i8 10) -// CHECK-NEXT: %18 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) -// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %18, align 8 -// CHECK-NEXT: %19 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %18, 1 -// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %19) +// CHECK-NEXT: %16 = call ptr @"{{.*}}/runtime/internal/runtime.AllocU"(i64 16) +// CHECK-NEXT: store %"{{.*}}/runtime/internal/runtime.String" { ptr @4, i64 21 }, ptr %16, align 8 +// CHECK-NEXT: %17 = insertvalue %"{{.*}}/runtime/internal/runtime.eface" { ptr @_llgo_string, ptr undef }, ptr %16, 1 +// CHECK-NEXT: call void @"{{.*}}/runtime/internal/runtime.Panic"(%"{{.*}}/runtime/internal/runtime.eface" %17) // CHECK-NEXT: unreachable // CHECK-EMPTY: // CHECK-NEXT: _llgo_6: ; preds = %_llgo_4 diff --git a/cl/_testrt/unreachable/in.go b/cl/_testrt/unreachable/in.go index b055dd9734..c6321d532d 100644 --- a/cl/_testrt/unreachable/in.go +++ b/cl/_testrt/unreachable/in.go @@ -8,12 +8,26 @@ import ( // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/unreachable.foo"(){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: unreachable +// CHECK-EMPTY: +// CHECK-NEXT: _llgo_1:{{.*}}; No predecessors! // CHECK-NEXT: ret void // CHECK-NEXT: } func foo() { c.Unreachable() } +// Keep a source Jump and merge Phi after the intrinsic. The unreachable +// lowering must move that tail to a dead physical continuation instead of +// either appending a second terminator or dropping the Phi predecessor. +func unreachableMerge(cond bool, value int) int { + result := value + if cond { + c.Unreachable() + result = value + 1 + } + return result +} + // CHECK-LABEL: define void @"{{.*}}/cl/_testrt/unreachable.main"(){{.*}} { // CHECK-NEXT: _llgo_0: // CHECK-NEXT: call void @"{{.*}}/cl/_testrt/unreachable.foo"() diff --git a/cl/blocks/block.go b/cl/blocks/block.go index 2aa4bea11b..372515df9b 100644 --- a/cl/blocks/block.go +++ b/cl/blocks/block.go @@ -22,8 +22,9 @@ import ( ) type Info struct { - Kind llssa.DoAction - Next int + Kind llssa.DoAction + Next int + InLoop bool } // ----------------------------------------------------------------------------- @@ -168,7 +169,11 @@ retry: ret := make([]Info, n) for i := 0; i < n; i++ { iblk := order[i] - ret[iblk] = Info{states[iblk].kind(), order[i+1]} + ret[iblk] = Info{ + Kind: states[iblk].kind(), + Next: order[i+1], + InLoop: states[iblk].inLoop, + } } return ret } diff --git a/cl/blocks/block_test.go b/cl/blocks/block_test.go index d0e5b72b23..4e4702b2f8 100644 --- a/cl/blocks/block_test.go +++ b/cl/blocks/block_test.go @@ -57,7 +57,7 @@ func TestFirstLoop(t *testing.T) { blk.Preds = []*ssa.BasicBlock{blk} blk.Succs = []*ssa.BasicBlock{blk} infos := Infos([]*ssa.BasicBlock{blk}) - if infos[0].Kind != llssa.DeferInLoop { + if infos[0].Kind != llssa.DeferInLoop || !infos[0].InLoop { t.Fatal("TestFirstLoop") } } diff --git a/cl/cgo_test.go b/cl/cgo_test.go index 8ba0838340..e5de5b9f91 100644 --- a/cl/cgo_test.go +++ b/cl/cgo_test.go @@ -27,6 +27,11 @@ func init() { } func buildGoSSAPkg(t *testing.T, src string) (*gossa.Package, *token.FileSet, []*ast.File) { + t.Helper() + return buildGoSSAPkgWithMode(t, src, gossa.SanityCheckFunctions|gossa.InstantiateGenerics) +} + +func buildGoSSAPkgWithMode(t *testing.T, src string, mode gossa.BuilderMode) (*gossa.Package, *token.FileSet, []*ast.File) { t.Helper() fset := token.NewFileSet() f, err := parser.ParseFile(fset, "foo.go", src, parser.ParseComments) @@ -36,7 +41,6 @@ func buildGoSSAPkg(t *testing.T, src string) (*gossa.Package, *token.FileSet, [] files := []*ast.File{f} pkg := types.NewPackage(f.Name.Name, f.Name.Name) imp := packages.NewImporter(fset) - mode := gossa.SanityCheckFunctions | gossa.InstantiateGenerics ssaPkg, _, err := ssautil.BuildPackage(&types.Config{Importer: imp}, fset, pkg, files, mode) if err != nil { t.Fatal(err) diff --git a/cl/compilation.go b/cl/compilation.go new file mode 100644 index 0000000000..6857b6fa73 --- /dev/null +++ b/cl/compilation.go @@ -0,0 +1,258 @@ +/* + * 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 ( + "encoding/hex" + "fmt" + "sync" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// CoroPlanObserver observes the immutable, compilation-scoped coroutine plan +// immediately before cl processes a package from source. It is report-only: +// installing an observer does not enable coroutine lowering or change LLVM IR. +// The observer is not called for a package whose compiled archive came from +// the build cache. Observers must treat both arguments as read-only. +type CoroPlanObserver func(pkg *ssa.Package, plan *coro.SSAPlan) + +// CoroFrameRetentionParkABIV2 is the sole stackless frame-retention identity. +// A generic llgo.coroPark state is frame-owned only when its exact prepare call +// has a frozen executor-safe, borrow-until-return callable contract. Event +// source symbols never participate in this compiler profile. +const CoroFrameRetentionParkABIV2 = coro.FrameRetentionParkABIV2 + +const ( + CoroProfileNone = coro.RuntimeProfileNone + CoroProfileStackless = coro.RuntimeProfileStackless +) + +func CoroNativeTargetCapabilities() coro.TargetCapabilities { + return coro.NewTargetCapabilities(true, true) +} + +// Compilation contains immutable inputs shared by every package compiled as +// part of one frontend compilation. Pass it by pointer and do not copy it after +// first use. A CoroPlan remains report-only unless CoroProfileStackless is +// selected. The prepared emission universe freezes every function that +// codegen may materialize, and any later out-of-universe lookup fails closed at +// its first symbol resolution. +type Compilation struct { + CoroPlan *coro.SSAPlan + CoroPlanObserver CoroPlanObserver + CoroProfile coro.RuntimeProfile + CoroTargetCapabilities coro.TargetCapabilities + // CoroPlanDigest and the ABI identities are populated by the build driver + // after whole-program analysis and participate in every package archive + // fingerprint. They are required before an active compilation may register + // a cache hit. + CoroPlanDigest string + CoroLoweringFacts coro.LoweringFacts + CoroLoweringFactsDigest string + CoroABI string + SchedulerABI string + PanicABI string + FuncRepABI string + // CoroFrameRetentionABI selects one compiler/runtime-owned contract under + // which x/tools Heap Allocs may be re-proved as current LLVM coroutine-frame + // storage. The zero value preserves the ordinary managed-allocation rule. + // Unknown identities and identities without runnable PhysicalABIV1 lowering + // fail before LLVM code generation. + CoroFrameRetentionABI string + + // EmissionUniverse is the immutable, compilation-scoped set of exact SSA + // functions that cl may resolve while emitting this compilation. Active + // coroutine entry resolution requires the universe to have been prepared + // before any package enters LLVM codegen. + EmissionUniverse *EmissionUniverse + + coroPreflight sync.Once + coroPreflightErr error + coroFactsValidation sync.Once + coroFactsValidationErr error + coroClosedInterfacePlain *coroClosedInterfacePlainPlan + coroManagedInterface *coroManagedInterfaceDispatchPlan +} + +// CoroProfileActive reports whether this compilation owns the complete +// stackless architecture. +func (c *Compilation) CoroProfileActive() bool { + return c != nil && c.CoroProfile.Active() +} + +func (c *Compilation) CoroEntryResolutionActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroPhysicalABIActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroChildAwaitActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroPlainDispatchActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroClosedStaticSpawnActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroProgramBootstrapActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroChannelActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroExplicitStatusActive() bool { + return c.CoroProfileActive() +} + +func (c *Compilation) CoroWorkerActive() bool { + return c != nil && c.CoroProfile.Active() && c.CoroTargetCapabilities.Worker() +} + +func (c *Compilation) validateCoroProfile() error { + if c == nil { + return nil + } + if !c.CoroProfile.Valid() { + return fmt.Errorf("unknown coroutine runtime profile %d", c.CoroProfile) + } + if !c.CoroTargetCapabilities.Valid() { + return fmt.Errorf("invalid coroutine target capability set %d", c.CoroTargetCapabilities) + } + if !c.CoroProfile.Active() && c.CoroTargetCapabilities != 0 { + return fmt.Errorf("coroutine target capabilities require the stackless runtime profile") + } + return nil +} + +func (c *Compilation) validateCoroCacheIdentity() error { + if c == nil { + return fmt.Errorf("coroutine cache registration requires a compilation") + } + decoded, err := hex.DecodeString(c.CoroPlanDigest) + if err != nil || len(decoded) != 32 || hex.EncodeToString(decoded) != c.CoroPlanDigest { + return fmt.Errorf("coroutine cache registration requires a canonical SHA-256 CoroPlanDigest") + } + if err := c.validateCoroLoweringFactsIdentity(); err != nil { + return err + } + return c.validateCoroABIIdentity(true) +} + +func (c *Compilation) validateCoroLoweringFactsIdentity() error { + if c == nil { + return fmt.Errorf("coroutine lowering-facts validation requires a compilation") + } + c.coroFactsValidation.Do(func() { + if c.CoroLoweringFacts.Schema != coro.LoweringFactsSchema { + c.coroFactsValidationErr = fmt.Errorf("coroutine cache registration lowering-facts schema %q, want %q", c.CoroLoweringFacts.Schema, coro.LoweringFactsSchema) + return + } + decoded, err := hex.DecodeString(c.CoroLoweringFactsDigest) + if err != nil || len(decoded) != 32 || hex.EncodeToString(decoded) != c.CoroLoweringFactsDigest { + c.coroFactsValidationErr = fmt.Errorf("coroutine cache registration requires a canonical SHA-256 lowering-facts digest") + return + } + digest, err := c.CoroLoweringFacts.Digest() + if err != nil { + c.coroFactsValidationErr = fmt.Errorf("coroutine cache registration validates lowering facts: %w", err) + return + } + if digest != c.CoroLoweringFactsDigest { + c.coroFactsValidationErr = fmt.Errorf("coroutine cache registration lowering-facts digest mismatch: have %q, want %q", digest, c.CoroLoweringFactsDigest) + } + }) + return c.coroFactsValidationErr +} + +func (c *Compilation) validateCoroABIIdentity(required bool) error { + if c == nil { + return fmt.Errorf("coroutine ABI validation requires a compilation") + } + if err := c.validateCoroProfile(); err != nil { + return err + } + if !c.CoroProfile.Active() { + if !required && c.CoroABI == "" && c.SchedulerABI == "" && c.PanicABI == "" && c.FuncRepABI == "" && c.CoroFrameRetentionABI == "" { + return nil + } + return fmt.Errorf("coroutine ABI identity requires the stackless runtime profile") + } + return c.validateStacklessCoroABIIdentity(required) +} + +func (c *Compilation) validateStacklessCoroABIIdentity(required bool) error { + if err := c.validateCoroProfile(); err != nil { + return err + } + wantScheduler := coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + if c.CoroWorkerActive() { + wantScheduler = coro.SchedulerProgramBootstrapChannelWorkerClosedStaticSpawnABIV0 + } + switch c.CoroFrameRetentionABI { + case "", CoroFrameRetentionParkABIV2: + default: + return fmt.Errorf("unknown coroutine frame-retention ABI %q", c.CoroFrameRetentionABI) + } + checks := []struct { + name string + got string + want string + }{ + {"coroutine", c.CoroABI, coro.PhysicalABIV1}, + {"scheduler", c.SchedulerABI, wantScheduler}, + {"panic", c.PanicABI, coro.PanicExplicitStatusABIV0}, + {"function representation", c.FuncRepABI, coro.FuncRepABIV1}, + } + if !required { + populated := false + for _, check := range checks { + populated = populated || check.got != "" + } + if !populated { + return nil + } + } + for _, check := range checks { + if check.got != check.want { + return fmt.Errorf("coroutine compilation %s ABI %q does not match %q", check.name, check.got, check.want) + } + } + return nil +} + +// PackageOptions contains inputs that vary for each package invocation. +type PackageOptions struct { + Compilation *Compilation + + // CacheHit means cl is rebuilding frontend registrations and link-time + // metadata for an already-compiled archive. The transient module is discarded + // by the build driver. Report-only observers are skipped; active coroutine + // entry resolution accepts the cache hit only after the driver has matched the + // archive's canonical plan digest and ABI identity, and keeps that plan + // installed so symbol and physical-ABI metadata match a source compilation. + CacheHit bool +} diff --git a/cl/compilation_test.go b/cl/compilation_test.go new file mode 100644 index 0000000000..8d3a59d9bf --- /dev/null +++ b/cl/compilation_test.go @@ -0,0 +1,324 @@ +//go:build !llgo +// +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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + "golang.org/x/tools/go/ssa" +) + +func TestCompilationCoroPlanObservationAndCacheRegistration(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, ` +package foo + +func F() int { return 42 } +`) + plan := new(coro.SSAPlan) + observerCalls := 0 + compilation := &Compilation{ + CoroPlan: plan, + CoroPlanObserver: func(pkg *ssa.Package, got *coro.SSAPlan) { + observerCalls++ + if pkg != ssaPkg { + t.Errorf("observer package = %p, want %p", pkg, ssaPkg) + } + if got != plan { + t.Errorf("observer plan = %p, want %p", got, plan) + } + }, + } + + compile := func(cacheHit bool) string { + t.Helper() + prog := newLLSSAProg(t) + defer prog.Dispose() + pkg, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: compilation, + CacheHit: cacheHit, + }) + if err != nil { + t.Fatalf("NewPackageExWithEmbedOptions(cache hit %v): %v", cacheHit, err) + } + return pkg.String() + } + + sourceIR := compile(false) + if observerCalls != 1 { + t.Fatalf("source observer calls = %d, want 1", observerCalls) + } + cachedIR := compile(true) + if observerCalls != 1 { + t.Fatalf("cache registration observer calls = %d, want unchanged 1", observerCalls) + } + if cachedIR != sourceIR { + t.Fatal("cache registration option changed frontend LLVM IR") + } +} + +func TestCompilationCoroABIIdentityValidation(t *testing.T) { + current := func() *Compilation { + return &Compilation{ + CoroProfile: CoroProfileStackless, + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, + CoroFrameRetentionABI: CoroFrameRetentionParkABIV2, + } + } + if err := current().validateCoroABIIdentity(false); err != nil { + t.Fatalf("current stackless ABI identity: %v", err) + } + if err := (&Compilation{CoroProfile: CoroProfileStackless}).validateCoroABIIdentity(false); err != nil { + t.Fatalf("omitted source ABI identity should use current defaults: %v", err) + } + + for _, test := range []struct { + name string + edit func(*Compilation) + want string + }{ + {name: "physical", edit: func(c *Compilation) { c.CoroABI = "invalid" }, want: "coroutine ABI"}, + {name: "scheduler", edit: func(c *Compilation) { c.SchedulerABI = "invalid" }, want: "scheduler ABI"}, + {name: "panic", edit: func(c *Compilation) { c.PanicABI = "invalid" }, want: "panic ABI"}, + {name: "function representation", edit: func(c *Compilation) { c.FuncRepABI = "invalid" }, want: "function representation ABI"}, + } { + t.Run(test.name, func(t *testing.T) { + compilation := current() + test.edit(compilation) + if err := compilation.validateCoroABIIdentity(false); err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("ABI mismatch error = %v, want substring %q", err, test.want) + } + }) + } + + inactive := current() + inactive.CoroProfile = CoroProfileNone + if err := inactive.validateCoroABIIdentity(false); err == nil || !strings.Contains(err.Error(), "requires the stackless runtime profile") { + t.Fatalf("inactive ABI identity error = %v", err) + } + if err := (&Compilation{}).validateCoroABIIdentity(false); err != nil { + t.Fatalf("inactive empty identity: %v", err) + } + + for _, retention := range []string{"", CoroFrameRetentionParkABIV2} { + compilation := current() + compilation.CoroFrameRetentionABI = retention + if err := compilation.validateCoroABIIdentity(false); err != nil { + t.Fatalf("frame-retention identity %q: %v", retention, err) + } + } + unknownRetention := current() + unknownRetention.CoroFrameRetentionABI = "invalid" + if err := unknownRetention.validateCoroABIIdentity(false); err == nil || !strings.Contains(err.Error(), "unknown coroutine frame-retention ABI") { + t.Fatalf("unknown frame-retention error = %v", err) + } + + worker := current() + worker.CoroTargetCapabilities = CoroNativeTargetCapabilities() + worker.SchedulerABI = coro.SchedulerProgramBootstrapChannelWorkerClosedStaticSpawnABIV0 + if err := worker.validateCoroABIIdentity(false); err != nil { + t.Fatalf("native worker ABI identity: %v", err) + } + worker.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + if err := worker.validateCoroABIIdentity(false); err == nil || !strings.Contains(err.Error(), "scheduler ABI") { + t.Fatalf("native worker scheduler mismatch = %v", err) + } + + if err := (&Compilation{CoroProfile: CoroProfileStackless}).validateCoroABIIdentity(true); err == nil || !strings.Contains(err.Error(), "coroutine ABI") { + t.Fatalf("missing cache ABI identity error = %v", err) + } + if err := current().preflightCoroPlan(); err == nil || !strings.Contains(err.Error(), "requires a compilation CoroPlan") { + t.Fatalf("active source preflight error = %v", err) + } +} + +func TestCoroEntryResolutionCacheRegistrationWithDigest(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, ` +package foo + +func F() int { return 42 } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: ssaPkg.Func("F"), Demand: coro.SyncDemand}, + }, coro.SSAConfig{EmissionUniverse: ssaUniverse, FunctionIDs: functionIDs}) + if err != nil { + t.Fatal(err) + } + observerCalls := 0 + compilation := &Compilation{ + CoroPlan: plan, + CoroPlanObserver: func(*ssa.Package, *coro.SSAPlan) { observerCalls++ }, + + CoroPlanDigest: strings.Repeat("0", 64), + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + } + installCoroLoweringFactsForTest(t, compilation) + mismatchedFacts := &Compilation{ + CoroPlanDigest: compilation.CoroPlanDigest, + CoroLoweringFacts: compilation.CoroLoweringFacts, + CoroLoweringFactsDigest: strings.Repeat("f", 64), + } + if err := mismatchedFacts.validateCoroCacheIdentity(); err == nil || !strings.Contains(err.Error(), "lowering-facts digest mismatch") { + t.Fatalf("mismatched lowering-facts cache identity error = %v", err) + } + pkg, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: compilation, + CacheHit: true, + }) + if err != nil { + t.Fatal(err) + } + if pkg == nil { + t.Fatal("cache registration returned a nil package") + } + if observerCalls != 0 { + t.Fatalf("cache registration observer calls = %d, want 0", observerCalls) + } +} + +func installCoroLoweringFactsForTest(t *testing.T, compilation *Compilation) { + t.Helper() + if compilation == nil || compilation.CoroPlan == nil || compilation.EmissionUniverse == nil { + t.Fatal("test lowering facts require a complete compilation plan and emission universe") + } + report, err := compilation.EmissionUniverse.BuildCoroLoweringFactsReport(compilation.CoroPlan) + if err != nil { + t.Fatalf("build test lowering facts: %v", err) + } + compilation.CoroLoweringFacts = report.Facts + compilation.CoroLoweringFactsDigest = report.Digest +} + +func TestCoroEntryResolutionPlainPrimaryPreservesIR(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, ` +package foo + +func F(value int) int { return value + 1 } +`) + compile := func(active bool) string { + t.Helper() + prog := newLLSSAProg(t) + defer prog.Dispose() + var compilation *Compilation + if active { + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: ssaPkg.Func("F"), Demand: coro.SyncDemand}, + {Function: ssaPkg.Func("init"), Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + }) + if err != nil { + t.Fatal(err) + } + compilation = &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + } + } + pkg, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: compilation, + }) + if err != nil { + t.Fatal(err) + } + return pkg.String() + } + + baseline := compile(false) + resolved := compile(true) + if resolved != baseline { + t.Fatal("plain-primary entry resolution changed emitted LLVM IR") + } +} + +func TestReportOnlyCoroPlanDoesNotSelectSafeArrayEmission(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, ` +package foo + +var values = [...]int{1, 2, 3, 4} +func Sum() int { + total := 0 + for index := range values { total += values[index] } + return total +} +`) + compile := func(reportOnly bool) string { + t.Helper() + prog := newLLSSAProg(t) + defer prog.Dispose() + var compilation *Compilation + if reportOnly { + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + // An intentionally empty report-only plan must not participate in + // physical lowering. In particular, recomputing a safe site outside + // this plan cannot trigger the active-plan consistency assertion. + compilation = &Compilation{CoroPlan: new(coro.SSAPlan), EmissionUniverse: universe} + } + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + return pkg.String() + } + + baseline := compile(false) + reportOnly := compile(true) + if reportOnly != baseline { + t.Fatal("report-only CoroPlan changed fixed-array LLVM emission") + } +} diff --git a/cl/compile.go b/cl/compile.go index 6cb94df500..cc7e5624ee 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -32,6 +32,7 @@ import ( "github.com/goplus/llgo/cl/blocks" "github.com/goplus/llgo/cl/ssawrap" + "github.com/goplus/llgo/internal/coro" "github.com/goplus/llgo/internal/goembed" "github.com/goplus/llgo/internal/typepatch" "golang.org/x/tools/go/ssa" @@ -171,15 +172,26 @@ type context struct { loaded map[*types.Package]*pkgInfo // loaded packages bvals map[ssa.Value]llssa.Expr // block values methodNilDerefChecks map[*ssa.UnOp]none + patchOriginalInitIf *ssa.If // exact synthetic guard whose successors are logically inverted + unevaluatedSSA map[ssa.Instruction]none // values used only by unsafe.Sizeof/Alignof vargs map[*ssa.Alloc][]llssa.Expr // varargs funcs map[*ssa.Function]llssa.Function + rawPlainFuncs map[*ssa.Function]llssa.Function linkOnceFns map[*ssa.Function]none stackDefers map[*ssa.Function]bool anonDefers map[*ssa.Function]bool debugDIVars map[*types.Var]llssa.DIVar debugAllocVars map[*ssa.Alloc]*types.Var runtimeCallerFuncs map[*ssa.Function]bool + compilation *Compilation + emissionUniverse *EmissionUniverse + emissionOwner *preparedEmissionPackage + cacheRegistration bool // cached archive: skip observers; emitted IR is transient pcLineSeq uint64 + coroEmission *coroPhysicalEmissionSession + rawPlainBody bool // compiling the legacy ABI variant of a managed function + coroRootFactories []coroRootFactoryRegistration + coroPlainDescriptors map[string]llssa.Expr patches Patches blkInfos []blocks.Info @@ -378,6 +390,9 @@ func (p *context) compileMethodsIf(pkg llssa.Package, typ types.Type, keep func( if keep != nil && !keep(ssaMthd) { continue } + if p.omitUnemittedFunction(ssaMthd) { + continue + } p.compileFuncDecl(pkg, ssaMthd) } } @@ -392,6 +407,15 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { } dbgInstrln("==> NewVar", name, typ) g := pkg.NewVar(name, typ, llssa.Background(vtype)) + if p.emissionUniverse != nil { + identity, certified, err := p.emissionUniverse.CoroGlobalPhysicalIdentity(gbl) + if err != nil { + panic(err) + } + if certified && identity.InternalLinkage { + g.SetInternalLinkage() + } + } if p.tryEmbedGlobalInit(pkg, gbl, g, name) { return } @@ -476,6 +500,9 @@ func (p *context) needsLinkOnce(f *ssa.Function) bool { if _, ok := p.linkOnceFns[f]; ok { return true } + if p.emissionUniverse != nil && p.emissionUniverse.generatedWrapperDefinitionNeedsLinkOnce(f) { + return true + } if hasGenericInstantiation(f) { return true } @@ -519,7 +546,59 @@ func hasInstantiatedRecv(recv *types.Var) bool { } func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Function, llssa.PyObjRef, int) { - pkgTypes, name, ftype := p.funcName(f) + entry := p.mustFunctionSymbol(f) + if entry.planned && entry.plan.Emission == coro.EmitRawPlain { + // Eager package enumeration still materializes raw-only functions, but + // their first and only body must use legacy-stack lowering. Starting in + // managed mode here would manufacture the dead twin this plan excludes. + return p.compileFuncDeclVariant(pkg, entry.function, true) + } + fn, py, kind := p.compileFuncDeclVariant(pkg, f, false) + if entry.planned && entry.plan.Emission == coro.EmitCoroutine && + p.compilation != nil && p.compilation.CoroPlan != nil && + p.compilation.CoroPlan.HasRawPlainVariant(entry.function) { + // RawPlainEntry is only the public address/ABI capability. A raw closure + // helper may need a private legacy-stack twin without being an entry. + // Eagerly materialize every planned twin in its defining package so a + // raw caller compiled in another package never leaves an unresolved + // declaration behind. + p.compileFuncDeclVariant(pkg, entry.function, true) + } + return fn, py, kind +} + +// compileFuncDeclVariant materializes either the managed primary or the exact +// legacy Go-ABI body requested by RawPlainEntry. The SSA CFG is shared, but the +// latter deliberately runs through ordinary native-stack lowering: no +// coroutine frame, explicit-status outcome, await, or preemption poll is +// emitted. Calls made while compiling that body are redirected by +// compileFunction to the corresponding raw/plain target entry. +func (p *context) compileFuncDeclVariant(pkg llssa.Package, f *ssa.Function, rawPlain bool) (llssa.Function, llssa.PyObjRef, int) { + var entry plannedFunctionSymbol + patchOriginal := f != nil && f.Name() == "init" && f.Signature != nil && f.Signature.Recv() == nil && + p.state == pkgHasPatch && p.compilation != nil && p.compilation.CoroEntryResolutionActive() + if patchOriginal { + entry = p.mustPatchOriginalInitFunctionSymbol(f) + } else { + entry = p.mustFunctionSymbol(f) + } + if rawPlain { + if patchOriginal { + entry = p.mustRawPlainFunctionSymbolFromEntry(entry, nil) + } else { + entry = p.mustRawPlainFunctionSymbol(f) + } + } + return p.compileFuncDeclVariantEntry(pkg, entry, rawPlain) +} + +// compileFuncDeclVariantEntry materializes an already-resolved physical symbol +// role. Ordinary definitions enter through compileFuncDeclVariant; the one +// compiler-owned patch-original await passes its private role directly so a +// second generic lookup cannot collapse it back to the public init symbol. +func (p *context) compileFuncDeclVariantEntry(pkg llssa.Package, entry plannedFunctionSymbol, rawPlain bool) (llssa.Function, llssa.PyObjRef, int) { + f := entry.function + pkgTypes, name, ftype := entry.pkgTypes, entry.name, entry.ftype if ftype != goFunc { return nil, nil, ignoredFunc } @@ -531,13 +610,27 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun }() return p.patchType(f.Signature).(*types.Signature) }() + sourceSig := sig state := p.state + if entry.patchOriginalInit { + state = pkgHasPatch + } isInit := (f.Name() == "init" && sig.Recv() == nil) - if isInit && state == pkgHasPatch { - name = initFnNameOfHasPatch(name) - // TODO(xsw): pkg.init$guard has been set, change ssa.If to ssa.Jump - block := f.Blocks[0].Instrs[1].(*ssa.If).Block() - block.Succs[0], block.Succs[1] = block.Succs[1], block.Succs[0] + var patchOriginalInitIf *ssa.If + if isInit && (entry.patchOriginalInit || state == pkgHasPatch) { + // The explicit coroutine role already owns init$hasPatch. Legacy and + // report-only compilation retain the historical state-derived spelling. + if !entry.patchOriginalInit { + name = initFnNameOfHasPatch(name) + } + if len(f.Blocks) == 0 || len(f.Blocks[0].Instrs) < 2 { + panic("patch original initializer has no synthetic guard") + } + var ok bool + patchOriginalInitIf, ok = f.Blocks[0].Instrs[1].(*ssa.If) + if !ok || patchOriginalInitIf.Block() != f.Blocks[0] || len(f.Blocks[0].Succs) != 2 { + panic("patch original initializer has an invalid synthetic guard") + } } fn := pkg.FuncOf(name) @@ -553,9 +646,23 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun } else { dbgInstrln("==> NewFunc", name, "type:", sig.Recv(), sig, "ftype:", ftype) } - if fn == nil { - fn = pkg.NewFuncEx(name, sig, llssa.Background(ftype), hasCtx, p.needsLinkOnce(f)) - } + var physicalABI *coroPhysicalABI + if entry.physical && entry.plan.Emission == coro.EmitCoroutine { + // x/tools exposes a declared method receiver as fn.Params[0]. Normalize + // the callable source ABI before adding the two coroutine-owned hidden + // parameters so compileValue's sourceParamBase maps every SSA parameter + // to the same physical position. + sourceSig = coroPhysicalNormalizeSourceSignature(sig) + abi := newCoroPhysicalABI(p, entry, sourceSig) + physicalABI = &abi + sig = abi.physicalSig + hasCtx = false + } + // Always revisit an existing declaration when materializing its body. + // NewFuncEx promotes that declaration to linkonce when required; declarations + // themselves must retain external linkage because LLVM rejects a bodyless + // linkonce global. + fn = pkg.NewFuncEx(name, sig, llssa.Background(ftype), hasCtx, p.needsLinkOnce(f)) noInlineDirective := hasNoInlineDirective(f) runtimeStackNoInline := needsRuntimeStackNoInline(pkgTypes, f) pcLineNoInline := p.needsPCLineNoInline(f) @@ -565,7 +672,14 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun if noInlineDirective || runtimeStackNoInline || pcLineNoInline { fn.DisableTailCalls() } - p.funcs[f] = fn + if rawPlain { + p.rawPlainFuncs[f] = fn + } else { + p.funcs[f] = fn + } + if physicalABI != nil && entry.childAwait { + p.emitCoroRootFactory(pkg, entry, *physicalABI, sourceSig, fn) + } isCgo := isCgoExternSymbol(f) if nblk := len(f.Blocks); nblk > 0 { if p.prog.FuncInfoMetadataEnabled() { @@ -577,10 +691,13 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun pkg.EmitFuncInfo(fn.Name(), funcInfoDisplayName(pkgTypes, goName), pos.Filename, pos.Line, pos.Column) } var childInits []func() - if len(f.AnonFuncs) > 0 { + if !rawPlain && len(f.AnonFuncs) > 0 { parentInits := p.inits p.inits = nil for _, af := range f.AnonFuncs { + if p.omitUnemittedFunction(af) { + continue + } p.compileFuncDecl(pkg, af) } childInits = append(childInits, p.inits...) @@ -589,24 +706,28 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun p.cgoCalled = false p.cgoArgs = nil p.cgoErrno = llssa.Nil - if isCgo { + if physicalABI != nil { + fn.MakeBlocks(1) // dedicated coroutine ramp entry + } else if isCgo { fn.MakeBlocks(1) } else { fn.MakeBlocks(nblk) // to set fn.HasBody() = true } - if f.Recover != nil { // set recover block + if f.Recover != nil && physicalABI == nil { // set recover block fn.SetRecover(fn.Block(f.Recover.Index)) } dbgEnabled := enableDbg dbgSymsEnabled := enableDbgSyms && (f == nil || f.Origin() == nil) p.inits = append(p.inits, func() { - oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark := p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark + oldFn, oldGoFn, oldMethodNilDerefChecks, oldPatchOriginalInitIf, oldUnevaluatedSSA, oldCallerFrameMark, oldRawPlainBody := p.fn, p.goFn, p.methodNilDerefChecks, p.patchOriginalInitIf, p.unevaluatedSSA, p.callerFrameMark, p.rawPlainBody p.fn = fn p.goFn = f + p.patchOriginalInitIf = patchOriginalInitIf + p.rawPlainBody = rawPlain p.callerFrameMark = llssa.Nil p.state = state // restore pkgState when compiling funcBody defer func() { - p.fn, p.goFn, p.methodNilDerefChecks, p.callerFrameMark = oldFn, oldGoFn, oldMethodNilDerefChecks, oldCallerFrameMark + p.fn, p.goFn, p.methodNilDerefChecks, p.patchOriginalInitIf, p.unevaluatedSSA, p.callerFrameMark, p.rawPlainBody = oldFn, oldGoFn, oldMethodNilDerefChecks, oldPatchOriginalInitIf, oldUnevaluatedSSA, oldCallerFrameMark, oldRawPlainBody }() p.phis = nil if dbgSymsEnabled { @@ -626,6 +747,28 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun } p.bvals = make(map[ssa.Value]llssa.Expr) p.methodNilDerefChecks = collectMethodNilDerefChecks(f) + if p.emissionUniverse != nil { + var frozen bool + p.unevaluatedSSA, frozen = p.emissionUniverse.frozenUnsafeSizeAlignUnevaluatedSSA(f) + if !frozen { + panic(fmt.Sprintf("function %q has no frozen unsafe.Sizeof/Alignof lowering facts", f.String())) + } + } else { + // Legacy one-package compilation has no whole-program inventory. + p.unevaluatedSSA = collectUnsafeSizeAlignUnevaluatedSSA(f) + } + if physicalABI != nil { + p.compileCoroPhysicalBody(b, f, *physicalABI, isInit) + // Anonymous bodies are collected while the physical owner is + // declared, but their deferred initializers still have to run after + // the owner's symbols and frame recipe exist. Returning here without + // them leaves captured coroutine targets as empty LLVM declarations. + for _, childInit := range childInits { + childInit() + } + b.EndBuild() + return + } off := make([]int, len(f.Blocks)) if isCgo { p.cgoArgs = make([]llssa.Expr, len(f.Params)) @@ -807,9 +950,10 @@ func (p *context) debugRef(b llssa.Builder, v *ssa.DebugRef) { scope := variable.Parent() diScope := b.DIScope(p.fn, scope) if v.IsAddr { - b.DIDeclare(variable, value, dbgVar, diScope, pos, b.Func.Block(v.Block().Index)) + // *ssa.Alloc + b.DIDeclare(variable, value, dbgVar, diScope, pos, p.sourceBlock(v.Block().Index)) } else { - b.DIValue(variable, value, dbgVar, diScope, pos, b.Func.Block(v.Block().Index)) + b.DIValue(variable, value, dbgVar, diScope, pos, p.sourceBlock(v.Block().Index)) } } @@ -827,10 +971,21 @@ func (p *context) debugParams(b llssa.Builder, f *ssa.Function) { if p.debugDIVars != nil { p.debugDIVars[variable] = div } - b.DIParam(variable, v, div, p.fn, pos, p.fn.Block(0)) + b.DIParam(variable, v, div, p.fn, pos, p.sourceBlock(0)) } } +// sourceBlock maps a Go SSA basic-block index to the logical LLVM block used +// by the current lowering. Plain functions retain the historical one-to-one +// Function.Block mapping. A physical coroutine has a dedicated ramp and +// internal suspend blocks, so its source CFG uses an explicit stable map. +func (p *context) sourceBlock(index int) llssa.BasicBlock { + if block, ok := p.coroEmissionSourceBlock(index); ok { + return block + } + return p.fn.Block(index) +} + func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, doModInit bool) llssa.BasicBlock { var last int var pyModInit bool @@ -838,7 +993,7 @@ func (p *context) compileBlock(b llssa.Builder, block *ssa.BasicBlock, n int, do var pkg = p.pkg var fn = p.fn var instrs = block.Instrs[n:] - var ret = fn.Block(block.Index) + var ret = p.sourceBlock(block.Index) b.SetBlock(ret) if block.Index == 0 && p.shouldTrackCallerFrames() { p.pushCallerLocationFrame(b, block.Parent()) @@ -872,10 +1027,18 @@ 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 _, skip := p.unevaluatedSSA[instr]; skip { + continue + } + if p.compileCoroInstructionPrologue(b, instr) { + continue + } 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) - b.Call(fnOld.Expr) + if !p.compileCoroPatchInitAtBlock(b) { + fnOld := pkg.NewFunc(initFnNameOld, llssa.NoArgsNoRet, llssa.InC) + b.Call(fnOld.Expr) + } } if isCgoCfunc || isCgoC2 || isCgoCmacro { switch instr := instr.(type) { @@ -1153,8 +1316,7 @@ func isPhi(i ssa.Instruction) bool { } func (p *context) compilePhis(b llssa.Builder, block *ssa.BasicBlock) int { - fn := p.fn - ret := fn.Block(block.Index) + ret := p.sourceBlock(block.Index) b.SetBlockEx(ret, llssa.AtEnd, false) if ninstr := len(block.Instrs); ninstr > 0 { if isPhi(block.Instrs[0]) { @@ -1165,10 +1327,16 @@ func (p *context) compilePhis(b llssa.Builder, block *ssa.BasicBlock) int { rets := make([]llssa.Expr, n) // TODO(xsw): check to remove this for i := 0; i < n; i++ { iv := block.Instrs[i].(*ssa.Phi) + if _, skip := p.unevaluatedSSA[iv]; skip { + continue + } rets[i] = p.compilePhi(b, iv) } for i := 0; i < n; i++ { iv := block.Instrs[i].(*ssa.Phi) + if _, skip := p.unevaluatedSSA[iv]; skip { + continue + } p.bvals[iv] = rets[i] } return n @@ -1184,7 +1352,7 @@ func (p *context) compilePhi(b llssa.Builder, v *ssa.Phi) (ret llssa.Expr) { preds := v.Block().Preds bblks := make([]llssa.BasicBlock, len(preds)) for i, pred := range preds { - bblks[i] = p.fn.Block(pred.Index) + bblks[i] = p.sourceBlock(pred.Index) } edges := v.Edges phi.AddIncoming(b, bblks, func(i int, blk llssa.BasicBlock) llssa.Expr { @@ -1204,11 +1372,80 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } switch v := iv.(type) { case *ssa.Call: - ret = p.call(b, llssa.Call, &v.Call) + if value, handled := p.tryCompileCoroPatchInitRedirect(b, v); handled { + ret = value + } else if p.rawPlainBody { + // A compiler-frozen closed SyncDispatch (currently the TLS destructor + // callback) has a complete singleton target and plain descriptor ABI. + // Preserve that exact path before the general raw-body dynamic-call + // rejection; open/invoke/method dispatch remains fail-closed. + callPlan, planned := p.compilation.CoroPlan.CallPlan(v) + handled := false + if planned { + switch { + case callPlan.Transport == coro.RawCCodePointer: + common := v.Common() + if common == nil || common.StaticCallee() != nil || common.IsInvoke() || common.Method != nil || + callPlan.Kind != coro.CallForeign || callPlan.Rep != coro.DirectPlain || !callPlan.Open || + callPlan.Unresolved != coro.UnknownForeign || callPlan.SyncDispatch { + panic(fmt.Errorf("raw plain body %q has malformed raw C code-pointer call %q", p.goFn.Name(), v.String())) + } + ret = p.call(b, llssa.Call, &v.Call) + handled = true + case callPlan.Rep == coro.Dispatch && !callPlan.SyncDispatch: + panic(fmt.Errorf("raw plain body %q contains non-synchronous descriptor call %q", p.goFn.Name(), v.String())) + case callPlan.SyncDispatch: + value, dispatched := p.tryCompileCoroPlainDispatchCall(b, v) + if !dispatched { + panic(fmt.Errorf("raw plain body %q lost its planned synchronous descriptor call %q", p.goFn.Name(), v.String())) + } + ret = value + handled = true + } + } + if !handled { + common := v.Common() + if common == nil { + panic("raw plain body contains a call without CallCommon") + } + if _, builtin := common.Value.(*ssa.Builtin); !builtin && + (common.StaticCallee() == nil || common.IsInvoke() || common.Method != nil) { + panic(fmt.Errorf("raw plain body %q contains an unplanned dynamic call %q", p.goFn.Name(), v.String())) + } + ret = p.call(b, llssa.Call, &v.Call) + } + } else if p.hasCoroPhysicalBody() { + if value, handled := p.tryCompileCoroPhysicalCall(b, v); handled { + ret = value + } else { + ret = p.call(b, llssa.Call, &v.Call) + } + } else if value, handled := p.tryCompileCoroManagedInterfaceDispatch(b, v); handled { + ret = value + } else if value, handled := p.tryCompileCoroPlainDispatchCall(b, v); handled { + ret = value + } else { + ret = p.call(b, llssa.Call, &v.Call) + } if p.rangeFuncCallNeedsDeferDrain(&v.Call) { b.DeferStackDrain() } case *ssa.BinOp: + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionInterfaceNilCompare { + if physicalInstruction.valueOperand == nil { + panic("interface nil comparison lost its frozen value operand") + } + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionInterfaceNilCompare) + physical := p.compileValue(b, physicalInstruction.valueOperand) + typeWord := b.InterfaceTypeWord(physical) + nilType := p.prog.Nil(p.prog.VoidPtr()) + ret = b.BinOp(v.Op, typeWord, nilType) + break + } + if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("BinOp selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } if isUntypedNilConst(v.X) && isUntypedNilConst(v.Y) { switch v.Op { case token.EQL: @@ -1224,11 +1461,32 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } x := p.compileValueAs(b, v.X, v.Y.Type()) y := p.compileValueAs(b, v.Y, v.X.Type()) - ret = b.BinOp(v.Op, x, y) + if (v.Op == token.QUO || v.Op == token.REM) && ssaIntegerValueProvenNonZeroAt(v.Y, v) { + ret = b.BinOpWithNonZeroDivisor(v.Op, x, y) + } else { + ret = b.BinOp(v.Op, x, y) + } case *ssa.UnOp: + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) if v.Op == token.MUL { - if _, ok := p.methodNilDerefChecks[v]; ok { - return p.compileCheckedDeref(b, v) + if _, ok := p.methodNilDerefChecks[v]; ok && !ssaValueProvenNonNilAt(v.X, v) { + if physicalPlanned && p.coroUsesExplicitStatusFaults() { + switch physicalInstruction.recipe { + case coroPhysicalInstructionDeref: + // The physical dereference recipe below preserves the same base + // pointer with an explicit-status guard. AssertNilDerefPtr is the + // native-stack spelling of that operation and must not escape into + // this stackless coroutine body. + case coroPhysicalInstructionOrdinary: + // A non-elided SitePlan retains the managed checked-pointer helper; + // its frozen lowered-call fact is observed by compileCheckedDeref. + return p.compileCheckedDeref(b, v) + default: + panic(fmt.Sprintf("value-receiver dereference selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } + } else { + return p.compileCheckedDeref(b, v) + } } if isEffectfulArrayPointerDeref(v) { x := p.compileValue(b, v.X) @@ -1287,14 +1545,37 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if v.Op != token.ARROW { p.recordPanicLocation(b, v.Pos()) } - if shouldAssertDirectNilDeref(v) { + guardedDeref := physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionDeref + if guardedDeref { + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionDeref) + p.observeCoroPhysicalNilGuard(v) + x = p.compileCoroImplicitNilDerefGuard(b, v, x) + } else if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("typed load selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } else if (!physicalPlanned || !p.coroUsesExplicitStatusFaults()) && shouldAssertDirectNilDeref(v) && !ssaValueProvenNonNilAt(v.X, v) { b.AssertNilDeref(x) } if v.Op == token.ARROW { - ret = b.Recv(x, v.CommaOk) + operation, operationPlanned := p.plannedCoroPhysicalOperation(v) + if operationPlanned && operation.operation == coroPhysicalOperationChannelReceive { + p.observeCoroPhysicalOperation(v, coroPhysicalOperationChannelReceive) + ret = p.compileCoroChanRecv(b, v, x) + } else if !operationPlanned || operation.operation == coroPhysicalOperationNone { + ret = b.Recv(x, v.CommaOk) + } else { + panic(fmt.Sprintf("channel receive selected incompatible frozen physical operation recipe %s", operation.operation)) + } } else { if v.Op == token.MUL { if t := p.type_(v.Type(), llssa.InGo); t.RawType() != nil && p.prog.SizeOf(t) == 0 { + if p.hasCoroPhysicalBody() { + // The explicit-status guard above owns the nullable case; + // a proven non-nil source needs no memory access. Avoid + // Builder.UnOp's legacy native-stack nil helper and + // materialize the sole zero-sized value directly. + ret = p.prog.Zero(t) + break + } p.assertNilDerefBase(b, v.X) } if isInterfaceCompareDeref(v) { @@ -1310,6 +1591,10 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue ret = p.nilOf(t) break } + if value, handled := p.tryCompileCoroRawCChangeType(b, v); handled { + ret = value + break + } x := p.compileValue(b, v.X) ret = b.ChangeType(p.type_(t, llssa.InGo), x) case *ssa.Convert: @@ -1323,7 +1608,14 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue case *ssa.FieldAddr: x := p.compileValue(b, v.X) p.recordPanicLocation(b, v.Pos()) - if p.isAddressOfFieldAddr(v) { + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionFieldAddr { + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionFieldAddr) + p.observeCoroPhysicalNilGuard(v) + x = p.compileCoroImplicitNilFieldAddrGuard(b, v, x) + } else if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("FieldAddr selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } else if (!physicalPlanned || !p.coroUsesExplicitStatusFaults()) && p.isAddressOfFieldAddr(v) && !ssaAddressValueProvenNonNilAt(v.X, v) { b.AssertNilDeref(x) } ret = b.FieldAddr(x, v.Field) @@ -1336,7 +1628,42 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue return } elem := p.type_(t.Elem(), llssa.InGo) - ret = b.Alloc(elem, v.Heap) + heap := v.Heap + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned { + switch physicalInstruction.recipe { + case coroPhysicalInstructionTerminalResultAllocation: + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionTerminalResultAllocation) + ret = p.compileCoroTerminalResultAllocation(v) + case coroPhysicalInstructionFrameBitcastAllocation: + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionFrameBitcastAllocation) + ret = p.coroFrameAlloca(elem) + case coroPhysicalInstructionFrameAllocation: + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionFrameAllocation) + ret = p.coroFrameAlloc(elem) + case coroPhysicalInstructionOrdinary: + default: + panic(fmt.Sprintf("Alloc selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } + if !ret.IsNil() { + p.debugAlloc(b, v, ret) + break + } + } + exactBitcast := false + if !physicalPlanned { + bitcast, exact := coro.ProveSSAExactScalarBitcast(v.Parent()) + exactBitcast = exact && bitcast.Allocation == v + } + if exactBitcast { + // The exact body stores the complete same-width scalar before its + // single reinterpreted load, so zero initialization is both unnecessary + // and would leave a misleading llvm.memset call in this call-free leaf. + ret = b.AllocaT(elem) + p.debugAlloc(b, v, ret) + break + } + ret = b.Alloc(elem, heap) p.debugAlloc(b, v, ret) case *ssa.IndexAddr: vx := v.X @@ -1346,12 +1673,29 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue x := p.compileValue(b, vx) idx := p.compileValue(b, v.Index) p.recordPanicLocation(b, v.Pos()) - ret = b.IndexAddr(x, idx) + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionIndexAddr { + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionIndexAddr) + ret = p.compileCoroIndexAddrPlanned(b, v, x, idx, physicalInstruction) + } else if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("IndexAddr selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } else if p.frozenSafeFixedArrayIndex(v, v.X, v.Index) { + if _, pointer := types.Unalias(p.patchType(v.X.Type())).Underlying().(*types.Pointer); pointer && + !emissionKnownNonNilArrayBase(v.X) && !ssaValueProvenNonNilAt(v.X, v) { + // Bounds safety says nothing about the implicit *array + // dereference. Keep its ordinary nil fault, routing it through + // the explicit outcome only in a physical coroutine body. + b.AssertNilDeref(x) + } + ret = b.IndexAddrUnchecked(x, idx) + } else { + ret = b.IndexAddr(x, idx) + } case *ssa.Index: x := p.compileValue(b, v.X) idx := p.compileValue(b, v.Index) p.recordPanicLocation(b, v.Pos()) - ret = b.Index(x, idx, func() (addr llssa.Expr, zero bool) { + takeArrayAddr := func() (addr llssa.Expr, zero bool) { switch n := v.X.(type) { case *ssa.Const: zero = true @@ -1359,7 +1703,28 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue addr = p.compileValue(b, n.X) } return - }) + } + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionIndex { + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionIndex) + ret = p.compileCoroIndexPlanned(b, v, x, idx, takeArrayAddr, physicalInstruction) + } else if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("Index selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } else if p.frozenSafeFixedArrayIndex(v, v.X, v.Index) { + switch types.Unalias(p.patchType(v.X.Type())).Underlying().(type) { + case *types.Array: + ret = b.IndexUnchecked(x, idx, takeArrayAddr) + case *types.Pointer: + if !emissionKnownNonNilArrayBase(v.X) && !ssaValueProvenNonNilAt(v.X, v) { + b.AssertNilDeref(x) + } + ret = b.Load(b.IndexAddrUnchecked(x, idx)) + default: + panic("safe fixed-array Index lost its frozen container shape") + } + } else { + ret = b.Index(x, idx, takeArrayAddr) + } case *ssa.Lookup: x := p.compileValue(b, v.X) idx := p.compileValue(b, v.Index) @@ -1385,9 +1750,26 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue max = p.compileValue(b, v.Max) } p.recordPanicLocation(b, v.Pos()) - ret = b.Slice(x, low, high, max) + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionSlice { + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionSlice) + ret = p.compileCoroSlicePlanned(b, v, x, low, high, max, physicalInstruction) + } else if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("Slice selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } else { + ret = b.Slice(x, low, high, max) + } ret.Type = p.type_(v.Type(), llssa.InGo) case *ssa.MakeInterface: + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionSyntheticSelectNoCaseBox { + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionSyntheticSelectNoCaseBox) + ret = p.prog.Nil(p.type_(v.Type(), llssa.InGo)) + break + } + if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("MakeInterface selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } if refs, _ := nonDebugReferrers(v); len(refs) == 1 { switch ref := refs[0].(type) { case *ssa.Store: @@ -1435,7 +1817,39 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue } ret = b.MakeMap(t, nReserve) case *ssa.MakeClosure: - fn := p.compileValue(b, v.Fn) + if !p.rawPlainBody { + if value, handled := p.tryCompileCoroPlainDispatchClosure(b, v); handled { + ret = value + break + } + } + var fn llssa.Expr + if target, ok := v.Fn.(*ssa.Function); ok && p.compilation != nil && p.compilation.CoroEntryResolutionActive() { + // The target's own ValuePlan may require a descriptor at another + // producer. MakeClosure still needs the raw body entry; feeding a + // descriptor-backed closure to Builder.MakeClosure would reinterpret + // the descriptor pointer as executable code. + fn = p.compileRawFunctionValue(target) + if !p.rawPlainBody && len(target.FreeVars) != 0 && p.compilation.CoroPlan != nil { + targetPlan, planned := p.compilation.CoroPlan.FunctionPlan(target) + if planned && targetPlan.Emission == coro.EmitCoroutine { + if p.emissionUniverse == nil { + panic("captured coroutine closure requires a prepared emission universe") + } + entrySig, err := p.emissionUniverse.coroPhysicalEntrySourceSignature(target) + if err != nil { + panic(fmt.Errorf("captured coroutine closure %q: %w", targetPlan.ID, err)) + } + // MakeClosure owns only the canonical {code,env} allocation. Retag + // the managed (g,out,ctx,args) entry as an opaque (ctx,args) + // carrier; no call is emitted through this temporary code word. + carrierSig := p.prog.PhysicalFuncDecl(entrySig, llssa.InGo) + fn = b.ChangeType(p.prog.Type(carrierSig, llssa.InC), fn) + } + } + } else { + fn = p.compileValue(b, v.Fn) + } bindings := p.compileValues(b, v.Bindings, 0) ret = b.MakeClosure(fn, bindings) case *ssa.TypeAssert: @@ -1478,10 +1892,46 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue states[i].Value = p.compileValue(b, s.Send) } } - ret = b.Select(states, v.Blocking) + operation, operationPlanned := p.plannedCoroPhysicalOperation(v) + if !operationPlanned || operation.operation == coroPhysicalOperationNone { + ret = b.Select(states, v.Blocking) + break + } + switch operation.operation { + case coroPhysicalOperationChannelSelectPark: + p.observeCoroPhysicalOperation(v, coroPhysicalOperationChannelSelectPark) + ret = p.compileCoroChanSelect(b, states) + case coroPhysicalOperationChannelSelectTry: + p.observeCoroPhysicalOperation(v, coroPhysicalOperationChannelSelectTry) + ret = p.compileCoroChanTrySelect(b, states) + default: + panic(fmt.Sprintf("channel select selected incompatible frozen physical operation recipe %s", operation.operation)) + } case *ssa.SliceToArrayPointer: t := p.type_(v.Type(), llssa.InGo) x := p.compileValue(b, v.X) + physicalInstruction, physicalPlanned := p.plannedCoroPhysicalInstruction(v) + if physicalPlanned && physicalInstruction.recipe == coroPhysicalInstructionSliceToArrayPointer { + p.observeCoroPhysicalInstruction(v, coroPhysicalInstructionSliceToArrayPointer) + if physicalInstruction.bound == 0 { + ret = b.SliceToArrayPointerUnchecked(x, t) + break + } + p.recordPanicLocation(b, v.Pos()) + ret = p.compileCoroSliceToArrayPointer(b, v, x, t, physicalInstruction) + break + } + if physicalPlanned && physicalInstruction.recipe != coroPhysicalInstructionOrdinary { + panic(fmt.Sprintf("SliceToArrayPointer selected incompatible frozen physical recipe %s", physicalInstruction.recipe)) + } + length, exact := coroSliceToArrayPointerLen(v, p.patchType) + if exact && length == 0 { + // Go deliberately preserves the slice data word here: a nil slice + // converts to nil *[0]T, while an empty non-nil slice converts to a + // non-nil pointer. There is no length fault for N==0. + ret = b.SliceToArrayPointerUnchecked(x, t) + break + } p.recordPanicLocation(b, v.Pos()) ret = b.SliceToArrayPointer(x, t) default: @@ -1612,9 +2062,8 @@ func (p *context) assertNilDerefBase(b llssa.Builder, addr ssa.Value) { } func (p *context) jumpTo(v *ssa.Jump) llssa.BasicBlock { - fn := p.fn succs := v.Block().Succs - return fn.Block(succs[0].Index) + return p.sourceBlock(succs[0].Index) } func (p *context) getDebugLocScope(v *ssa.Function, pos token.Pos) *types.Scope { @@ -1632,6 +2081,9 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if _, ok := p.staticInitInstrs[instr]; ok { return } + finishSite := p.beginCoroSiteEmission(instr) + defer finishSite() + p.observeCoroSemanticInstruction(instr) if enableDbg && instr.Parent().Origin() == nil { if _, isDebugRef := instr.(*ssa.DebugRef); !isDebugRef { scope := p.getDebugLocScope(instr.Parent(), instr.Pos()) @@ -1651,6 +2103,13 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if _, ok := p.staticInitStores[v]; ok { return } + if p.compilation != nil && p.compilation.CoroPlan != nil && + p.compilation.CoroPlan.ElidesConditionalManagedStore(v) { + // Whole-program analysis proved this exact direct descriptor + // publication has no live reader or other target consumer. Avoid + // materializing a reference to the intentionally EmitNone target. + return + } va := v.Addr if va, ok := va.(*ssa.IndexAddr); ok { if args, ok := p.isVArgs(va.X); ok { // varargs: this is a varargs store @@ -1695,13 +2154,28 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { if p.shouldTrackCallerFrames() { p.popCallerLocationFrame(b) } + outcome, outcomePlanned := p.plannedCoroPhysicalOutcome(v) + if outcomePlanned { + if outcome.outcome != coroPhysicalOutcomeReturn { + panic(fmt.Sprintf("return selected incompatible frozen physical outcome recipe %s", outcome.outcome)) + } + p.observeCoroPhysicalOutcome(v, coroPhysicalOutcomeReturn) + p.compileCoroReturn(b, results) + return + } b.Return(results...) case *ssa.If: - fn := p.fn cond := p.compileValue(b, v.Cond) succs := v.Block().Succs - thenb := fn.Block(succs[0].Index) - elseb := fn.Block(succs[1].Index) + thenIndex, elseIndex := 0, 1 + if v == p.patchOriginalInitIf { + // The public patch initializer already claimed init$guard. Enter the + // original source body through the opposite guard edge without + // mutating the shared x/tools SSA CFG. + thenIndex, elseIndex = 1, 0 + } + thenb := p.sourceBlock(succs[thenIndex].Index) + elseb := p.sourceBlock(succs[elseIndex].Index) b.If(cond, thenb, elseb) case *ssa.MapUpdate: m := p.compileValue(b, v.Map) @@ -1710,17 +2184,51 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { p.recordPanicLocation(b, v.Pos()) b.MapUpdate(m, key, val) case *ssa.Defer: + outcome, outcomePlanned := p.plannedCoroPhysicalOutcome(v) + if outcomePlanned { + if outcome.outcome != coroPhysicalOutcomeDeferRegister { + panic(fmt.Sprintf("defer selected incompatible frozen physical outcome recipe %s", outcome.outcome)) + } + p.observeCoroPhysicalOutcome(v, coroPhysicalOutcomeDeferRegister) + p.compileCoroDefer(b, v) + return + } if v.DeferStack != nil { p.callDeferStack(b, p.blkInfos[v.Block().Index].Kind, &v.Call, v.DeferStack, v.Parent()) return } p.call(b, p.blkInfos[v.Block().Index].Kind, &v.Call) case *ssa.Go: + if p.tryCompileCoroClosedStaticSpawn(b, v) { + return + } p.call(b, llssa.Go, &v.Call) case *ssa.RunDefers: + outcome, outcomePlanned := p.plannedCoroPhysicalOutcome(v) + if outcomePlanned { + if outcome.outcome != coroPhysicalOutcomeRunDefers { + panic(fmt.Sprintf("RunDefers selected incompatible frozen physical outcome recipe %s", outcome.outcome)) + } + p.observeCoroPhysicalOutcome(v, coroPhysicalOutcomeRunDefers) + p.compileCoroRunDefers(b, v) + return + } p.recordPanicLocation(b, v.Pos()) b.RunDefers() case *ssa.Panic: + outcome, outcomePlanned := p.plannedCoroPhysicalOutcome(v) + if outcomePlanned { + p.observeCoroPhysicalOutcome(v, outcome.outcome) + switch outcome.outcome { + case coroPhysicalOutcomeSyntheticSelectTrap: + p.compileCoroSyntheticSelectPanic(b, v) + case coroPhysicalOutcomePanic: + p.compileCoroExplicitStatusPanic(b, v) + default: + panic(fmt.Sprintf("panic selected incompatible frozen physical outcome recipe %s", outcome.outcome)) + } + return + } arg := p.compileValue(b, v.X) p.recordPanicLocation(b, v.Pos()) b.Panic(arg) @@ -1728,7 +2236,15 @@ func (p *context) compileInstr(b llssa.Builder, instr ssa.Instruction) { ch := p.compileValue(b, v.Chan) x := p.compileValue(b, v.X) p.recordPanicLocation(b, v.Pos()) - b.Send(ch, x) + operation, operationPlanned := p.plannedCoroPhysicalOperation(v) + if operationPlanned && operation.operation == coroPhysicalOperationChannelSend { + p.observeCoroPhysicalOperation(v, coroPhysicalOperationChannelSend) + p.compileCoroChanSend(b, ch, x) + } else if !operationPlanned || operation.operation == coroPhysicalOperationNone { + b.Send(ch, x) + } else { + panic(fmt.Sprintf("channel send selected incompatible frozen physical operation recipe %s", operation.operation)) + } case *ssa.DebugRef: if enableDbgSyms && v.Parent().Origin() == nil { p.debugRef(b, v) @@ -1755,6 +2271,31 @@ func (p *context) getLocalVariable(b llssa.Builder, fn *ssa.Function, v *types.V } func (p *context) compileFunction(v *ssa.Function) (goFn llssa.Function, pyFn llssa.PyObjRef, kind int) { + if p.rawPlainBody { + return p.compileRawPlainFunction(v) + } + return p.compileManagedFunction(v) +} + +func (p *context) compileManagedFunction(v *ssa.Function) (goFn llssa.Function, pyFn llssa.PyObjRef, kind int) { + if p.compilation != nil && p.compilation.CoroEntryResolutionActive() && + p.compilation.CoroPlan != nil && p.compilation.EmissionUniverse != nil { + canonical, ok := p.compilation.EmissionUniverse.Resolve(v) + if !ok || canonical == nil { + panic(fmt.Errorf("managed function resolution: function %q is absent from the prepared emission universe", v.Name())) + } + if plan, planned := p.compilation.CoroPlan.FunctionPlan(canonical); planned && plan.Emission == coro.EmitRawPlain { + owner := "" + if p.goFn != nil { + owner = p.goFn.String() + } + panic(fmt.Errorf( + "managed function resolution: raw-plain-only function %q (%s) has no managed entry while compiling %s", + plan.ID, canonical.String(), owner, + )) + } + v = canonical + } // TODO(xsw) v.Pkg == nil: means auto generated function? if v.Pkg == p.goPkg || v.Pkg == nil { // function in this package @@ -1766,6 +2307,90 @@ func (p *context) compileFunction(v *ssa.Function) (goFn llssa.Function, pyFn ll return p.funcOf(v) } +// compileFunctionEntry preserves a compiler-selected physical symbol role. +// Generic entries continue through the ordinary resolver. The private +// patch-original initializer must instead carry its already-frozen name into +// both a same-package definition and a cross-package declaration. +func (p *context) compileFunctionEntry(entry plannedFunctionSymbol) (goFn llssa.Function, pyFn llssa.PyObjRef, kind int) { + if !entry.patchOriginalInit { + return p.compileFunction(entry.function) + } + if p.rawPlainBody { + panic("managed patch-original initializer entry requested from a raw plain body") + } + if err := entry.checkSupported(); err != nil { + panic(err) + } + if entry.function.Pkg == p.goPkg || entry.function.Pkg == nil { + return p.compileFuncDeclVariantEntry(p.pkg, entry, false) + } + return p.funcOfEntry(entry) +} + +func (p *context) compileRawPlainFunction(v *ssa.Function) (goFn llssa.Function, pyFn llssa.PyObjRef, kind int) { + if v == nil || p.compilation == nil || p.compilation.CoroPlan == nil || p.compilation.EmissionUniverse == nil { + panic("raw plain function resolution requires an exact function, emission universe, and coroutine plan") + } + canonical, ok := p.compilation.EmissionUniverse.Resolve(v) + if !ok || canonical == nil { + panic(fmt.Errorf("raw plain function resolution: function %q is absent from the prepared emission universe", v.Name())) + } + v = canonical + entry, err := p.resolveFunctionSymbol(v) + if err != nil { + panic(err) + } + if entry.ftype != goFunc { + // Frontend intrinsics such as internal/abi.FuncPCABI0 intentionally have + // no emitted Go body and therefore no raw-demand closure member. Preserve + // their ordinary instruction classification before consulting the Go-body + // emission plan, exactly as managed function resolution does. + return p.funcOfEntry(entry) + } + plan, planned := p.compilation.CoroPlan.FunctionPlan(v) + if !planned { + panic(fmt.Errorf("raw plain function resolution: function %q is absent from the compilation plan", v.Name())) + } + switch plan.Emission { + case coro.EmitPlain, coro.EmitExternal: + // A bounded plain primary or an independently classified external leaf + // already has the only physical ABI this raw caller needs. + return p.compileManagedFunction(v) + case coro.EmitRawPlain: + if !p.compilation.CoroPlan.HasRawPlainVariant(v) { + panic(fmt.Errorf("raw plain function resolution: raw-only function %q has no planned raw plain body", plan.ID)) + } + if v.Pkg == p.goPkg || v.Pkg == nil { + return p.compileFuncDeclVariant(p.pkg, v, true) + } + return p.funcOfEntry(p.mustRawPlainFunctionSymbol(v)) + case coro.EmitCoroutine: + // Continue below: a mixed suspendable target has a separately lowered + // raw body selected by the same frozen closure proof. + case coro.EmitNone: + caller := "" + if p.goFn != nil { + caller = p.goFn.String() + if callerPlan, ok := p.compilation.CoroPlan.FunctionPlan(p.goFn); ok { + caller = fmt.Sprintf("%s [%s]", caller, callerPlan.ID) + } + } + panic(fmt.Errorf( + "raw plain function resolution: caller %s selected non-emitted target %s [%s] (synthetic=%q)", + caller, v.String(), plan.ID, v.Synthetic, + )) + default: + panic(fmt.Errorf("raw plain function resolution: function %q has unsupported emission %s", plan.ID, plan.Emission)) + } + if !p.compilation.CoroPlan.HasRawPlainVariant(v) { + panic(fmt.Errorf("raw plain function resolution: managed coroutine %q has no planned raw plain variant", plan.ID)) + } + if v.Pkg == p.goPkg || v.Pkg == nil { + return p.compileFuncDeclVariant(p.pkg, v, true) + } + return p.funcOfEntry(p.mustRawPlainFunctionSymbol(v)) +} + func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { if iv, ok := v.(instrOrValue); ok { return p.compileInstrOrValue(b, iv, true) @@ -1775,18 +2400,16 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { fn := v.Parent() for idx, param := range fn.Params { if param == v { - return b.Param(idx) + return b.Param(idx + p.coroEmissionSourceParamBase()) } } case *ssa.Function: - if _, _, ftype := p.funcName(v); ftype == llgoInstr { - v = ssawrap.MakeCallWrapper(p.goProg, v) - } - aFn, pyFn, _ := p.compileFunction(v) - if aFn != nil { - return aFn.Expr + if !p.rawPlainBody { + if value, handled := p.tryCompileCoroPlainDispatchFunctionValue(b, v); handled { + return value + } } - return pyFn.Expr + return p.compileRawFunctionValue(v) case *ssa.Global: varName := v.Name() val := p.varOf(b, v) @@ -1809,6 +2432,9 @@ func (p *context) compileValue(b llssa.Builder, v ssa.Value) llssa.Expr { fn := v.Parent() for idx, freeVar := range fn.FreeVars { if freeVar == v { + if value, handled := p.tryCompileCoroFreeVar(b, fn, idx); handled { + return value + } return p.fn.FreeVar(b, idx) } } @@ -1990,7 +2616,8 @@ func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret ll // NewPackageEx and NewPackage compile as a one-shot compilation: each // call gets fresh caller-tracking memoization. Multi-package drivers -// use NewPackageExWithEmbed with a shared CallerTracking instead. +// use NewPackageExWithEmbedOptions with shared CallerTracking and Compilation +// inputs instead. // NewPackageEx compiles a Go package to LLVM IR package. // @@ -2004,7 +2631,7 @@ func NewPackage(prog llssa.Program, pkg *ssa.Package, files []*ast.File) (ret ll // The rewrites map uses short variable names (without package qualifier) and // only affects string-typed globals defined in the current package. func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, nil, patches, rewrites, pkg, files, nil) + return newPackageEx(prog, nil, patches, rewrites, pkg, files, nil, PackageOptions{}) } // NewPackageExWithEmbed compiles a package using pre-loaded go:embed metadata. @@ -2015,15 +2642,47 @@ func NewPackageEx(prog llssa.Program, patches Patches, rewrites map[string]strin // of one compilation (like patches). nil means one-shot: a fresh // instance is created for this call. func NewPackageExWithEmbed(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap) (ret llssa.Package, externs []string, err error) { - return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap) + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, PackageOptions{}) } -func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap *goembed.VarMap) (ret llssa.Package, externs []string, err error) { +// NewPackageExWithEmbedOptions compiles a package with compilation-scoped and +// per-package inputs. Existing one-shot entry points use zero PackageOptions. +func NewPackageExWithEmbedOptions(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap goembed.VarMap, opts PackageOptions) (ret llssa.Package, externs []string, err error) { + return newPackageEx(prog, ct, patches, rewrites, pkg, files, &embedMap, opts) +} + +func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewrites map[string]string, pkg *ssa.Package, files []*ast.File, embedMap *goembed.VarMap, opts PackageOptions) (ret llssa.Package, externs []string, err error) { + var prepared *preparedEmissionPackage + if opts.Compilation != nil && (opts.Compilation.CoroEntryResolutionActive() || opts.Compilation.CoroPhysicalABIActive()) { + if err := opts.Compilation.preflightCoroPlan(); err != nil { + return nil, nil, err + } + if err := opts.Compilation.validateCoroWorkerCodegenProgram(prog); err != nil { + return nil, nil, err + } + if opts.CacheHit { + if err := opts.Compilation.validateCoroCacheIdentity(); err != nil { + return nil, nil, err + } + } + if opts.Compilation.EmissionUniverse != nil { + prepared, err = opts.Compilation.EmissionUniverse.checkPackage(pkg, files, patches) + if err != nil { + return nil, nil, fmt.Errorf("coroutine entry resolution: %w", err) + } + } + } pkgProg := pkg.Prog pkgTypes := pkg.Pkg oldTypes := pkgTypes pkgName, pkgPath := pkgTypes.Name(), llssa.PathOf(pkgTypes) patch, hasPatch := patches[pkgPath] + if prepared != nil { + pkgTypes = prepared.pkgTypes + oldTypes = prepared.oldTypes + patch = prepared.patch + hasPatch = prepared.hasPatch + } if hasPatch { pkgTypes = patch.Types pkg.Pkg = pkgTypes @@ -2052,6 +2711,7 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri skips: make(map[string]none), vargs: make(map[*ssa.Alloc][]llssa.Expr), funcs: make(map[*ssa.Function]llssa.Function), + rawPlainFuncs: make(map[*ssa.Function]llssa.Function), linkOnceFns: make(map[*ssa.Function]none), addrOfFieldAddrs: collectAddrOfFieldSelectors(files), loaded: map[*types.Package]*pkgInfo{ @@ -2060,9 +2720,24 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri cgoSymbols: make([]string, 0, 128), rewrites: rewrites, + compilation: opts.Compilation, + cacheRegistration: opts.CacheHit, + trackCallerFrames: filesUseRuntimeCaller(files) || packageUsesRuntimeCaller(ct, pkg), runtimeCallerFuncs: runtimeCallerFuncSet(ct, pkg), } + if opts.Compilation != nil && opts.Compilation.CoroEntryResolutionActive() { + ctx.emissionUniverse = opts.Compilation.EmissionUniverse + ctx.emissionOwner = prepared + } + ctx.observeCoroPlan() + if ctx.compilation != nil && !ctx.compilation.CoroProfileActive() { + // A report-only plan is delivered exactly once above and must be + // unreachable from every code-generation decision. Clearing the context + // capability here provides one boundary instead of relying on every + // lowering helper to remember an inactive-profile check. + ctx.compilation = nil + } if embedMap != nil { ctx.embedMap = *embedMap } else { @@ -2076,6 +2751,12 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri ctx.prog.SetPatch(ctx.patchType) ctx.prog.SetCompileMethods(ctx.checkCompileMethods) ret.SetResolveLinkname(ctx.resolveLinkname) + if opts.Compilation != nil && opts.Compilation.CoroEntryResolutionActive() { + ret.SetResolveMethodLinkname(ctx.resolveMethodLinkname) + ret.SetResolveMethodToken(ctx.resolveMethodToken) + ret.SetResolveInterfaceMethodDescriptor(ctx.resolveInterfaceMethodDescriptor) + ret.SetResolveRuntimeCall(ctx.resolveCoroLoweredRuntimeCall) + } if hasPatch { skips := ctx.skips @@ -2103,11 +2784,51 @@ func newPackageEx(prog llssa.Program, ct *CallerTracking, patches Patches, rewri ctx.initAfter = nil fn() } + ctx.emitCoroRootPackageAnchor(ret) ret.MaterializePreserveSyms() externs = ctx.cgoSymbols return } +func (p *context) observeCoroPlan() { + if p.cacheRegistration || p.compilation == nil || p.compilation.CoroPlan == nil { + return + } + if observer := p.compilation.CoroPlanObserver; observer != nil { + observer(p.goPkg, p.compilation.CoroPlan) + } +} + +// compileRawFunctionValue returns the selected body entry without applying a +// function-value representation conversion. Static calls and MakeClosure use +// this path even when a different exact producer for the same SSA target is +// descriptor-backed. +func (p *context) compileRawFunctionValue(v *ssa.Function) llssa.Expr { + if p.compilation != nil && p.compilation.CoroEntryResolutionActive() && p.compilation.EmissionUniverse != nil { + canonical, ok := p.compilation.EmissionUniverse.Resolve(v) + if !ok { + panic(fmt.Errorf("coroutine entry resolution: function value %q is absent from the prepared emission universe", v.Name())) + } + v = canonical + } + if _, _, ftype := p.funcName(v); ftype == llgoInstr { + if p.compilation != nil && p.compilation.CoroEntryResolutionActive() && p.compilation.EmissionUniverse != nil { + wrapper, ok := p.compilation.EmissionUniverse.intrinsicWrapper(p.goPkg, v) + if !ok { + panic(fmt.Errorf("coroutine entry resolution: intrinsic function value %q was not materialized before codegen", v.Name())) + } + v = wrapper + } else { + v = ssawrap.MakeCallWrapper(p.goProg, v) + } + } + aFn, pyFn, _ := p.compileFunction(v) + if aFn != nil { + return aFn.Expr + } + return pyFn.Expr +} + func initFnNameOfHasPatch(name string) string { return name + "$hasPatch" } @@ -2143,6 +2864,9 @@ func processPkg(ctx *context, ret llssa.Package, pkg *ssa.Package) { // Do not try to build generic (non-instantiated) functions. continue } + if ctx.omitUnemittedFunction(member) { + continue + } ctx.compileFuncDecl(ret, member) case *ssa.Type: ctx.compileType(ret, member) @@ -2164,7 +2888,16 @@ func (p *context) patchType(typ types.Type) (r types.Type) { } func (p *context) _patchType(typ types.Type) (types.Type, bool) { + original := typ + if universe := p.emissionUniverseForPatch(); universe != nil { + typ, _ = universe.patchEmissionTypeGraph(p, typ) + } switch typ := typ.(type) { + case *types.Alias: + actual := types.Unalias(typ) + if patched, ok := p._patchType(actual); ok { + return patched, true + } case *types.Pointer: if t, ok := p._patchType(typ.Elem()); ok { return types.NewPointer(t), true @@ -2213,6 +2946,37 @@ func (p *context) _patchType(typ types.Type) (types.Type, bool) { if patched { return types.NewStruct(vars, tags), true } + case *types.Interface: + typ.Complete() + methods := make([]*types.Func, typ.NumExplicitMethods()) + embeddeds := make([]types.Type, typ.NumEmbeddeds()) + patched := false + for index := range methods { + method := typ.ExplicitMethod(index) + methodType, ok := p._patchType(method.Type()) + if ok { + methods[index] = types.NewFunc(method.Pos(), method.Pkg(), method.Name(), methodType.(*types.Signature)) + patched = true + } else { + methods[index] = method + } + } + for index := range embeddeds { + embedded := typ.EmbeddedType(index) + if replacement, ok := p._patchType(embedded); ok { + embeddeds[index] = replacement + patched = true + } else { + embeddeds[index] = embedded + } + } + if patched { + iface := types.NewInterfaceType(methods, embeddeds) + if typ.IsImplicit() { + iface.MarkImplicit() + } + return iface.Complete(), true + } case *types.Named: if t, ok := p.patchLocalGenericNamed(typ); ok { return t, true @@ -2248,20 +3012,70 @@ func (p *context) _patchType(typ types.Type) (types.Type, bool) { return types.NewSignature(typ.Recv(), params.(*types.Tuple), results.(*types.Tuple), typ.Variadic()), true } } - return typ, false + return typ, typ != original +} + +func (p *context) emissionUniverseForPatch() *EmissionUniverse { + if p == nil { + return nil + } + if p.emissionUniverse != nil { + return p.emissionUniverse + } + if p.compilation != nil && p.compilation.CoroEntryResolutionActive() { + return p.compilation.EmissionUniverse + } + return nil } func (p *context) patchLocalGenericNamed(t *types.Named) (*types.Named, bool) { - if p.goFn == nil || len(p.goFn.TypeArgs()) == 0 || !p.isGenericLocalType(t.Obj()) { + if p.goFn == nil || isPatchedLocalGenericName(t.Obj().Name()) { return nil, false } - if isPatchedLocalGenericName(t.Obj().Name()) { + universe := p.emissionUniverseForPatch() + if universe != nil { + if canonical := universe.cachedLocalGenericNamed(t); canonical != nil { + return canonical, true + } + } + localCtx := p.localGenericTypeContext(t) + if localCtx == nil && universe != nil { + localCtx = universe.registeredLocalGenericContext(p, t) + } + if localCtx == nil { return nil, false } - obj := types.NewTypeName(t.Obj().Pos(), t.Obj().Pkg(), p.localNamedName(t, false), nil) + if universe != nil { + if canonical := universe.canonicalLocalGenericNamed(localCtx, t); canonical != nil { + return canonical, true + } + } + name := localCtx.localNamedName(t, false) + obj := types.NewTypeName(t.Obj().Pos(), t.Obj().Pkg(), name, nil) return types.NewNamed(obj, t.Underlying(), nil), true } +// localGenericTypeContext finds the instantiated lexical owner of a local +// named type. Anonymous functions share their parent's substitutions, but an +// x/tools local TypeName may have no scope parent; walking Function.Parent is +// therefore required to give outer-body and closure uses one canonical type. +func (p *context) localGenericTypeContext(t *types.Named) *context { + if p == nil || p.goFn == nil || t == nil || t.Obj() == nil { + return nil + } + ctx := *p + for fn := p.goFn; fn != nil; fn = fn.Parent() { + if len(fn.TypeArgs()) == 0 { + continue + } + ctx.goFn = fn + if ctx.isGenericLocalType(t.Obj()) { + return &ctx + } + } + return nil +} + func isPatchedLocalGenericName(name string) bool { // The patched name embeds type arguments in brackets. Go identifiers cannot // contain '[', so this also prevents repeatedly expanding the generated name. @@ -2320,6 +3134,9 @@ func typeListArgs(list *types.TypeList, nameOf func(types.Type) string) []string func (p *context) typeArgName(t types.Type) string { // Keep this formatter aligned with ssa/abi.typeArgString; this variant must // additionally encode local generic type names while patching frontend types. + if universe := p.emissionUniverseForPatch(); universe != nil { + return universe.emissionTypeArgName(p, t) + } switch t := t.(type) { case *types.Alias: return p.typeArgName(types.Unalias(t)) @@ -2494,21 +3311,46 @@ func (p *context) resolveLinkname(name string) string { return name } +// resolveMethodLinkname maps the signature reconstructed by the ABI type +// builder back to the exact x/tools method or wrapper selected for that +// receiver. Active coroutine codegen must use the same frozen physical symbol +// for method-table references and compileFuncDecl definitions. The ordinary +// SetResolveLinkname path remains unchanged for report-only codegen. +func (p *context) resolveMethodLinkname(_ string, method *types.Func, sig *types.Signature) string { + if name, managed := p.resolveManagedInterfaceRawMethodSymbol(method, sig); managed { + return name + } + fn := p.resolveInterfaceMethodSSA(method, sig) + return p.mustFunctionSymbol(fn).name +} + // checkCompileMethods ensures that methods referenced from ABI method tables // are available to the linker. Generic instances and anonymous structural // types are emitted in the current SSA package. Package-level non-generic -// named types normally have source methods emitted by their defining package, -// but promoted wrappers can be synthesized only when a use-site asks for a -// method table, so emit those wrappers on demand. +// named types have declared methods emitted while the defining package's type +// members are compiled. Their generated wrappers are also materialized at each +// ABI-table use site: package archives are compiled independently, so the +// declaring package's plan cannot see every consumer demand. Deterministically +// named generated wrappers are linkonce and may therefore be coalesced safely. +// Active codegen uses the emission universe's declaration certificate instead +// of relying on cloned go/types scope pointers. func (p *context) checkCompileMethods(pkg llssa.Package, typ types.Type) { nt := typ retry: switch t := types.Unalias(nt).(type) { case *types.Named: - if t.TypeArgs() == nil { + if !hasTypeArgs(t) { + if universe := p.emissionUniverseForPatch(); universe != nil { + if _, packageNamed := universe.frozenPackageNamedType(t); packageNamed { + p.compileSyntheticMethods(pkg, typ) + return + } + } obj := t.Obj() - // skip package-level type - if obj.Parent() == obj.Pkg().Scope() { + // Legacy/report-only builds have no frozen provenance. Retain their + // historical package-level test, while active builds above never depend + // on scope pointer equality after typepatch.Clone/Merge. + if obj != nil && obj.Pkg() != nil && obj.Parent() == obj.Pkg().Scope() { p.compileSyntheticMethods(pkg, typ) return } diff --git a/cl/coro_abi.go b/cl/coro_abi.go new file mode 100644 index 0000000000..27c80c7ed3 --- /dev/null +++ b/cl/coro_abi.go @@ -0,0 +1,3090 @@ +/* + * 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 ( + "crypto/sha256" + "encoding/hex" + "fmt" + "go/ast" + "go/constant" + "go/token" + "go/types" + "strings" + + "github.com/goplus/llgo/cl/blocks" + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const coroSyntheticSelectNoCaseMessage = "blocking select matched no case" + +// x/tools emits one unreachable panic block after every blocking select to +// guard its synthetic case-index dispatch. Physical channel lowering proves +// that a completed runtime decision is either a real state index or a +// compiler-owned cancellation edge, so this block is an internal invariant +// trap rather than a user panic requiring managed interface allocation. +func coroSyntheticSelectNoCasePanic(instruction *ssa.Panic) bool { + if instruction == nil || instruction.Pos() != token.NoPos { + return false + } + boxed, ok := instruction.X.(*ssa.MakeInterface) + if !ok { + return false + } + value, ok := boxed.X.(*ssa.Const) + if !ok || value.Value == nil || value.Value.Kind() != constant.String || + constant.StringVal(value.Value) != coroSyntheticSelectNoCaseMessage { + return false + } + for _, block := range instruction.Parent().Blocks { + for _, candidate := range block.Instrs { + if selected, ok := candidate.(*ssa.Select); ok && selected.Blocking { + return true + } + } + } + return false +} + +func coroSyntheticSelectNoCaseBox(instruction *ssa.MakeInterface) bool { + if instruction == nil { + return false + } + refs := instruction.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + panicInstruction, ok := (*refs)[0].(*ssa.Panic) + return ok && panicInstruction.X == instruction && coroSyntheticSelectNoCasePanic(panicInstruction) +} + +const ( + // Version zero is intentionally experimental: the complete CoroHeader and + // FrameDescriptor ABI is not frozen until scheduler/root lowering lands. + coroPhysicalABIVersion uint32 = 0 + coroFrameAllocHook = "__llgo_coro_frame_alloc_v0" + coroFrameFreeHook = "__llgo_coro_frame_free_v0" + coroDescriptorPrefix = "__llgo_coro_frame_descriptor_v0." + + coroPhysicalABIVersionV1 uint32 = 1 + coroFrameAllocHookV1 = "__llgo_coro_frame_alloc_v1" + coroFramePublishHookV1 = "__llgo_coro_frame_publish_v1" + coroAwaitPrepareHookV1 = "__llgo_coro_await_prepare_v3" + coroAwaitConsumeHookV1 = "__llgo_coro_await_consume_v1" + coroPreemptPollHookV1 = "__llgo_coro_preempt_poll_v1" + coroYieldPrepareHookV1 = "__llgo_coro_yield_prepare_v1" + coroCriticalEnterHookV1 = "__llgo_coro_critical_enter_v1" + coroCriticalExitHookV1 = "__llgo_coro_critical_exit_v1" + coroKeyedParkHookV2 = "__llgo_coro_keyed_park_v2" + coroKeyedResumeHookV2 = "__llgo_coro_keyed_resume_v2" + coroRunDecisionTakeHookV1 = "__llgo_coro_run_decision_take_v1" + coroRunDecisionTakeZeroHookV1 = "__llgo_coro_run_decision_take_zero_v1" + coroPanicPrepareHookV1 = "__llgo_coro_panic_prepare_v1" + coroRecoverTakeHookV1 = "__llgo_coro_recover_take_v1" + coroSpawnBeginHookV1 = "__llgo_coro_spawn_begin_v1" + coroSpawnCommitHookV1 = "__llgo_coro_spawn_commit_v1" + coroCompletePrepareHookV2 = "__llgo_coro_complete_prepare_v2" + coroFrameFreeHookV1 = "__llgo_coro_frame_free_v1" + coroDescriptorPrefixV1 = "__llgo_coro_frame_descriptor_v1." +) + +const ( + coroKeyedResumeSuccessV2 uint64 = iota + 1 + coroKeyedResumeTaskAbortV2 + coroKeyedResumeShutdownV2 +) + +const ( + coroHeaderTask = iota + coroHeaderParent + coroHeaderDescriptor + coroHeaderAllocationBase + coroHeaderResultSlot + coroHeaderSuspendReason + coroHeaderLifecycle + coroHeaderStateID + coroHeaderFlags +) + +const ( + coroSuspendNone uint64 = iota + coroSuspendCall + coroSuspendFrameComplete + coroSuspendYield + coroSuspendPark + coroSuspendPanic +) + +const ( + coroLifecycleAllocated uint64 = iota + coroLifecycleInitialSuspended + coroLifecycleActive + coroLifecycleSuspended + coroLifecycleFinalSuspended + coroLifecycleDestroyPending + coroLifecycleDestroyed +) + +// coroPreemptInstructionBudget bounds straight-line source work between +// compiler-inserted scheduler handoffs. Loop SCC entries are separate +// safepoints, so even a tiny loop cannot run forever without a cut. +const coroPreemptInstructionBudget = 64 + +type coroPhysicalABI struct { + version uint32 + hash [16]byte + descriptorName string + frameAllocHook string + frameFreeHook string + framePublishHook string + awaitPrepareHook string + awaitConsumeHook string + preemptPollHook string + yieldPrepareHook string + criticalEnterHook string + criticalExitHook string + runDecisionTakeHook string + runDecisionTakeZeroHook string + panicPrepareHook string + recoverTakeHook string + completePrepareHook string + physicalSig *types.Signature + resultSlotType types.Type + resultCount int +} + +// coroBodyContext exists only while emitting one physical coroutine body. It +// carries the current handle/header explicitly so call lowering never guesses a +// frame layout from a raw handle. +type coroBodyContext struct { + coro *llssa.CoroBuilder + abi coroPhysicalABI + cleanup *coroStaticCleanupState + header llssa.Expr + task llssa.Expr + resultSlot llssa.Expr + completion llssa.BasicBlock + finalSuspend llssa.BasicBlock + preemptPoll llssa.Expr + yieldPrepare llssa.Expr + criticalEnter llssa.Expr + criticalExit llssa.Expr + runDecisionTakeZero llssa.Expr + runDecisionTrap llssa.Expr + unsupportedRunDecision llssa.BasicBlock + cancelRunDecision llssa.BasicBlock + abortRunDecision llssa.BasicBlock + shutdownRunDecision llssa.BasicBlock + panicPrepare llssa.Expr + completePrepare llssa.Expr + terminalStatus llssa.Expr + nextState uint32 + terminalState uint32 + needsPreempt bool + instructions int + frameRetention *coroFrameRetentionProof + critical *coroCriticalProof + terminalResultAllocs map[*ssa.Alloc]llssa.Expr + sourceBlockPollFresh bool +} + +func newCoroPhysicalABI(p *context, entry plannedFunctionSymbol, sourceSig *types.Signature) coroPhysicalABI { + // Declared methods use x/tools' receiver-as-Params[0] SSA convention. Keep + // one receiver-free callable signature everywhere below so the descriptor + // hash, ramp parameters, result slot, and child-await call all see the same + // physical source ABI. + sourceSig = coroPhysicalNormalizeSourceSignature(sourceSig) + version := coroPhysicalABIVersion + frameAllocHook := coroFrameAllocHook + frameFreeHook := coroFrameFreeHook + descriptorPrefix := coroDescriptorPrefix + framePublishHook := "" + awaitPrepareHook := "" + awaitConsumeHook := "" + preemptPollHook := "" + yieldPrepareHook := "" + criticalEnterHook := "" + criticalExitHook := "" + runDecisionTakeHook := "" + runDecisionTakeZeroHook := "" + panicPrepareHook := "" + recoverTakeHook := "" + faultPrepareHook := "" + faultPayloadHook := "" + completePrepareHook := "" + if p.compilation != nil && p.compilation.CoroChildAwaitActive() { + version = coroPhysicalABIVersionV1 + frameAllocHook = coroFrameAllocHookV1 + frameFreeHook = coroFrameFreeHookV1 + descriptorPrefix = coroDescriptorPrefixV1 + framePublishHook = coroFramePublishHookV1 + awaitPrepareHook = coroAwaitPrepareHookV1 + awaitConsumeHook = coroAwaitConsumeHookV1 + preemptPollHook = coroPreemptPollHookV1 + yieldPrepareHook = coroYieldPrepareHookV1 + runDecisionTakeHook = coroRunDecisionTakeHookV1 + runDecisionTakeZeroHook = coroRunDecisionTakeZeroHookV1 + completePrepareHook = coroCompletePrepareHookV2 + if p.compilation.CoroProgramBootstrapActive() { + criticalEnterHook = coroCriticalEnterHookV1 + criticalExitHook = coroCriticalExitHookV1 + } + } + if p.compilation != nil && p.compilation.CoroExplicitStatusActive() { + panicPrepareHook = coroPanicPrepareHookV1 + recoverTakeHook = coroRecoverTakeHookV1 + faultPrepareHook = coroFaultPrepareHookV1 + faultPayloadHook = coroFaultPayloadHookV1 + } + resultFields := make([]*types.Var, sourceSig.Results().Len()) + for i := range resultFields { + resultFields[i] = types.NewField(token.NoPos, nil, fmt.Sprintf("r%d", i), sourceSig.Results().At(i).Type(), false) + } + resultSlotType := types.NewStruct(resultFields, nil) + physicalParams := make([]*types.Var, 0, sourceSig.Params().Len()+2) + physicalParams = append(physicalParams, + types.NewParam(token.NoPos, nil, "__llgo_g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "__llgo_out", types.Typ[types.UnsafePointer]), + ) + for i := 0; i < sourceSig.Params().Len(); i++ { + physicalParams = append(physicalParams, sourceSig.Params().At(i)) + } + physicalResults := types.NewTuple(types.NewParam(token.NoPos, nil, "__llgo_handle", types.Typ[types.UnsafePointer])) + physicalSig := types.NewSignatureType(nil, nil, nil, types.NewTuple(physicalParams...), physicalResults, false) + + qualified := func(pkg *types.Package) string { + if pkg == nil { + return "" + } + return llssa.PathOf(pkg) + } + target := p.prog.TargetSpec() + coroABI := coro.PhysicalABIV0 + schedulerABI := coro.SchedulerNoneABIV0 + if p.compilation != nil && p.compilation.CoroChildAwaitActive() { + coroABI = coro.PhysicalABIV1 + schedulerABI = coro.SchedulerChildAwaitABIV0 + } + panicABI := coro.PanicLegacyABIV0 + funcRepABI := coro.FuncRepABIV0 + if p.compilation != nil { + if p.compilation.CoroABI != "" { + coroABI = p.compilation.CoroABI + } + if p.compilation.SchedulerABI != "" { + schedulerABI = p.compilation.SchedulerABI + } + if p.compilation.PanicABI != "" { + panicABI = p.compilation.PanicABI + } + if p.compilation.FuncRepABI != "" { + funcRepABI = p.compilation.FuncRepABI + } + } + key := fmt.Sprintf( + "llgo-coro-physical-v%d\x00%s\x00coro=%s\x00scheduler=%s\x00panic=%s\x00panic-hook=%s\x00recover-take=%s\x00fault-hook=%s\x00fault-payload-hook=%s\x00func-rep=%s\x00await-prepare=%s\x00await-consume=%s\x00resume-decision=%s\x00resume-decision-zero=%s\x00critical-enter=%s\x00critical-exit=%s\x00triple=%s\x00cpu=%s\x00features=%s\x00target-abi=%s\x00data-layout=%s\x00ptr=%d\x00sig=%s\x00result=%s", + version, + entry.plan.ID, + coroABI, + schedulerABI, + panicABI, + panicPrepareHook, + recoverTakeHook, + faultPrepareHook, + faultPayloadHook, + funcRepABI, + awaitPrepareHook, + awaitConsumeHook, + runDecisionTakeHook, + runDecisionTakeZeroHook, + criticalEnterHook, + criticalExitHook, + target.Triple, + target.CPU, + target.Features, + target.TargetABI, + p.prog.DataLayout(), + p.prog.PointerSize(), + types.TypeString(sourceSig, qualified), + types.TypeString(resultSlotType, qualified), + ) + sum := sha256.Sum256([]byte(key)) + var hash [16]byte + copy(hash[:], sum[:len(hash)]) + return coroPhysicalABI{ + version: version, + hash: hash, + descriptorName: descriptorPrefix + hex.EncodeToString(hash[:]), + frameAllocHook: frameAllocHook, + frameFreeHook: frameFreeHook, + framePublishHook: framePublishHook, + awaitPrepareHook: awaitPrepareHook, + awaitConsumeHook: awaitConsumeHook, + preemptPollHook: preemptPollHook, + yieldPrepareHook: yieldPrepareHook, + criticalEnterHook: criticalEnterHook, + criticalExitHook: criticalExitHook, + runDecisionTakeHook: runDecisionTakeHook, + runDecisionTakeZeroHook: runDecisionTakeZeroHook, + panicPrepareHook: panicPrepareHook, + recoverTakeHook: recoverTakeHook, + completePrepareHook: completePrepareHook, + physicalSig: physicalSig, + resultSlotType: resultSlotType, + resultCount: sourceSig.Results().Len(), + } +} + +func coroHeaderType(prog llssa.Program) llssa.Type { + return prog.Struct( + prog.VoidPtr(), // g + prog.VoidPtr(), // parent + prog.VoidPtr(), // descriptor + prog.VoidPtr(), // allocation base (published by the future runtime) + prog.VoidPtr(), // result slot + prog.Uint16(), // suspend reason + prog.Uint16(), // lifecycle state + prog.Uint32(), // state ID + prog.Uint32(), // flags + ) +} + +func (p *context) beginCoroBody( + b llssa.Builder, + abi coroPhysicalABI, + terminalResultAllocations []*ssa.Alloc, +) *coroBodyContext { + prog := p.prog + resultType := prog.Type(abi.resultSlotType, llssa.InGo) + descriptor := p.pkg.NewCoroFrameDescriptor(abi.descriptorName, llssa.CoroFrameDescriptorOptions{ + Version: abi.version, + ABIHash: abi.hash, + Result: resultType, + }) + descriptorPtr := b.Convert(prog.VoidPtr(), descriptor) + task := p.fn.PhysicalParam(0) + resultSlot := p.fn.PhysicalParam(1) + null := prog.Nil(prog.VoidPtr()) + headerType := coroHeaderType(prog) + header := b.AllocaT(headerType) + initialLifecycle := uint64(coroLifecycleAllocated) + if abi.version >= coroPhysicalABIVersionV1 { + initialLifecycle = coroLifecycleInitialSuspended + } + headerValues := []llssa.Expr{ + task, + null, + descriptorPtr, + null, + resultSlot, + prog.IntVal(coroSuspendNone, prog.Uint16()), + prog.IntVal(initialLifecycle, prog.Uint16()), + prog.IntVal(0, prog.Uint32()), + prog.IntVal(0, prog.Uint32()), + } + allocSig := coroFrameAllocSignature(abi.version) + freeSig := coroFrameFreeSignature(abi.version) + alloc := p.pkg.NewFunc(abi.frameAllocHook, allocSig, llssa.InC) + free := p.pkg.NewFunc(abi.frameFreeHook, freeSig, llssa.InC) + frame := llssa.CoroFrameOps{ + Alloc: func(b llssa.Builder, size, align llssa.Expr) llssa.Expr { + if abi.version >= coroPhysicalABIVersionV1 { + return b.Call(alloc.Expr, task, size, align, descriptorPtr) + } + return b.Call(alloc.Expr, size, align, descriptorPtr) + }, + Free: func(b llssa.Builder, storage, size, align llssa.Expr) { + if abi.version >= coroPhysicalABIVersionV1 { + b.Call(free.Expr, task, storage, size, align, descriptorPtr) + return + } + b.Call(free.Expr, storage, size, align, descriptorPtr) + }, + } + body := &coroBodyContext{ + abi: abi, + header: header, + task: task, + resultSlot: resultSlot, + nextState: 1, + terminalResultAllocs: make(map[*ssa.Alloc]llssa.Expr, len(terminalResultAllocations)), + } + if abi.version >= coroPhysicalABIVersionV1 { + // The cleanup base is frame-local rather than G-local: deferred code + // invoked while a task is canceling must still be able to make ordinary + // managed calls and receive their ordinary Return outcomes. + body.terminalStatus = b.AllocaT(prog.Uint32()) + b.Store(body.terminalStatus, prog.IntVal(coroAwaitCompletionReturn, prog.Uint32())) + } + if abi.runDecisionTakeZeroHook != "" { + body.runDecisionTakeZero = p.pkg.NewFunc( + abi.runDecisionTakeZeroHook, coroRunDecisionTakeZeroSignature(), llssa.InC, + ).Expr + if p.compilation != nil && (p.compilation.CoroChannelActive() || p.compilation.CoroWorkerActive() || + p.compilation.CoroFrameRetentionABI == CoroFrameRetentionParkABIV2) { + body.unsupportedRunDecision = p.fn.MakeBlock() + body.runDecisionTrap = p.pkg.NewFunc( + "llvm.trap", types.NewSignatureType(nil, nil, nil, nil, nil, false), llssa.InC, + ).Expr + } + } + if abi.completePrepareHook != "" { + body.completePrepare = p.pkg.NewFunc(abi.completePrepareHook, coroCompletePrepareSignature(), llssa.InC).Expr + } + if abi.yieldPrepareHook != "" { + body.yieldPrepare = p.pkg.NewFunc(abi.yieldPrepareHook, coroYieldPrepareSignature(), llssa.InC).Expr + } + if abi.criticalEnterHook != "" { + body.criticalEnter = p.pkg.NewFunc(abi.criticalEnterHook, coroCriticalEnterSignature(), llssa.InC).Expr + } + if abi.criticalExitHook != "" { + body.criticalExit = p.pkg.NewFunc(abi.criticalExitHook, coroCriticalExitSignature(), llssa.InC).Expr + } + if abi.panicPrepareHook != "" { + body.panicPrepare = p.pkg.NewFunc(abi.panicPrepareHook, coroPanicPrepareSignature(), llssa.InC).Expr + } + if abi.preemptPollHook != "" { + body.preemptPoll = p.pkg.NewFunc(abi.preemptPollHook, coroPreemptPollSignature(), llssa.InC).Expr + } + coroOptions := llssa.CoroOptions{ + Promise: header, + Frame: frame, + BeforeInitialSuspend: func(b llssa.Builder, handle, storage llssa.Expr) { + for i, value := range headerValues { + b.Store(b.FieldAddr(header, i), value) + } + if abi.framePublishHook != "" { + publish := p.pkg.NewFunc(abi.framePublishHook, coroFramePublishSignature(), llssa.InC) + b.Call(publish.Expr, task, handle, b.Convert(prog.VoidPtr(), header), storage) + } + // A named result captured by a defer is an ordinary Go heap object, + // but x/tools reloads it from compiler-owned RunDefers continuations. + // Define only that structurally certified subset after frame/header + // publication and before the initial suspend. Its pointer then dominates + // every normal, cancellation, and cleanup continuation, and CoroSplit + // retains it exactly when live without changing heap identity. + for _, allocation := range terminalResultAllocations { + if allocation == nil || !allocation.Heap || allocation.Parent() != p.goFn || + allocation.Block() == nil || allocation.Block().Index != 0 { + panic("coroutine terminal-result allocation lost its exact source-entry heap proof") + } + if _, duplicate := body.terminalResultAllocs[allocation]; duplicate { + panic("duplicate coroutine terminal-result allocation") + } + pointer, ok := types.Unalias(allocation.Type()).Underlying().(*types.Pointer) + if !ok { + panic("coroutine terminal-result allocation is not pointer typed") + } + value := func() llssa.Expr { + finishSite := p.beginCoroRelocatedSiteEmission(allocation, coroRuntimeHelperAtPrologue) + defer finishSite() + return b.Alloc(p.type_(pointer.Elem(), llssa.InGo), true) + }() + body.terminalResultAllocs[allocation] = value + p.bvals[allocation] = value + } + }, + } + if !body.runDecisionTakeZero.IsNil() { + coroOptions.AfterResumeDispatch = body.dispatchZeroRunDecision + } + body.coro = b.BeginCoro(coroOptions) + if body.unsupportedRunDecision != nil { + // Every zero-ticket gate in this physical body shares one fail-closed + // destination. Restore the compiler-owned initial normal continuation + // before source lowering starts. + initialResume := body.coro.InitialResumeBlock() + b.SetBlock(body.unsupportedRunDecision) + b.Call(body.runDecisionTrap) + b.Unreachable() + b.SetBlock(initialResume) + } + return body +} + +func coroFrameAllocSignature(version uint32) *types.Signature { + params := []*types.Var{ + types.NewParam(token.NoPos, nil, "size", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, nil, "align", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, nil, "descriptor", types.Typ[types.UnsafePointer]), + } + if version >= coroPhysicalABIVersionV1 { + params = append([]*types.Var{types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer])}, params...) + } + results := types.NewTuple(types.NewParam(token.NoPos, nil, "frame", types.Typ[types.UnsafePointer])) + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), results, false) +} + +func coroFrameFreeSignature(version uint32) *types.Signature { + params := []*types.Var{ + types.NewParam(token.NoPos, nil, "frame", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "size", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, nil, "align", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, nil, "descriptor", types.Typ[types.UnsafePointer]), + } + if version >= coroPhysicalABIVersionV1 { + params = append([]*types.Var{types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer])}, params...) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), nil, false) +} + +func coroFramePublishSignature() *types.Signature { + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "handle", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "header", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "storage", types.Typ[types.UnsafePointer]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroAwaitPrepareSignature() *types.Signature { + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "parent", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "child", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "recoverMode", types.Typ[types.Uint32]), + types.NewParam(token.NoPos, nil, "recoverType", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "recoverData", types.Typ[types.UnsafePointer]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroAwaitConsumeSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "parent", pointer), + types.NewParam(token.NoPos, nil, "typeOut", pointer), + types.NewParam(token.NoPos, nil, "dataOut", pointer), + ) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "status", types.Typ[types.Uint32])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func coroCompletePrepareSignature() *types.Signature { + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "handle", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "header", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "status", types.Typ[types.Uint32]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroYieldPrepareSignature() *types.Signature { + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "handle", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "header", types.Typ[types.UnsafePointer]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroKeyedParkSignatureV2() *types.Signature { + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "handle", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "header", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "state", types.Typ[types.UnsafePointer]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroKeyedResumeSignatureV2() *types.Signature { + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "state", types.Typ[types.UnsafePointer]), + ) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "status", types.Typ[types.Uint32])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func coroRunDecisionTakeZeroSignature() *types.Signature { + params := types.NewTuple(types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer])) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "taskKind", types.Typ[types.Uint32])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func coroPreemptPollSignature() *types.Signature { + params := types.NewTuple(types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer])) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "requested", types.Typ[types.Bool])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func coroCriticalEnterSignature() *types.Signature { + params := types.NewTuple(types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer])) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroCriticalExitSignature() *types.Signature { + params := types.NewTuple(types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer])) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "requested", types.Typ[types.Bool])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func coroPanicPrepareSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + types.NewParam(token.NoPos, nil, "header", pointer), + types.NewParam(token.NoPos, nil, "typeWord", pointer), + types.NewParam(token.NoPos, nil, "dataWord", pointer), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func (c *coroBodyContext) publishState(b llssa.Builder, reason, lifecycle uint64, stateID uint32) { + prog := b.Prog + b.Store(b.FieldAddr(c.header, coroHeaderSuspendReason), prog.IntVal(reason, prog.Uint16())) + b.Store(b.FieldAddr(c.header, coroHeaderLifecycle), prog.IntVal(lifecycle, prog.Uint16())) + b.Store(b.FieldAddr(c.header, coroHeaderStateID), prog.IntVal(uint64(stateID), prog.Uint32())) +} + +func (c *coroBodyContext) activate(b llssa.Builder) { + if c.abi.version < coroPhysicalABIVersionV1 { + return + } + prog := b.Prog + b.Store(b.FieldAddr(c.header, coroHeaderSuspendReason), prog.IntVal(coroSuspendNone, prog.Uint16())) + b.Store(b.FieldAddr(c.header, coroHeaderLifecycle), prog.IntVal(coroLifecycleActive, prog.Uint16())) +} + +// dispatchZeroRunDecision emits the exactly-once compiler resume gate for a +// non-park continuation. The runtime scalar ABI validates the complete +// zero-ticket decision and returns only None/Abort/Shutdown. No output address +// exists for CoroSplit to retain in the stackless coroutine frame. +func (c *coroBodyContext) dispatchZeroRunDecision(b llssa.Builder, normal llssa.BasicBlock) { + if c.cancelRunDecision == nil { + c.cancelRunDecision = b.Func.MakeBlock() + } + c.dispatchZeroRunDecisionTo(b, normal, c.cancelRunDecision) +} + +func (c *coroBodyContext) dispatchZeroRunDecisionTo( + b llssa.Builder, normal, canceled llssa.BasicBlock, +) { + if c.abi.version < coroPhysicalABIVersionV1 || c.runDecisionTakeZero.IsNil() { + panic("coroutine resume requires PhysicalABIV1 zero-ticket run-decision hook") + } + if canceled == nil { + panic("coroutine resume decision has no cancellation destination") + } + if c.terminalStatus.IsNil() { + panic("coroutine resume cancellation requires frame-local terminal status") + } + zero := b.Prog.IntVal(0, b.Prog.Uint32()) + taskKind := b.Call(c.runDecisionTakeZero, c.task) + // The runtime ABI validates the complete decision and aborts before return + // for every value other than None/Abort/Shutdown. Any nonzero value reaching + // generated IR is therefore an exact task-cancellation cleanup request. + isCanceled := b.BinOp(token.NEQ, taskKind, zero) + // Runtime validation restricts nonzero taskKind to Abort=1/Shutdown=2; + // CompletionAbort/Shutdown are exactly those values plus two. Preserve the + // existing base on normal resumes so safepoints inside cleanup are masked. + mapped := b.BinOp(token.ADD, taskKind, b.Prog.IntVal(2, b.Prog.Uint32())) + current := b.Load(c.terminalStatus) + b.Store(c.terminalStatus, b.SelectValue(isCanceled, mapped, current)) + b.If(isCanceled, canceled, normal) +} + +func (c *coroBodyContext) bindCancellationCompletion(b llssa.Builder) { + if c.cancelRunDecision == nil && c.runDecisionTakeZero.IsNil() { + return + } + if c.cancelRunDecision == nil || c.completion == nil { + panic("coroutine cancellation resume gate requires a completion block") + } + b.SetBlock(c.cancelRunDecision) + if c.cleanup == nil { + b.Jump(c.completion) + } else { + c.cleanup.enterCancellation(b) + } +} + +// cancellationRunDecisionTargets adapts operation-specific resume statuses to +// the same frame-local terminal base used by the scalar zero-ticket gate. The +// operation resume hook has already consumed/discarded its result ownership; +// these tiny blocks only retain Abort versus Shutdown before shared cleanup. +func (c *coroBodyContext) cancellationRunDecisionTargets( + b llssa.Builder, +) (abort, shutdown llssa.BasicBlock) { + if b.Func == nil || c.cancelRunDecision == nil || c.terminalStatus.IsNil() { + panic("coroutine operation cancellation requires a bound cleanup destination") + } + makeTarget := func(status uint64) llssa.BasicBlock { + target := b.Func.MakeBlock() + builder := b.Func.NewBuilder() + defer builder.Dispose() + builder.SetBlock(target) + builder.Store(c.terminalStatus, builder.Prog.IntVal(status, builder.Prog.Uint32())) + builder.Jump(c.cancelRunDecision) + return target + } + if c.abortRunDecision == nil { + c.abortRunDecision = makeTarget(coroAwaitCompletionAbort) + } + if c.shutdownRunDecision == nil { + c.shutdownRunDecision = makeTarget(coroAwaitCompletionShutdown) + } + return c.abortRunDecision, c.shutdownRunDecision +} + +func (c *coroBodyContext) enterCancellation(b llssa.Builder, status uint64) { + if c.terminalStatus.IsNil() || c.completion == nil || + (status != coroAwaitCompletionAbort && status != coroAwaitCompletionShutdown) { + panic("coroutine cancellation has an invalid terminal status") + } + b.Store(c.terminalStatus, b.Prog.IntVal(status, b.Prog.Uint32())) + if c.cleanup == nil { + b.Jump(c.completion) + } else { + c.cleanup.enterCancellation(b) + } +} + +func (c *coroBodyContext) suspendForChild(b llssa.Builder) uint32 { + if c.abi.version < coroPhysicalABIVersionV1 { + panic("coroutine child suspension requires PhysicalABIV1") + } + stateID := c.nextState + c.nextState++ + c.instructions = 0 + c.publishState(b, coroSuspendCall, coroLifecycleSuspended, stateID) + return stateID +} + +func (c *coroBodyContext) pollAndSuspendForPreempt(b llssa.Builder) uint32 { + if c.abi.version < coroPhysicalABIVersionV1 || c.preemptPoll.IsNil() || c.yieldPrepare.IsNil() { + panic("coroutine preemption requires PhysicalABIV1 poll and scheduler handoff hooks") + } + return c.suspendCurrentFrameIfYieldRequested(b, b.Call(c.preemptPoll, c.task)) +} + +// suspendCurrentFrameIfYieldRequested is the shared conditional runnable +// handoff used by an ordinary poll and by the outermost critical-region exit. +// The runtime has already consumed the exact request before requested=true is +// returned; only the true edge publishes and suspends this physical frame. +func (c *coroBodyContext) suspendCurrentFrameIfYieldRequested(b llssa.Builder, requested llssa.Expr) uint32 { + if c.abi.version < coroPhysicalABIVersionV1 || c.yieldPrepare.IsNil() { + panic("coroutine conditional preemption requires the PhysicalABIV1 scheduler handoff hook") + } + stateID := c.nextState + c.nextState++ + c.instructions = 0 + c.coro.SuspendCurrentBlockIf(requested, func(suspend llssa.Builder) { + c.publishState(suspend, coroSuspendYield, coroLifecycleSuspended, stateID) + suspend.Call(c.yieldPrepare, c.task, c.coro.Handle(), suspend.Convert(suspend.Prog.VoidPtr(), c.header)) + }) + // The false edge is already active. The CoroBuilder AfterResume callback + // consumes a decision only on the resumed true edge before this join. + c.activate(b) + return stateID +} + +func (c *coroBodyContext) yieldCurrentFrame(b llssa.Builder) uint32 { + if c.abi.version < coroPhysicalABIVersionV1 || c.yieldPrepare.IsNil() { + panic("coroutine yield requires PhysicalABIV1 scheduler handoff hooks") + } + stateID := c.nextState + c.nextState++ + c.instructions = 0 + c.publishState(b, coroSuspendYield, coroLifecycleSuspended, stateID) + b.Call(c.yieldPrepare, c.task, c.coro.Handle(), b.Convert(b.Prog.VoidPtr(), c.header)) + c.coro.SuspendCurrentBlock() + c.activate(b) + return stateID +} + +func (p *context) compileCoroPark(b llssa.Builder, args []llssa.Expr) { + body := p.requireCoroParkV2Body(b, "keyed wait") + if b.Func != p.fn || len(args) != 2 { + panic("llgo.coroPark requires exactly (state, reserved) in the active coroutine function") + } + state := b.Convert(b.Prog.VoidPtr(), args[0]) + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.Prog.BoolVal(true), + park: func(suspend llssa.Builder) { + park := p.pkg.NewFunc(coroKeyedParkHookV2, coroKeyedParkSignatureV2(), llssa.InC) + suspend.Call( + park.Expr, + body.task, + body.coro.Handle(), + suspend.Convert(suspend.Prog.VoidPtr(), body.header), + state, + ) + }, + resume: func(resume llssa.Builder) llssa.Expr { + resumeHook := p.pkg.NewFunc(coroKeyedResumeHookV2, coroKeyedResumeSignatureV2(), llssa.InC) + return resume.Call(resumeHook.Expr, body.task, state) + }, + normal: []uint64{coroKeyedResumeSuccessV2}, + abort: coroKeyedResumeTaskAbortV2, + shutdown: coroKeyedResumeShutdownV2, + }) +} + +func (p *context) compileCoroYield(b llssa.Builder) { + body := p.coroBody() + if body == nil || p.compilation == nil || !p.compilation.CoroChildAwaitActive() { + panic("llgo.coroYield requires an active PhysicalABIV1 coroutine body") + } + if b.Func != p.fn { + panic("llgo.coroYield requires the active coroutine function") + } + body.yieldCurrentFrame(b) +} + +func (c *coroBodyContext) countInstructionAndMaybeYield(b llssa.Builder) { + if !c.needsPreempt { + return + } + if c.instructions >= coroPreemptInstructionBudget { + c.pollAndSuspendForPreempt(b) + } + c.instructions++ +} + +func (c *coroBodyContext) terminalStateID() uint32 { + if c.terminalState == 0 { + c.terminalState = c.nextState + c.nextState++ + } + return c.terminalState +} + +func (c *coroBodyContext) complete(b llssa.Builder) { + if c.abi.version < coroPhysicalABIVersionV1 { + b.Jump(c.finalSuspend) + return + } + c.publishState(b, coroSuspendFrameComplete, coroLifecycleFinalSuspended, c.terminalStateID()) + if !c.completePrepare.IsNil() { + if c.terminalStatus.IsNil() { + panic("coroutine completion has no frame-local terminal status") + } + b.Call( + c.completePrepare, + c.task, + c.coro.Handle(), + b.Convert(b.Prog.VoidPtr(), c.header), + b.Load(c.terminalStatus), + ) + } + b.Jump(c.finalSuspend) +} + +func (c *coroBodyContext) panic(b llssa.Builder, typeWord, dataWord llssa.Expr) { + if c.abi.version < coroPhysicalABIVersionV1 || c.panicPrepare.IsNil() || c.finalSuspend == nil { + panic("explicit-status panic requires a PhysicalABIV1 prepare hook and shared final suspend") + } + c.publishState(b, coroSuspendPanic, coroLifecycleFinalSuspended, c.terminalStateID()) + b.Call( + c.panicPrepare, + c.task, + c.coro.Handle(), + b.Convert(b.Prog.VoidPtr(), c.header), + b.Convert(b.Prog.VoidPtr(), typeWord), + b.Convert(b.Prog.VoidPtr(), dataWord), + ) + b.Jump(c.finalSuspend) +} + +func (c *coroBodyContext) finish(b llssa.Builder) { + c.coro.Finish() +} + +func (p *context) storeCoroLeafResult(b llssa.Builder, abi coroPhysicalABI, resultSlot llssa.Expr, results []llssa.Expr) { + if len(results) != abi.resultCount { + panic(fmt.Sprintf("coroutine result count %d does not match ABI count %d", len(results), abi.resultCount)) + } + if len(results) == 0 { + return + } + resultType := p.prog.Type(abi.resultSlotType, llssa.InGo) + typedSlot := b.Convert(p.prog.Pointer(resultType), resultSlot) + for i, result := range results { + b.Store(b.FieldAddr(typedSlot, i), result) + } +} + +func (p *context) compileCoroPhysicalBody(b llssa.Builder, fn *ssa.Function, abi coroPhysicalABI, isInit bool) { + sourceParamBase := 2 + if len(fn.FreeVars) != 0 { + // Captured descriptor entries are (g,out,ctx,args...). The context is an + // explicit physical parameter rather than aFunction's legacy implicit + // closure parameter, so SSA source parameters begin after all three words. + sourceParamBase = 3 + } + + if p.emissionUniverse == nil || p.emissionUniverse.coroProgramIR == nil { + panic("coroutine physical body has no ProgramIR") + } + physicalPlan, err := (emissionCanonicalIndex{universe: p.emissionUniverse}).physicalFunctionPlanForEmission(fn, p.emissionOwner) + if err != nil { + panic(fmt.Errorf("load frozen coroutine physical plan: %w", err)) + } + frameRetention := physicalPlan.frameRetention + critical := physicalPlan.critical + cleanupPlan := physicalPlan.cleanup + emission, finishEmission := p.beginCoroPhysicalEmission( + physicalPlan, sourceParamBase, abi.panicPrepareHook != "", + ) + defer finishEmission() + + b.SetBlock(p.fn.Block(0)) + cleanup := p.beginCoroStaticCleanup(b, cleanupPlan) + terminalResultAllocations := []*ssa.Alloc(nil) + if cleanupPlan != nil { + terminalResultAllocations = cleanupPlan.terminalResultAllocations + } + if !coroTerminalResultAllocationSetMatches(frameRetention, terminalResultAllocations) { + panic("coroutine cleanup plan and frame-retention proof disagree on terminal-result allocations") + } + physical := p.beginCoroBody(b, abi, terminalResultAllocations) + physical.frameRetention = frameRetention + physical.critical = critical + physical.cleanup = cleanup + if physical.cleanup != nil { + physical.cleanup.bindBlocks(p.fn) + } + + // Create source blocks after BeginCoro's canonical ramp/suspend blocks so + // presplit IR remains in execution order for LLVM diagnostics and ABI tests. + sourceBlocks := make([]llssa.BasicBlock, len(fn.Blocks)) + for i := range sourceBlocks { + sourceBlocks[i] = p.fn.MakeBlock() + } + physical.completion = p.fn.MakeBlock() + physical.finalSuspend = p.fn.MakeBlock() + physical.bindCancellationCompletion(b) + emission.bindCoroPhysicalBody(physical, sourceBlocks) + b.SetBlock(physical.coro.InitialResumeBlock()) + physical.activate(b) + b.Jump(sourceBlocks[0]) + + off := make([]int, len(fn.Blocks)) + for i, block := range fn.Blocks { + off[i] = p.compilePhis(b, block) + } + p.blkInfos = blocks.Infos(fn.Blocks) + plan, ok := p.compilation.CoroPlan.FunctionPlan(fn) + if !ok { + panic("coroutine physical body has no compilation plan") + } + physical.needsPreempt = plan.Exec.Contains(coro.NeedsPreempt) + + i := 0 + for { + block := fn.Blocks[i] + physical.sourceBlockPollFresh = false + entryDepth := uint32(0) + if physical.critical != nil { + var proven bool + entryDepth, proven = physical.critical.entryDepth[block] + if !proven { + panic("coroutine critical proof has no source-block entry depth") + } + } + if physical.needsPreempt && entryDepth == 0 { + physical.instructions = 0 + // Every source block, including block zero, begins with a poll. A + // child initial suspend is a scheduler boundary but not necessarily + // a fairness boundary: pendingAwait can immediately resume a long + // static child chain on the same G without returning to ready-queue + // selection. Polling block zero therefore bounds that chain as well + // as ordinary CFG paths and block-zero backedges. + b.SetBlock(p.sourceBlock(i)) + physical.pollAndSuspendForPreempt(b) + physical.sourceBlockPollFresh = true + } + doModInit := i == 1 && isInit + p.compileBlock(b, block, off[i], doModInit) + if i = p.blkInfos[i].Next; i < 0 { + break + } + } + for _, phi := range p.phis { + phi() + } + b.SetBlock(physical.completion) + if physical.cleanup == nil { + physical.complete(b) + } else { + physical.cleanup.enterCompletion(b) + physical.cleanup.emit(p, b) + b.SetBlock(physical.cleanup.complete) + physical.complete(b) + b.SetBlock(physical.cleanup.panic) + physical.panic( + b, + b.Load(physical.cleanup.panicType), + b.Load(physical.cleanup.panicData), + ) + } + b.SetBlock(physical.finalSuspend) + physical.finish(b) + emission.completeCoroPhysicalBody(physical) +} + +func validateCoroPhysicalABI(fn *ssa.Function, plan coro.FunctionPlan, whole *coro.SSAPlan, childAwait, programRun bool) error { + return validateCoroPhysicalABIWithUniverseCapabilities(fn, plan, whole, nil, childAwait, programRun, false, false) +} + +// validateCoroPhysicalABIWithUniverse is the production preflight. The +// prepared emission universe supplies the exact frontend lowering context used +// to prove that an accepted pure SSA instruction emits no hidden runtime call. +// The wrapper above is retained for narrow structural unit tests; active +// Compilation paths always call this form with their frozen universe. +func validateCoroPhysicalABIWithUniverse(fn *ssa.Function, plan coro.FunctionPlan, whole *coro.SSAPlan, universe *EmissionUniverse, childAwait, programRun bool) error { + return validateCoroPhysicalABIWithUniverseCapabilities(fn, plan, whole, universe, childAwait, programRun, false, false) +} + +func validateCoroPhysicalABIWithUniverseCapabilities(fn *ssa.Function, plan coro.FunctionPlan, whole *coro.SSAPlan, universe *EmissionUniverse, childAwait, programRun, staticSpawn, explicitPanic bool) error { + return validateCoroPhysicalABIWithUniverseCapabilitiesAndFrameRetention( + fn, plan, whole, universe, childAwait, programRun, staticSpawn, explicitPanic, "", + ) +} + +func validateCoroPhysicalABIWithUniverseCapabilitiesAndFrameRetention(fn *ssa.Function, plan coro.FunctionPlan, whole *coro.SSAPlan, universe *EmissionUniverse, childAwait, programRun, staticSpawn, explicitPanic bool, frameRetentionABI string) error { + return validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + fn, plan, whole, universe, childAwait, programRun, staticSpawn, explicitPanic, frameRetentionABI, false, false, false, + ) +} + +func validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel(fn *ssa.Function, plan coro.FunctionPlan, whole *coro.SSAPlan, universe *EmissionUniverse, childAwait, programRun, staticSpawn, explicitPanic bool, frameRetentionABI string, channel, managedDispatch, rawMethodToken bool) error { + return validateCoroPhysicalABIForOwner( + fn, plan, whole, universe, nil, childAwait, programRun, staticSpawn, explicitPanic, + frameRetentionABI, channel, managedDispatch, rawMethodToken, nil, nil, nil, + ) +} + +func validateCoroPhysicalABIForOwner( + fn *ssa.Function, + plan coro.FunctionPlan, + whole *coro.SSAPlan, + universe *EmissionUniverse, + owner *preparedEmissionPackage, + childAwait, programRun, staticSpawn, explicitPanic bool, + frameRetentionABI string, + channel, managedDispatch, rawMethodToken bool, + interfacePlain *coroClosedInterfacePlainPlan, + managedInterface *coroManagedInterfaceDispatchPlan, + accept func(*coroPhysicalFunctionPlan) error, +) error { + if !childAwait { + if explicitPanic { + return fmt.Errorf("coroutine physical ABI: function %q: explicit-status panic requires PhysicalABIV1 child-await lowering", plan.ID) + } + if err := validateCoroLeafPhysicalABI(fn, plan); err != nil { + return err + } + if accept == nil { + return nil + } + audit, err := newCoroPhysicalPureSSAAuditForOwner(universe, whole, fn, owner, frameRetentionABI) + if err != nil { + return fmt.Errorf("coroutine physical ABI: function %q: cannot freeze leaf physical proof: %w", plan.ID, err) + } + critical, err := proveCoroCriticalRegions(universe, whole, audit) + if err != nil { + return fmt.Errorf("coroutine physical ABI: function %q: leaf critical region: %w", plan.ID, err) + } + physical, err := prepareCoroPhysicalFunctionPlan( + audit, owner, whole, nil, critical, false, coroPhysicalLoweringCapabilities{}, + ) + if err != nil { + return fmt.Errorf("coroutine physical ABI: function %q: leaf physical plan: %w", plan.ID, err) + } + return accept(physical) + } + + fail := func(format string, args ...any) error { + name := "" + if fn != nil { + name = fn.String() + } + return fmt.Errorf("coroutine physical ABI: function %q (%s): %s", plan.ID, name, fmt.Sprintf(format, args...)) + } + if fn == nil || plan.External != coro.Defined || len(fn.Blocks) == 0 { + return fail("requires one defined SSA body") + } + if emitShadowStackInstrumentation { + return fail("legacy thread-local shadow-stack instrumentation is incompatible with stackless coroutine suspension") + } + managedDispatchTarget := managedDispatch && plan.FuncRep == coro.Dispatch && + fn.Signature != nil && fn.Signature.Recv() == nil + rawMethodDispatchToken := rawMethodToken && plan.FuncRep == coro.Dispatch && + fn.Signature != nil && fn.Signature.Recv() != nil + if plan.Emission != coro.EmitCoroutine || + plan.FuncRep != coro.DirectCoro && !managedDispatchTarget && !rawMethodDispatchToken { + return fail("requires a direct coroutine or capability-certified Dispatch emission, got emission=%s representation=%s", plan.Emission, plan.FuncRep) + } + if !plan.ManagedDemand.Contains(coro.AsyncDemand) { + return fail( + "requires managed async demand, got aggregate=%s managed=%s raw=%t raw-entry=%t", + plan.Demand, plan.ManagedDemand, plan.RawPlainDemand, plan.RawPlainEntry, + ) + } + rawVariant := whole != nil && whole.HasRawPlainVariant(fn) + // A recursive edge uses the same structured child-frame transaction as any + // other exact coroutine call. Recursive SCCs also carry NeedsPreempt, whose + // runnable-scheduler gate below guarantees bounded execution between polls. + // PhysicalABIV0 remains leaf-only and retains its separate rejection. + cleanupPlan, cleanupErr := prepareCoroStaticCleanupPlan( + fn, whole, universe, frameRetentionABI, explicitPanic, + ) + if cleanupErr != nil { + return fail("static cleanup: %v", cleanupErr) + } + if err := validateCoroDynamicCleanupHelpers(cleanupPlan, whole); err != nil { + return fail("dynamic cleanup: %v", err) + } + if plan.Exec.Contains(coro.NeedsPreempt) && !programRun { + return fail("needs-preempt execution requires the runnable scheduler ABI") + } + // IRQUnsafe constrains interrupt roots; an ordinary scheduler-managed G is + // not an IRQ context. Preserve the bit in the plan/digest while allowing the + // CFG lowering to execute it. Thread affinity and opaque execution still + // require scheduler protocols that this ABI does not provide. + allowedExec := coro.MayUnwind | coro.NeedsPreempt | coro.IRQUnsafe + if cleanupPlan != nil { + allowedExec |= coro.NeedsCleanupFrame + } + if unsupported := plan.Exec &^ allowedExec; unsupported != 0 { + return fail("execution flags %s require lowering outside the CFG physical ABI", unsupported) + } + if len(fn.FreeVars) != 0 && !managedDispatchTarget && plan.FuncRep != coro.DirectCoro { + return fail("captured coroutine bodies require one exact direct or capability-certified descriptor context ABI") + } + if fn.Recover != nil && cleanupPlan == nil { + return fail("recover blocks require coroutine cleanup/unwind lowering") + } + if cleanupPlan != nil { + if err := validateCoroStaticCleanupRecoverBlock(fn); err != nil { + return fail("static cleanup recover block: %v", err) + } + } + directive, directiveErr := coroRawABIDirective(fn, universe) + if directiveErr != nil { + return fail("classify ABI directive: %v", directiveErr) + } + if directive != "" && !(plan.RawPlainEntry && rawVariant) { + return fail("ABI directive %q requires a root or foreign adapter", directive) + } + if isCgoExternSymbol(fn) { + return fail("cgo entry requires a foreign adapter") + } + programEntry := programRun && isCoroProgramManagedEntry(fn) + genericInstance := coroMaterializedGenericCallable(fn) + boundMethodWrapper := false + if managedDispatchTarget && strings.HasPrefix(fn.Synthetic, "bound method wrapper for ") { + if err := validateCoroExactBoundMethodWrapper(fn); err != nil { + return fail("invalid bound method wrapper: %v", err) + } + boundMethodWrapper = true + } + methodExpressionThunk := false + if strings.HasPrefix(fn.Synthetic, "thunk for ") { + if err := validateCoroExactMethodExpressionThunk(fn); err != nil { + return fail("invalid method-expression thunk: %v", err) + } + methodExpressionThunk = true + } + methodTokenWrapper := rawMethodToken && fn.Signature != nil && fn.Signature.Recv() != nil && + strings.Contains(fn.Synthetic, "wrapper for") + capturedRawVariant := rawVariant && len(fn.FreeVars) != 0 + if fn.Synthetic != "" && !genericInstance && !boundMethodWrapper && !methodExpressionThunk && !methodTokenWrapper && + !capturedRawVariant && + !(programEntry && fn.Name() == "init" && fn.Synthetic == "package initializer") { + return fail("synthetic function %q is outside the leaf ABI", fn.Synthetic) + } + if list := fn.TypeParams(); list != nil && list.Len() != 0 && !genericInstance { + return fail("generic declarations are not materialized coroutine bodies") + } + if list := fn.Signature.RecvTypeParams(); list != nil && list.Len() != 0 && !genericInstance { + return fail("generic receivers are not materialized coroutine bodies") + } + if list := fn.TypeArgs(); len(list) != 0 && !genericInstance { + return fail("generic instances require a frozen instantiated ABI") + } + if isCoroProgramManagedEntry(fn) && !programEntry { + return fail("program roots require scheduler bootstrap lowering") + } + physicalSourceSig := coroPhysicalNormalizeSourceSignature(fn.Signature) + if universe != nil { + var signatureErr error + physicalSourceSig, signatureErr = universe.coroPhysicalEntrySourceSignature(fn) + if signatureErr != nil { + return fail("derive effective source signature: %v", signatureErr) + } + } + if err := validateCoroPhysicalSSAParameterShape(plan, fn, physicalSourceSig, universe); err != nil { + return err + } + if err := validateCoroLeafPhysicalSignature(plan, physicalSourceSig); err != nil { + return err + } + pureSSA, err := newCoroPhysicalPureSSAAuditForOwner(universe, whole, fn, owner, frameRetentionABI) + if err != nil { + return fail("cannot audit pure SSA lowering: %v", err) + } + // Nullable FieldAddr values are accepted only under the target-wide + // explicit-status identity. Codegen then replaces the host signal/legacy + // AssertNilDeref behavior with a compiler-owned terminal coroutine edge. + pureSSA.allowImplicitNilFault = explicitPanic + pureSSA.allowExplicitRecover = explicitPanic + terminalResultAllocations := []*ssa.Alloc(nil) + if cleanupPlan != nil { + terminalResultAllocations = cleanupPlan.terminalResultAllocations + } + if !coroTerminalResultAllocationSetMatches(pureSSA.currentFrameRetentionProof(), terminalResultAllocations) { + return fail("static cleanup and frame-retention proofs disagree on terminal-result allocations") + } + critical, criticalErr := proveCoroCriticalRegions(universe, whole, pureSSA) + if criticalErr != nil { + return fail("critical region: %v", criticalErr) + } + if critical != nil && !programRun { + return fail("critical regions require the runnable scheduler ABI") + } + physical, physicalErr := prepareCoroPhysicalFunctionPlan( + pureSSA, owner, whole, cleanupPlan, critical, explicitPanic, + coroPhysicalLoweringCapabilities{ + childAwait: childAwait, + staticSpawn: staticSpawn, + managedDispatch: managedDispatch, + explicitPanic: explicitPanic, + channel: channel, + worker: universe != nil && universe.CoroWorkerEnabled(), + interfacePlain: interfacePlain, + managedInterface: managedInterface, + }, + ) + if physicalErr != nil { + return fail("cannot freeze physical instruction plan: %v", physicalErr) + } + + panics := 0 + awaits := 0 + parks := 0 + foreignWaits := 0 + yields := 0 + spawns := 0 + if cleanupPlan != nil { + for _, site := range cleanupPlan.sites { + switch site.kind { + case coroStaticCleanupCoroutine: + awaits++ + case coroStaticCleanupDispatch: + if !managedDispatch { + return fail("managed descriptor defer requires the v1 descriptor dispatch capability") + } + if site.callPlan.Open || coroDispatchCallHasCoroutineTarget(whole, site.callPlan) { + awaits++ + } + } + } + } + infos := blocks.Infos(fn.Blocks) + hasCyclicBlock := false + for _, info := range infos { + hasCyclicBlock = hasCyclicBlock || info.InLoop + } + // A RawPlainVariant with a cyclic body can lack NeedsPreempt only when the + // frontend's exact compiler-runtime island policy suppressed the scanner + // seed. Its raw execution is an intentionally atomic/bounded scheduler + // transaction; ordinary source callbacks are not given that policy and keep + // NeedsPreempt. The managed primary otherwise requires normal poll lowering. + if hasCyclicBlock && !plan.Exec.Contains(coro.NeedsPreempt) && !rawVariant { + return fail("cyclic CFG requires needs-preempt execution classification") + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + instructionPlan, frozen := physical.instructions[instr] + if !frozen { + return coroLeafInstructionError(fn, plan, instr, "instruction is absent from the frozen physical plan") + } + if instructionPlan.outcomeFailure != "" { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.outcomeFailure) + } + if instructionPlan.operationFailure != "" { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.operationFailure) + } + if instructionPlan.recipe == coroPhysicalInstructionSyntheticSelectNoCaseBox || + instructionPlan.outcome == coroPhysicalOutcomeSyntheticSelectTrap { + continue + } + if handled, reason := pureSSA.validate(instr); handled { + if reason != "" { + return coroLeafInstructionError(fn, plan, instr, reason) + } + if instructionPlan.mayFault() { + panics++ + } + if call, ok := instr.(*ssa.Call); ok && isCoroCloseBuiltinCall(call) { + instructionPlan := physical.instructions[instr] + if instructionPlan.operation != coroPhysicalOperationChannelClose { + if instructionPlan.operationFailure != "" { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.operationFailure) + } + return coroLeafInstructionError(fn, plan, instr, "channel close has no frozen operation recipe") + } + panics++ + } + if call, ok := instr.(*ssa.Call); ok && isCoroRecoverBuiltinCall(call) && + instructionPlan.outcome != coroPhysicalOutcomeRecover { + return coroLeafInstructionError(fn, plan, instr, "recover builtin has no frozen outcome recipe") + } + continue + } + switch instr := instr.(type) { + case *ssa.DebugRef, *ssa.Jump: + case *ssa.Return: + if instructionPlan.outcome != coroPhysicalOutcomeReturn { + return coroLeafInstructionError(fn, plan, instr, "return has no frozen outcome recipe") + } + case *ssa.Defer, *ssa.RunDefers: + want := coroPhysicalOutcomeDeferRegister + if _, run := instr.(*ssa.RunDefers); run { + want = coroPhysicalOutcomeRunDefers + } + if instructionPlan.outcome != want { + return coroLeafInstructionError(fn, plan, instr, "defer instruction has no frozen cleanup outcome recipe") + } + case *ssa.Panic: + if instructionPlan.outcome != coroPhysicalOutcomePanic { + return coroLeafInstructionError(fn, plan, instr, "panic has no frozen outcome recipe") + } + panics++ + case *ssa.If: + if !coroLeafScalar(instr.Cond.Type()) { + return coroLeafInstructionError(fn, plan, instr, "non-scalar branch condition") + } + case *ssa.BinOp: + if instr.Op == token.QUO || instr.Op == token.REM || instr.Op == token.SHL || instr.Op == token.SHR || + !coroLeafScalar(instr.Type()) || + !coroLeafScalar(instr.X.Type()) || !coroLeafScalar(instr.Y.Type()) { + return coroLeafInstructionError(fn, plan, instr, "potentially panicking or non-scalar binary operation") + } + case *ssa.Send: + instructionPlan, frozen := physical.instructions[instr] + if !frozen || instructionPlan.operation != coroPhysicalOperationChannelSend { + if frozen && instructionPlan.operationFailure != "" { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.operationFailure) + } + return coroLeafInstructionError(fn, plan, instr, "channel send has no frozen operation recipe") + } + parks++ + case *ssa.Select: + instructionPlan, frozen := physical.instructions[instr] + if !frozen || instructionPlan.operation != coroPhysicalOperationChannelSelectPark && + instructionPlan.operation != coroPhysicalOperationChannelSelectTry { + if frozen && instructionPlan.operationFailure != "" { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.operationFailure) + } + return coroLeafInstructionError(fn, plan, instr, "channel select has no frozen operation recipe") + } + if instructionPlan.operation == coroPhysicalOperationChannelSelectPark { + parks++ + } + case *ssa.UnOp: + if instr.Op == token.ARROW { + instructionPlan, frozen := physical.instructions[instr] + if !frozen || instructionPlan.operation != coroPhysicalOperationChannelReceive { + if frozen && instructionPlan.operationFailure != "" { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.operationFailure) + } + return coroLeafInstructionError(fn, plan, instr, "channel receive has no frozen operation recipe") + } + parks++ + continue + } + if (instr.Op != token.SUB && instr.Op != token.XOR && instr.Op != token.NOT) || !coroLeafScalar(instr.Type()) { + return coroLeafInstructionError(fn, plan, instr, "unsupported unary operation") + } + case *ssa.Call: + instructionPlan, frozen := physical.instructions[instr] + if !frozen { + return coroLeafInstructionError(fn, plan, instr, "instruction is absent from the frozen physical plan") + } + if whole != nil && whole.ElidesCall(instr) { + if universe != nil { + frozen, found, err := universe.coroProgramIR.callSitePlan(instr) + if err != nil || !found { + if err == nil { + err = fmt.Errorf("call is absent from the frozen ProgramIR") + } + return coroLeafInstructionError(fn, plan, instr, err.Error()) + } + if frozen.failure != "" { + return coroLeafInstructionError(fn, plan, instr, "invalid frozen intrinsic: "+frozen.failure) + } + callPlan := frozen.plan + semantics, intrinsic := callPlan.IntrinsicSemantics, callPlan.Intrinsic + if cleanupPlan != nil && callPlan.Elision != CoroCallElidedNoInit && (!intrinsic || + (semantics != CoroIntrinsicCallInlineNoSuspend && semantics != CoroIntrinsicCallInlineSuspend && + semantics != CoroIntrinsicCallInlineYield)) { + return coroLeafInstructionError(fn, plan, instr, "elided intrinsic has no cleanup-safe no-unwind contract") + } + if intrinsic && semantics == CoroIntrinsicCallInlineSuspend { + if isLLGoSyscallIntrinsic(frozen.opcode) { + if instructionPlan.operation != coroPhysicalOperationWorkerSyscall { + return coroLeafInstructionError(fn, plan, instr, "worker llgo.syscall has no frozen operation recipe") + } + } + parks++ + } else if intrinsic && semantics == CoroIntrinsicCallInlineYield { + yields++ + } + if intrinsic { + if isLLGoSyscallIntrinsic(frozen.opcode) && semantics != CoroIntrinsicCallInlineSuspend { + return coroLeafInstructionError(fn, plan, instr, + "elided worker llgo.syscall has no frozen function-word capability") + } + if frozen.opcode == llgoAlloca { + return coroLeafInstructionError(fn, plan, instr, + "dynamic llgo.alloca is valid only in a no-suspend plain island; a physical coroutine requires an exact resume-local lifetime proof") + } + } + } + // The frozen frontend proved that this declaration call emits no + // callable edge. A structured park is counted above; ordinary + // noinit/inline intrinsics need no await/plain entry. + continue + } + if instr.Common().IsInvoke() { + switch instructionPlan.control { + case coroPhysicalControlClosedInterfaceAwait, coroPhysicalControlManagedInterfaceAwait: + awaits++ + continue + case coroPhysicalControlNone: + if instructionPlan.controlFailureHard { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.controlFailure) + } + continue + default: + return coroLeafInstructionError(fn, plan, instr, + "interface invoke has mismatched frozen control recipe "+instructionPlan.control.String()) + } + } + switch instructionPlan.control { + case coroPhysicalControlPlainDispatch: + continue + case coroPhysicalControlDispatchAwait: + awaits++ + continue + default: + if instructionPlan.controlFailureHard { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.controlFailure) + } + } + if instructionPlan.operation == coroPhysicalOperationWorkerForeign { + if instructionPlan.operationWorker == nil { + return coroLeafInstructionError(fn, plan, instr, "bounded worker call has no frozen physical shape") + } + foreignWaits++ + continue + } + if instructionPlan.control == coroPhysicalControlDirectAwait { + awaits++ + continue + } + if instructionPlan.controlFailureHard { + return coroLeafInstructionError(fn, plan, instr, instructionPlan.controlFailure) + } + if explicitPanic { + if _, targetPlan, plainErr := resolveCoroStaticPlainCall(whole, instr); plainErr == nil { + // A direct plain call needs no hidden outcome slot when the + // whole-program SSA plan has proved that its exact target cannot + // initiate a Go unwind. resolveCoroStaticPlainCall already proves + // that the call is closed, non-suspending, and has one exact plain + // entry. Keep MayUnwind fail-closed: a merely synchronous function + // may still panic and therefore must use the managed explicit-status + // ABI rather than silently unwinding through this coroutine frame. + if !targetPlan.Exec.Contains(coro.MayUnwind) { + continue + } + return coroLeafInstructionError(fn, plan, instr, fmt.Sprintf( + "direct plain target %q (exec=%s) has no certified explicit-status hidden-outcome/unwind contract", + targetPlan.ID, targetPlan.Exec, + )) + } + } + if !programRun { + return coroLeafInstructionError(fn, plan, instr, "unsupported child await: "+instructionPlan.controlFailure) + } + if _, _, plainErr := resolveCoroStaticPlainCall(whole, instr); plainErr != nil { + return coroLeafInstructionError(fn, plan, instr, "unsupported call: child await: "+instructionPlan.controlFailure+"; direct plain: "+plainErr.Error()) + } + case *ssa.Go: + instructionPlan, frozen := physical.instructions[instr] + if !frozen { + return coroLeafInstructionError(fn, plan, instr, "instruction is absent from the frozen physical plan") + } + switch instructionPlan.control { + case coroPhysicalControlDirectSpawn, coroPhysicalControlDispatchSpawn: + case coroPhysicalControlNone: + return coroLeafInstructionError(fn, plan, instr, instructionPlan.controlFailure) + default: + return coroLeafInstructionError(fn, plan, instr, "goroutine spawn has mismatched frozen control recipe "+instructionPlan.control.String()) + } + spawns++ + default: + return coroLeafInstructionError(fn, plan, instr, "instruction is outside the CFG physical ABI allowlist") + } + } + } + // A Go function may deliberately never return (for example select{} or an + // infinite scheduler-polled loop). Cancellation still reaches the compiler- + // owned completion block, so a source Return is not an ABI prerequisite. + if panics != 0 && !plan.Exec.Contains(coro.MayUnwind) { + return fail("explicit panic body lacks may-unwind execution classification: %s", plan.Exec) + } + if !plan.Effect.MaySuspend() { + return fail("CFG physical body lacks a suspension-capable final effect: %s", plan.Effect) + } + if awaits != 0 && !plan.Effect.Contains(coro.AwaitStructured) { + return fail("child-await body lacks await-structured final effect: %s", plan.Effect) + } + if parks != 0 && !plan.Effect.Contains(coro.MayPark) { + return fail("structured-park body lacks may-park final effect: %s", plan.Effect) + } + if foreignWaits != 0 && !plan.Effect.Contains(coro.WaitForeign) { + return fail("bounded worker body lacks wait-foreign final effect: %s", plan.Effect) + } + if yields != 0 && (!plan.DeclaredEffect.Contains(coro.YieldOnly) || !plan.LocalEffect.Contains(coro.YieldOnly) || !plan.Effect.Contains(coro.YieldOnly)) { + return fail("structured-yield body lacks yield-only owner effect: declared=%s local=%s final=%s", plan.DeclaredEffect, plan.LocalEffect, plan.Effect) + } + if spawns != 0 && (!plan.DeclaredEffect.Contains(coro.YieldOnly) || !plan.LocalEffect.Contains(coro.YieldOnly) || !plan.Effect.Contains(coro.YieldOnly)) { + return fail("coroutine spawn body lacks its exact yield-only owner seed: declared=%s local=%s final=%s", plan.DeclaredEffect, plan.LocalEffect, plan.Effect) + } + if plan.DeclaredEffect.Contains(coro.MayPark) && parks == 0 { + return fail("declared may-park effect has no exact structured park intrinsic") + } + // WaitForeign may be inherited from a structured child. An ordinary local + // foreign edge was counted and shape-checked above; the effect bit alone can + // never authorize a raw foreign call on this scheduler thread. + if unsupported := plan.Effect &^ (coro.YieldOnly | coro.AwaitStructured | coro.OutcomeStructured | coro.MayPark | coro.WaitForeign); unsupported != 0 { + return fail("child-await body has unsupported final effect %s", unsupported) + } + if unsupported := plan.DeclaredEffect &^ (coro.YieldOnly | coro.MayPark); unsupported != 0 { + return fail("child-await body has unsupported declared effect %s", unsupported) + } + if unsupported := plan.LocalEffect &^ (coro.YieldOnly | coro.AwaitStructured | coro.OutcomeStructured | coro.MayPark); unsupported != 0 { + return fail("child-await body has unsupported local effect %s", unsupported) + } + if accept != nil { + if err := accept(physical); err != nil { + return fail("freeze physical plan: %v", err) + } + } + return nil +} + +func validateCoroPhysicalChannelType(typ types.Type) error { + channel, ok := types.Unalias(typ).Underlying().(*types.Chan) + if !ok { + return fmt.Errorf("operand is not a channel") + } + if err := validateCoroPhysicalValueType(channel.Elem(), make(map[types.Type]bool)); err != nil { + return fmt.Errorf("element type: %w", err) + } + return nil +} + +func validateCoroExplicitStatusPanic(audit *coroPhysicalPureSSAAudit, instruction *ssa.Panic) string { + if instruction == nil || instruction.X == nil { + return "explicit-status panic requires a non-nil operand" + } + if audit == nil { + return "explicit-status panic requires a prepared pure-SSA audit" + } + boxed, ok := instruction.X.(*ssa.MakeInterface) + if !ok { + target, interfaceValue := types.Unalias(audit.typeOf(instruction.X.Type())).Underlying().(*types.Interface) + if !interfaceValue || !target.Empty() { + return "explicit-status panic requires one empty-interface operand" + } + if reason := validateCoroExplicitStatusPanicInterfaceValue(audit, instruction.X, make(map[ssa.Value]bool)); reason != "" { + return "explicit-status panic interface payload is not frame-stable: " + reason + } + return "" + } + if boxed.X == nil { + return "explicit-status panic requires a complete concrete MakeInterface operand" + } + if boxed.Parent() != instruction.Parent() { + return "explicit-status panic MakeInterface belongs to a different SSA body" + } + refs := boxed.Referrers() + if refs == nil || len(*refs) != 1 || (*refs)[0] != instruction { + return "explicit-status panic requires its MakeInterface to have the panic site as its sole consumer" + } + target, ok := types.Unalias(boxed.Type()).Underlying().(*types.Interface) + if !ok || !target.Empty() { + return "explicit-status panic requires an empty-interface MakeInterface result" + } + if isUntypedNilConst(boxed.X) { + return "explicit-status panic does not yet support an untyped nil value" + } + source := boxed.X.Type() + if audit != nil { + source = audit.typeOf(source) + } + if source == nil { + return "explicit-status panic MakeInterface has no concrete source type" + } + if reason := audit.validateMakeInterface(boxed); reason != "" { + return "explicit-status panic MakeInterface has no outcome-safe lowering: " + reason + } + // Non-direct interface representations live in the managed backing cell + // created by the exact MakeInterface helper. Under ExplicitStatus that helper + // is an awaited coroutine child, so the allocation has completed before its + // stable data word is published to the parent CompletionRecord. + if !emissionDirectIfaceType(source) { + return "" + } + if constant, ok := boxed.X.(*ssa.Const); ok && constant.Value == nil { + // A typed nil pointer still produces a non-nil interface type word and + // carries no frame-owned storage in its data word. + return "" + } + switch types.Unalias(source).Underlying().(type) { + case *types.Map, *types.Chan: + // These direct interface words identify managed heap objects; publishing + // the word itself retains the object independently of the child frame. + return "" + case *types.Pointer: + // A frozen AllocZ result is a real managed-heap object, not storage + // owned by the LLVM frame. The scheduler publishes this exact data word + // into its parent CompletionRecord (or root PanicRecord) before destroying + // the frame, so the non-moving-conservative/no-GC root profile retains it + // just like a package-global pointer. + root, reason := audit.stableAddress(boxed.X, make(map[ssa.Value]bool)) + if reason != "" || root != coroPhysicalAddressGlobal && root != coroPhysicalAddressManagedHeap { + if reason == "" { + reason = "payload is not rooted in package-global or managed-heap storage" + } + return "explicit-status panic data word may outlive its coroutine frame: " + reason + } + return "" + default: + // unsafe.Pointer, direct one-field wrappers, and function values can + // borrow frame-owned storage. They need a dedicated payload-lifetime + // certificate before the child may be destroyed. + return "explicit-status direct panic payload has no post-destroy lifetime proof" + } +} + +// validateCoroExplicitStatusPanicInterfaceValue accepts an already-built +// interface only when its two words can be copied without borrowing storage +// that disappears before publication. For a typed load, the address therefore +// needs to be stable only through the load itself: an interface value is the +// type/data pair, and neither word points back at the interface cell. The pair +// is copied immediately into parent-owned completion storage, whose current +// nonmoving-conservative-or-none profile retains the dynamic data word after +// the child frame is destroyed. Parameters and arbitrary dynamic producers +// still stay fail-closed until they carry their own lifetime certificate. +func validateCoroExplicitStatusPanicInterfaceValue( + audit *coroPhysicalPureSSAAudit, + value ssa.Value, + visiting map[ssa.Value]bool, +) string { + if audit == nil || value == nil { + return "missing pure-SSA audit or interface value" + } + if visiting[value] { + return "cyclic interface value" + } + visiting[value] = true + defer delete(visiting, value) + if instruction, ok := value.(ssa.Instruction); ok && instruction.Parent() != audit.fn { + return "interface producer belongs to a different SSA body" + } + interfaceType, ok := types.Unalias(audit.typeOf(value.Type())).Underlying().(*types.Interface) + if !ok { + return "value is not an interface" + } + interfaceType.Complete() + switch value := value.(type) { + case *ssa.ChangeInterface: + if reason := audit.validateChangeInterface(value); reason != "" { + return "interface conversion has no outcome-safe lowering: " + reason + } + return validateCoroExplicitStatusPanicInterfaceValue(audit, value.X, visiting) + case *ssa.ChangeType: + if reason := audit.validateChangeType(value); reason != "" { + return "interface change-type has no pure lowering: " + reason + } + return validateCoroExplicitStatusPanicInterfaceValue(audit, value.X, visiting) + case *ssa.Phi: + if len(value.Edges) == 0 { + return "interface phi has no incoming values" + } + for _, edge := range value.Edges { + if reason := validateCoroExplicitStatusPanicInterfaceValue(audit, edge, visiting); reason != "" { + return "interface phi input: " + reason + } + } + return "" + case *ssa.UnOp: + if value.Op != token.MUL { + return "interface producer is not a typed load" + } + if reason := audit.validateUnOp(value); reason != "" { + return "interface load has no pure lowering: " + reason + } + root, reason := audit.stableAddressAt(value.X, value, make(map[ssa.Value]bool)) + if reason != "" { + return "interface load address: " + reason + } + if root == coroPhysicalAddressInvalid { + return "interface load address has no stable root" + } + return "" + case *ssa.Call: + if builtin, ok := value.Call.Value.(*ssa.Builtin); ok && builtin.Name() == "recover" { + if reason := audit.validateBuiltin(value); reason != "" { + return "recover result has no explicit-status lowering: " + reason + } + // The direct deferred-child hook copied these words from the + // parent-owned CompletionRecord, which retains them until this child has + // published its terminal CompletionRecord and been destroyed. A + // repanic therefore transfers the same stable pair without borrowing + // child-frame storage. + return "" + } + if audit.plan == nil || audit.fn == nil { + return "interface call result requires a whole-program call plan" + } + callerPlan, planned := audit.plan.FunctionPlan(audit.fn) + if !planned { + return "interface call result owner has no function plan" + } + if _, _, err := resolveCoroStaticAwait(audit.plan, callerPlan, value, audit.universe); err == nil { + // The child writes its Go result into parent-owned result storage before + // the parent resumes and destroys the child. Go escape semantics keep + // any backing cell referenced by the returned interface alive; copying + // the two words into the panic completion record is therefore stable. + return "" + } + if _, targetPlan, err := resolveCoroStaticPlainCall(audit.plan, value); err == nil && + targetPlan.External == coro.Defined && !targetPlan.Exec.Contains(coro.MayUnwind) { + // A bounded owned Go callee has completed normally on the same stack; + // its returned interface obeys the same language-level escape lifetime. + return "" + } + return "interface call result is not one exact managed child or non-unwinding owned plain call" + default: + return fmt.Sprintf("interface producer %T has no post-destroy lifetime proof", value) + } +} + +func isCoroProgramManagedEntry(fn *ssa.Function) bool { + if fn == nil { + return false + } + name := fn.Name() + if name == "init" || strings.HasPrefix(name, "init#") { + return true + } + return name == "main" && fn.Pkg != nil && fn.Pkg.Pkg != nil && fn.Pkg.Pkg.Name() == "main" +} + +func coroMaterializedGenericInstance(fn *ssa.Function) bool { + if fn == nil || fn.Origin() == nil || fn.Origin() == fn || len(fn.TypeArgs()) == 0 || + !hasGenericInstantiation(fn) || !coroGroundGenericTypeArgs(fn.TypeArgs()) { + return false + } + if parent := fn.Parent(); parent != nil { + // x/tools materializes a function literal inside each instantiated + // generic body. The child keeps the origin's TypeParams metadata, but its + // signature, parameters, free variables, and TypeArgs are concrete. Bind + // the exception to that exact parent/Origin/AnonFuncs graph; an arbitrary + // nested synthetic function cannot acquire a dispatch ABI merely by + // carrying TypeArgs. + if !coroMaterializedGenericInstance(parent) || fn.Synthetic != "" || fn.Object() != nil { + return false + } + if _, ok := fn.Syntax().(*ast.FuncLit); !ok { + return false + } + originParent := fn.Origin().Parent() + if originParent == nil || originParent != parent.Origin() { + return false + } + found := false + for _, child := range parent.AnonFuncs { + if child == fn { + if found { + return false + } + found = true + } + } + if !found || len(fn.TypeArgs()) != len(parent.TypeArgs()) { + return false + } + for index, argument := range fn.TypeArgs() { + if !types.Identical(argument, parent.TypeArgs()[index]) { + return false + } + } + } else if !strings.HasPrefix(fn.Synthetic, "instance of ") { + return false + } + // x/tools erases ordinary declaration type parameters from an instantiated + // callable signature. For an instantiated generic receiver method, and for + // its parentless method body only, it keeps the origin's RecvTypeParams + // metadata even though Recv and every physical parameter/result are already + // concrete. Judge materialization from that callable value shape, not from + // the stale declaration metadata alone. + if params := fn.Signature.TypeParams(); params != nil && params.Len() != 0 { + return false + } + if params := fn.Signature.RecvTypeParams(); params != nil && params.Len() != 0 && + (fn.Parent() != nil || fn.Signature.Recv() == nil) { + return false + } + normalized := coroPhysicalNormalizeSourceSignature(fn.Signature) + if normalized == nil || normalized.Params().Len() != len(fn.Params) { + return false + } + for index, parameter := range fn.Params { + if parameter == nil || !types.Identical(parameter.Type(), normalized.Params().At(index).Type()) { + return false + } + } + for _, tuple := range []*types.Tuple{normalized.Params(), normalized.Results()} { + for index := 0; index < tuple.Len(); index++ { + if validateCoroPhysicalValueType(tuple.At(index).Type(), make(map[types.Type]bool)) != nil { + return false + } + } + } + for _, free := range fn.FreeVars { + if free == nil || coroTypeContainsUnresolvedTypeParam(free.Type(), make(map[types.Type]bool)) || + validateCoroPhysicalValueType(free.Type(), make(map[types.Type]bool)) != nil { + return false + } + } + return true +} + +// coroMaterializedGenericCallable includes the one Pkg-nil method-set wrapper +// shape that x/tools synthesizes when a pointer invokes an instantiated generic +// value-receiver method. Such a wrapper has no Origin or TypeArgs of its own, +// but its receiver, SSA parameters, and sole callee are fully concrete. Keep +// this separate from ordinary generic instances so arbitrary synthetic bodies +// cannot acquire a physical ABI from stale RecvTypeParams metadata. +func coroMaterializedGenericCallable(fn *ssa.Function) bool { + return coroMaterializedGenericInstance(fn) || coroMaterializedGenericMethodWrapper(fn) +} + +func coroMaterializedGenericMethodWrapper(fn *ssa.Function) bool { + if fn == nil || fn.Pkg != nil || fn.Parent() != nil || len(fn.FreeVars) != 0 || + fn.Signature == nil || fn.Signature.Recv() == nil || + !strings.HasPrefix(fn.Synthetic, "wrapper for ") || !hasGenericInstantiation(fn) || + typeParamCount(fn.Signature.TypeParams()) != 0 || + typeParamCount(fn.Signature.RecvTypeParams()) == 0 || len(fn.Blocks) != 1 { + return false + } + var nilCheck *ssa.Call + var receiverLoad *ssa.UnOp + var wrapperCall *ssa.Call + var callee *ssa.Function + for _, instruction := range fn.Blocks[0].Instrs { + switch instruction := instruction.(type) { + case *ssa.DebugRef, *ssa.Return: + case *ssa.Call: + if builtin, ok := instruction.Common().Value.(*ssa.Builtin); ok { + if nilCheck != nil || builtin.Name() != "ssa:wrapnilchk" || len(instruction.Common().Args) != 3 || + instruction.Common().Args[0] != fn.Params[0] || + !types.Identical(instruction.Type(), fn.Params[0].Type()) { + return false + } + nilCheck = instruction + continue + } + if wrapperCall != nil || instruction.Common() == nil || instruction.Common().IsInvoke() { + return false + } + callee = instruction.Common().StaticCallee() + if callee == nil { + return false + } + wrapperCall = instruction + case *ssa.UnOp: + if receiverLoad != nil || instruction.Op != token.MUL { + return false + } + receiverLoad = instruction + default: + return false + } + } + if nilCheck == nil || receiverLoad == nil || receiverLoad.X != nilCheck || wrapperCall == nil || + callee == nil || !coroMaterializedGenericInstance(callee) || + callee.Signature == nil || callee.Signature.Recv() == nil || len(wrapperCall.Common().Args) == 0 || + wrapperCall.Common().Args[0] != receiverLoad { + return false + } + calleeOrigin := callee.Origin() + if calleeOrigin == nil || calleeOrigin.Name() != fn.Name() { + return false + } + wrapperReceiver, pointerReceiver := types.Unalias(fn.Signature.Recv().Type()).Underlying().(*types.Pointer) + if !pointerReceiver || !types.Identical(wrapperReceiver.Elem(), callee.Signature.Recv().Type()) || + !types.Identical(receiverLoad.Type(), callee.Signature.Recv().Type()) { + return false + } + expectedSyntheticPrefix := "wrapper for func (" + callee.Signature.Recv().Type().String() + ")." + calleeOrigin.Name() + "(" + if !strings.HasPrefix(fn.Synthetic, expectedSyntheticPrefix) { + return false + } + wrapperSig := coroPhysicalNormalizeSourceSignature(fn.Signature) + calleeSig := coroPhysicalNormalizeSourceSignature(callee.Signature) + if wrapperSig == nil || calleeSig == nil || wrapperSig.Params().Len() != len(fn.Params) || + wrapperSig.Params().Len() != calleeSig.Params().Len() || + wrapperSig.Results().Len() != calleeSig.Results().Len() { + return false + } + for index, parameter := range fn.Params { + if parameter == nil || !types.Identical(parameter.Type(), wrapperSig.Params().At(index).Type()) || + (index != 0 && !types.Identical(wrapperSig.Params().At(index).Type(), calleeSig.Params().At(index).Type())) { + return false + } + } + for index := 0; index < wrapperSig.Results().Len(); index++ { + if !types.Identical(wrapperSig.Results().At(index).Type(), calleeSig.Results().At(index).Type()) { + return false + } + } + + return true +} + +func coroGroundGenericTypeArgs(arguments []types.Type) bool { + if len(arguments) == 0 { + return false + } + for _, argument := range arguments { + if coroTypeContainsUnresolvedTypeParam(argument, make(map[types.Type]bool)) { + return false + } + } + return true +} + +// coroTypeContainsUnresolvedTypeParam is deliberately deeper than the +// physical-value validator: a pointer has a fixed transport width, but +// *Box[T] is still not a materialized generic identity. This proof is used +// only for instantiation identity and therefore follows referents and named +// underlying types all the way to a TypeParam. +func coroTypeContainsUnresolvedTypeParam(typ types.Type, visiting map[types.Type]bool) bool { + if typ == nil { + return true + } + typ = types.Unalias(typ) + if visiting[typ] { + return false + } + visiting[typ] = true + defer delete(visiting, typ) + + switch value := typ.(type) { + case *types.TypeParam: + return true + case *types.Named: + if arguments := value.TypeArgs(); arguments != nil { + for index := 0; index < arguments.Len(); index++ { + if coroTypeContainsUnresolvedTypeParam(arguments.At(index), visiting) { + return true + } + } + } + return coroTypeContainsUnresolvedTypeParam(value.Underlying(), visiting) + case *types.Pointer: + return coroTypeContainsUnresolvedTypeParam(value.Elem(), visiting) + case *types.Array: + return coroTypeContainsUnresolvedTypeParam(value.Elem(), visiting) + case *types.Slice: + return coroTypeContainsUnresolvedTypeParam(value.Elem(), visiting) + case *types.Chan: + return coroTypeContainsUnresolvedTypeParam(value.Elem(), visiting) + case *types.Map: + return coroTypeContainsUnresolvedTypeParam(value.Key(), visiting) || + coroTypeContainsUnresolvedTypeParam(value.Elem(), visiting) + case *types.Struct: + for index := 0; index < value.NumFields(); index++ { + if coroTypeContainsUnresolvedTypeParam(value.Field(index).Type(), visiting) { + return true + } + } + case *types.Signature: + if typeParamCount(value.TypeParams()) != 0 || typeParamCount(value.RecvTypeParams()) != 0 { + return true + } + if value.Recv() != nil && coroTypeContainsUnresolvedTypeParam(value.Recv().Type(), visiting) { + return true + } + for _, tuple := range []*types.Tuple{value.Params(), value.Results()} { + for index := 0; index < tuple.Len(); index++ { + if coroTypeContainsUnresolvedTypeParam(tuple.At(index).Type(), visiting) { + return true + } + } + } + case *types.Tuple: + for index := 0; index < value.Len(); index++ { + if coroTypeContainsUnresolvedTypeParam(value.At(index).Type(), visiting) { + return true + } + } + case *types.Interface: + value.Complete() + for index := 0; index < value.NumMethods(); index++ { + if coroTypeContainsUnresolvedTypeParam(value.Method(index).Type(), visiting) { + return true + } + } + for index := 0; index < value.NumEmbeddeds(); index++ { + if coroTypeContainsUnresolvedTypeParam(value.EmbeddedType(index), visiting) { + return true + } + } + case *types.Union: + for index := 0; index < value.Len(); index++ { + if coroTypeContainsUnresolvedTypeParam(value.Term(index).Type(), visiting) { + return true + } + } + } + return false +} + +// resolveCoroStaticPlainCall proves the synchronous island allowed inside a +// runnable physical coroutine. The exact CallPlan must select either one +// defined primary plain body, one frozen known external plain entry, or one +// exact TrustedInline invocation of a conservatively unknown foreign entry. +// The last form is an edge capability: it suppresses BlockForeign only for +// that call and never upgrades the target's default policy. A +// missing/open/dynamic edge may not fall back to the legacy source symbol. +func resolveCoroStaticPlainCall(plan *coro.SSAPlan, call ssa.CallInstruction) (*ssa.Function, coro.FunctionPlan, error) { + if plan == nil || call == nil || call.Common() == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("requires a compilation CallPlan") + } + common := call.Common() + if common.IsInvoke() { + return nil, coro.FunctionPlan{}, fmt.Errorf("requires a non-invoke call") + } + callPlan, ok := plan.CallPlan(call) + if !ok { + return nil, coro.FunctionPlan{}, fmt.Errorf("call has no compilation CallPlan") + } + trustedInline := callPlan.Kind == coro.CallTrustedInline + ordinaryDirect := callPlan.Kind == coro.CallDirect + if (!ordinaryDirect && !trustedInline) || callPlan.Rep != coro.DirectPlain || callPlan.Open || callPlan.MayBeNil || len(callPlan.Targets) != 1 { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "requires one closed non-nil direct plain target, got kind=%v representation=%s open=%t may-be-nil=%t targets=%d", + callPlan.Kind, callPlan.Rep, callPlan.Open, callPlan.MayBeNil, len(callPlan.Targets), + ) + } + if trustedInline { + if callPlan.InvocationPolicy != coro.InvocationTrustedInline || callPlan.InvocationContract == "" || + callPlan.InvocationABI == "" || callPlan.InvocationCertificate == "" { + return nil, coro.FunctionPlan{}, fmt.Errorf("trusted-inline direct call has incomplete frozen invocation metadata") + } + } else if callPlan.InvocationPolicy != "" || callPlan.InvocationContract != "" || + callPlan.InvocationABI != "" || callPlan.InvocationCertificate != "" { + return nil, coro.FunctionPlan{}, fmt.Errorf("ordinary direct call unexpectedly carries invocation capability metadata") + } + target, ok := plan.Function(callPlan.Targets[0]) + if !ok || target == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("direct plain target %q is absent from the compilation plan", callPlan.Targets[0]) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.ID != callPlan.Targets[0] { + return nil, coro.FunctionPlan{}, fmt.Errorf("direct plain target %q has no canonical function plan", callPlan.Targets[0]) + } + if common.StaticCallee() == nil && (len(target.FreeVars) != 0 || target.Signature == nil || target.Signature.Recv() != nil) { + return nil, coro.FunctionPlan{}, fmt.Errorf("closed direct plain target requires a non-capturing non-method callable") + } + validBody := targetPlan.External == coro.Defined && targetPlan.Emission == coro.EmitPlain && targetPlan.Primary == coro.PrimaryPlain + validExternal := targetPlan.External == coro.ExternalKnown && targetPlan.Emission == coro.EmitExternal && targetPlan.Primary == coro.PrimaryExternal + validTrustedExternal := trustedInline && targetPlan.External == coro.ExternalUnknownForeign && + targetPlan.Emission == coro.EmitExternal && targetPlan.Primary == coro.PrimaryExternal + effectiveExec := targetPlan.Exec + allowedExec := coro.MayUnwind | coro.IRQUnsafe + if trustedInline { + targetCertificate, certified := plan.CallableContractCertificate(target) + if !certified { + return nil, coro.FunctionPlan{}, fmt.Errorf("trusted-inline target has no frozen callable contract certificate") + } + if err := targetCertificate.Validate(); err != nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("trusted-inline target has invalid callable contract certificate: %w", err) + } + if targetCertificate.Scope != coro.CallableContractScopeDeclaration || !targetCertificate.HasTrustedInlineContract { + return nil, coro.FunctionPlan{}, fmt.Errorf("trusted-inline target does not own one declaration refinement") + } + if callPlan.InvocationContract != targetCertificate.TrustedInlineContract.ID { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "trusted-inline invocation contract %q is not owned by target %q (want %q)", + callPlan.InvocationContract, targetPlan.ID, targetCertificate.TrustedInlineContract.ID, + ) + } + if callPlan.InvocationABI != targetCertificate.CallableABI { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "trusted-inline invocation ABI %q differs from target %q ABI %q", + callPlan.InvocationABI, targetPlan.ID, targetCertificate.CallableABI, + ) + } + if err := coro.ValidateTrustedInlineCallableContractRefinement( + targetCertificate.TrustedInlineContract, targetCertificate.Contract, + ); err != nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("trusted-inline target refinement is invalid: %w", err) + } + defaultExec := coro.CallableContractExecConstraints(targetCertificate.Contract) + selectedExec := coro.CallableContractExecConstraints(targetCertificate.TrustedInlineContract) + const contractExec = coro.ThreadAffine | coro.OpaqueExec + if unsupported := (defaultExec | selectedExec) &^ contractExec; unsupported != 0 { + return nil, coro.FunctionPlan{}, fmt.Errorf("trusted-inline target projected non-contract execution flags %s", unsupported) + } + if widening := selectedExec &^ defaultExec; widening != 0 { + return nil, coro.FunctionPlan{}, fmt.Errorf("trusted-inline selected execution projection widens default by %s", widening) + } + declared := targetPlan.DeclaredExec & contractExec + localLane := targetPlan.LocalExec & contractExec + finalLane := targetPlan.Exec & contractExec + if declared != defaultExec || localLane != defaultExec || finalLane != defaultExec { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "trusted-inline target default contract execution projection is %s, lanes are declared=%s local=%s final=%s", + defaultExec, declared, localLane, finalLane, + ) + } + // ProgressExecutorSafe removes this exact edge's default stack cut. The + // selected contract replaces only its own projected lane; IRQUnsafe, + // MayUnwind, and unrelated constraints remain in effectiveExec. + effectiveExec &^= coro.BlockForeign + effectiveExec &^= defaultExec + effectiveExec |= selectedExec + } + unsupportedExec := effectiveExec &^ allowedExec + directEntry := targetPlan.FuncRep == coro.DirectPlain || + (common.StaticCallee() != nil && targetPlan.FuncRep == coro.Dispatch && validBody) + if (!validBody && !validExternal && !validTrustedExternal) || !directEntry || targetPlan.Effect != coro.NoSuspend || + targetPlan.Demand == coro.NoDemand || unsupportedExec != 0 { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "target %q is not one demanded defined, known-external, or exact trusted-inline foreign bounded no-suspend plain entry (external=%s emission=%s primary=%s representation=%s effect=%s exec=%s effective-exec=%s demand=%s)", + targetPlan.ID, targetPlan.External, targetPlan.Emission, targetPlan.Primary, targetPlan.FuncRep, targetPlan.Effect, targetPlan.Exec, effectiveExec, targetPlan.Demand, + ) + } + return target, targetPlan, nil +} + +// validateCoroLeafPhysicalABI preserves the v0 leaf-only acceptance boundary +// and diagnostics. Enabling later physical ABI capabilities must not silently +// change an archive still identified as PhysicalABIV0/SchedulerNoneABIV0. +func validateCoroLeafPhysicalABI(fn *ssa.Function, plan coro.FunctionPlan) error { + fail := func(format string, args ...any) error { + return fmt.Errorf("coroutine physical ABI: function %q: %s", plan.ID, fmt.Sprintf(format, args...)) + } + if fn == nil || plan.External != coro.Defined || len(fn.Blocks) == 0 { + return fail("requires one defined SSA body") + } + if emitShadowStackInstrumentation { + return fail("legacy thread-local shadow-stack instrumentation is incompatible with stackless coroutine suspension") + } + if plan.Emission != coro.EmitCoroutine || plan.FuncRep != coro.DirectCoro { + return fail("requires a direct coroutine emission, got emission=%s representation=%s", plan.Emission, plan.FuncRep) + } + if !plan.ManagedDemand.Contains(coro.AsyncDemand) { + return fail( + "requires managed async demand, got aggregate=%s managed=%s raw=%t raw-entry=%t", + plan.Demand, plan.ManagedDemand, plan.RawPlainDemand, plan.RawPlainEntry, + ) + } + if plan.Recursive { + return fail("recursive coroutine lowering requires child frames and preemption polls") + } + if plan.DeclaredEffect != coro.YieldOnly || plan.LocalEffect != coro.YieldOnly || plan.Effect != coro.YieldOnly { + return fail("requires an explicit, isolated yield-only effect, got declared=%s local=%s final=%s", plan.DeclaredEffect, plan.LocalEffect, plan.Effect) + } + if unsupported := plan.Exec &^ coro.MayUnwind; unsupported != 0 { + return fail("execution flags %s require lowering outside the leaf ABI", unsupported) + } + if len(fn.FreeVars) != 0 { + return fail("closures require the coroutine context ABI") + } + for _, nested := range fn.AnonFuncs { + if nested != nil && len(nested.FreeVars) != 0 { + return fail("nested function literals require closure body lowering") + } + } + if fn.Signature.Variadic() { + return fail("variadic coroutine ABI is not implemented") + } + if directive := coroLeafABIDirective(fn); directive != "" { + return fail("ABI directive %q requires a root or foreign adapter", directive) + } + if isCgoExternSymbol(fn) { + return fail("cgo entry requires a foreign adapter") + } + genericInstance := coroMaterializedGenericCallable(fn) + if fn.Synthetic != "" && !genericInstance { + return fail("synthetic function %q is outside the leaf ABI", fn.Synthetic) + } + if list := fn.TypeParams(); list != nil && list.Len() != 0 && !genericInstance { + return fail("generic declarations are not materialized coroutine bodies") + } + if list := fn.Signature.RecvTypeParams(); list != nil && list.Len() != 0 && !genericInstance { + return fail("generic receivers are not materialized coroutine bodies") + } + if list := fn.TypeArgs(); len(list) != 0 && !genericInstance { + return fail("generic instances require a frozen instantiated ABI") + } + if isCoroProgramManagedEntry(fn) { + return fail("program roots require scheduler bootstrap lowering") + } + if len(fn.Blocks) != 1 { + return fail("requires exactly one basic block, got %d", len(fn.Blocks)) + } + physicalSourceSig := coroPhysicalNormalizeSourceSignature(fn.Signature) + if err := validateCoroPhysicalSSAParameterShape(plan, fn, physicalSourceSig, nil); err != nil { + return err + } + if err := validateCoroLeafPhysicalSignature(plan, physicalSourceSig); err != nil { + return err + } + + returns := 0 + for _, instr := range fn.Blocks[0].Instrs { + switch instr := instr.(type) { + case *ssa.DebugRef: + case *ssa.Return: + returns++ + case *ssa.BinOp: + if instr.Op == token.QUO || instr.Op == token.REM || instr.Op == token.SHL || instr.Op == token.SHR || + !coroLeafScalar(instr.Type()) || + !coroLeafScalar(instr.X.Type()) || !coroLeafScalar(instr.Y.Type()) { + return coroLeafInstructionError(fn, plan, instr, "potentially panicking or non-scalar binary operation") + } + case *ssa.UnOp: + if (instr.Op != token.SUB && instr.Op != token.XOR && instr.Op != token.NOT) || !coroLeafScalar(instr.Type()) { + return coroLeafInstructionError(fn, plan, instr, "unsupported unary operation") + } + case *ssa.Convert, *ssa.ChangeType: + value, ok := instr.(ssa.Value) + if !ok || !coroLeafScalar(value.Type()) { + return coroLeafInstructionError(fn, plan, instr, "non-scalar conversion") + } + default: + return coroLeafInstructionError(fn, plan, instr, "instruction is outside the ABI-only leaf allowlist") + } + } + if returns != 1 { + return fail("requires exactly one return instruction, got %d", returns) + } + return nil +} + +func (u *EmissionUniverse) coroPhysicalSourceSignature(fn *ssa.Function) (*types.Signature, error) { + owner := u.ownerOf(fn) + ctx, err := u.functionABIContext(fn, owner) + if err != nil { + return nil, fmt.Errorf("coroutine physical ABI: function %q: derive effective signature: %w", fn.Name(), err) + } + sig, ok := ctx.patchType(fn.Signature).(*types.Signature) + if !ok { + return nil, fmt.Errorf("coroutine physical ABI: function %q: effective type is not a signature", fn.Name()) + } + if receiver := fn.Signature.Recv(); receiver != nil { + effectiveReceiver := ctx.patchType(receiver.Type()) + if !types.Identical(effectiveReceiver, sig.Recv().Type()) { + materialized := coroMaterializedGenericCallable(fn) + if !materialized && (typeParamCount(sig.RecvTypeParams()) != 0 || typeParamCount(sig.TypeParams()) != 0) { + return nil, fmt.Errorf( + "coroutine physical ABI: function %q requires receiver patching before its generic declaration is materialized", + fn.Name(), + ) + } + receiver = types.NewVar(receiver.Pos(), receiver.Pkg(), receiver.Name(), effectiveReceiver) + // A concrete x/tools receiver instance may retain the origin's + // RecvTypeParams list even though every callable type is ground. Those + // TypeParam objects are already bound to sig and cannot legally be + // rebound into a second go/types Signature. They are source metadata, + // not part of the physical receiver-first ABI, so the reconstructed + // concrete signature deliberately clears both parameter lists. + sig = types.NewSignatureType( + receiver, + nil, + nil, + sig.Params(), sig.Results(), sig.Variadic(), + ) + } + } + if params := sig.RecvTypeParams(); params != nil && params.Len() != 0 && !coroMaterializedGenericCallable(fn) { + pkgPath := "" + if fn.Pkg != nil && fn.Pkg.Pkg != nil { + pkgPath = fn.Pkg.Pkg.Path() + } + origin, originName := fn.Origin(), "" + if origin != nil { + originName = origin.String() + } + return nil, fmt.Errorf( + "coroutine physical ABI: function %q (%s, package=%q, synthetic=%q, origin=%s, type-args=%d): effective generic receiver has %d type parameters", + fn.Name(), fn.String(), pkgPath, fn.Synthetic, originName, len(fn.TypeArgs()), params.Len(), + ) + } + return coroPhysicalNormalizeSourceSignature(sig), nil +} + +// coroPhysicalEntrySourceSignature adds the one typed closure environment that +// belongs to a captured physical entry. It is deliberately separate from +// coroPhysicalSourceSignature: source call sites and lowered helper markers see +// only explicit Go parameters, while the descriptor thunk supplies this +// compiler-owned context between (g,out) and those parameters. +func (u *EmissionUniverse) coroPhysicalEntrySourceSignature(fn *ssa.Function) (*types.Signature, error) { + sig, err := u.coroPhysicalSourceSignature(fn) + if err != nil || fn == nil || len(fn.FreeVars) == 0 { + return sig, err + } + if fn.Signature == nil || fn.Signature.Recv() != nil { + return nil, fmt.Errorf("coroutine physical ABI: captured function %q must be a receiver-free closure body", fn.Name()) + } + owner := u.ownerOf(fn) + if owner == nil || owner.pkgTypes == nil { + return nil, fmt.Errorf("coroutine physical ABI: captured function %q has no emission owner", fn.Name()) + } + return llssa.FuncAddCtx(makeClosureCtx(owner.pkgTypes, fn.FreeVars), sig), nil +} + +// coroPhysicalNormalizeSourceSignature maps a declared receiver to the exact +// leading ordinary parameter used by x/tools SSA and LLGo's existing Go method +// declaration ABI. It also clears the source-only variadic marker: x/tools SSA +// has already packed a variadic call into its final []T argument, and every +// LLVM coroutine entry/call transports that ordinary slice value. Thus no C +// varargs convention or second coroutine ABI is involved. It is idempotent. +func coroPhysicalNormalizeSourceSignature(sig *types.Signature) *types.Signature { + if sig == nil { + return sig + } + if sig.Recv() != nil { + sig = llssa.FuncAddCtx(sig.Recv(), sig) + } + if !sig.Variadic() { + return sig + } + return types.NewSignatureType(nil, nil, nil, sig.Params(), sig.Results(), false) +} + +func validateCoroPhysicalSSAParameterShape(plan coro.FunctionPlan, fn *ssa.Function, effective *types.Signature, universe *EmissionUniverse) error { + fail := func(format string, args ...any) error { + name := "" + if fn != nil { + name = fn.String() + } + return fmt.Errorf("coroutine physical ABI: function %q (%s): %s", plan.ID, name, fmt.Sprintf(format, args...)) + } + if fn == nil || fn.Signature == nil || effective == nil { + return fail("requires an SSA function and effective source signature") + } + source := coroPhysicalNormalizeSourceSignature(fn.Signature) + if source.Params().Len() != len(fn.Params) { + return fail("normalized source parameters=%d do not match SSA parameters=%d", source.Params().Len(), len(fn.Params)) + } + offset := 0 + if len(fn.FreeVars) != 0 { + offset = 1 + if effective.Params().Len() == 0 || !coroPhysicalClosureContextMatches(fn, effective.Params().At(0).Type()) { + return fail("effective captured entry has no exact typed closure context") + } + } + if effective.Params().Len() != len(fn.Params)+offset { + return fail("effective entry parameters=%d do not match SSA parameters=%d plus hidden-context=%d", effective.Params().Len(), len(fn.Params), offset) + } + for index, parameter := range fn.Params { + if parameter == nil || !types.Identical(parameter.Type(), source.Params().At(index).Type()) { + return fail("SSA parameter %d type %v does not match normalized source parameter %v", index, parameterType(parameter), source.Params().At(index).Type()) + } + effectiveType := effective.Params().At(index + offset).Type() + sourceType := source.Params().At(index).Type() + if !types.Identical(effectiveType, sourceType) && + (universe == nil || coroPhysicalTransportTypeKey(universe, effectiveType) != coroPhysicalTransportTypeKey(universe, sourceType)) { + return fail("effective parameter %d type %v is not ABI-compatible with normalized source parameter %v", index, effectiveType, sourceType) + } + } + return nil +} + +// coroPhysicalTransportTypeKey describes only the value transported by the +// coroutine entry ABI. It deliberately erases pointee and logical descriptor +// identity: LLVM opaque pointers do not carry a referent type, while LLGo's +// map, channel, interface, and slice values each have one frozen aggregate +// shape independent of their source element or method types. Function values +// are different: an exact //llgo:type C function is one opaque code pointer, +// whereas a managed Go function is a two-word descriptor. Inline arrays and +// structs remain recursive and exact. This is used only after the emission +// universe has proved an exact source -> canonical patch alias. +func coroPhysicalTransportTypeKey(universe *EmissionUniverse, typ types.Type) string { + var key func(types.Type) string + key = func(typ types.Type) string { + typ = types.Unalias(typ) + if _, signature := typ.Underlying().(*types.Signature); signature { + transport, err := coroCallableLeafTransport(universe, typ) + if err == nil && transport == coro.RawCCodePointer { + // Raw C function values use the same one-word LLVM transport as + // every other opaque pointer. Keeping this physical equivalence is + // required for exact frontend patch aliases to pointer types. + return framedEmissionKey("opaque-pointer") + } + // Fail closed on absent/invalid raw-C metadata: only an exact + // frontend classification can select the one-word transport. + return framedEmissionKey("managed-function-descriptor") + } + switch value := typ.(type) { + case *types.Named: + return key(value.Underlying()) + case *types.Basic: + if value.Kind() == types.UnsafePointer { + return framedEmissionKey("opaque-pointer") + } + return framedEmissionKey("basic", fmt.Sprint(int(value.Kind()))) + case *types.Pointer: + return framedEmissionKey("opaque-pointer") + case *types.Map: + return framedEmissionKey("map") + case *types.Chan: + return framedEmissionKey("chan") + case *types.Interface: + return framedEmissionKey("interface") + case *types.Slice: + return framedEmissionKey("slice") + case *types.Array: + return framedEmissionKey("array", fmt.Sprint(value.Len()), key(value.Elem())) + case *types.Struct: + fields := make([]string, 0, value.NumFields()+1) + fields = append(fields, "struct") + for index := 0; index < value.NumFields(); index++ { + fields = append(fields, key(value.Field(index).Type())) + } + return framedEmissionKey(fields...) + case *types.Tuple: + fields := make([]string, 0, value.Len()+1) + fields = append(fields, "tuple") + for index := 0; index < value.Len(); index++ { + fields = append(fields, key(value.At(index).Type())) + } + return framedEmissionKey(fields...) + default: + return framedEmissionKey("unsupported", fmt.Sprintf("%T", typ)) + } + } + return key(typ) +} + +func parameterType(parameter *ssa.Parameter) types.Type { + if parameter == nil { + return nil + } + return parameter.Type() +} + +func coroPhysicalClosureContextMatches(fn *ssa.Function, typ types.Type) bool { + if fn == nil || len(fn.FreeVars) == 0 || typ == nil { + return false + } + pointer, ok := types.Unalias(typ).Underlying().(*types.Pointer) + if !ok { + return false + } + fields, ok := types.Unalias(pointer.Elem()).Underlying().(*types.Struct) + if !ok || fields.NumFields() != len(fn.FreeVars) { + return false + } + for index, free := range fn.FreeVars { + if free == nil || !types.Identical(fields.Field(index).Type(), free.Type()) { + return false + } + } + return true +} + +func validateCoroLeafPhysicalSignature(plan coro.FunctionPlan, sig *types.Signature) error { + fail := func(format string, args ...any) error { + return fmt.Errorf("coroutine physical ABI: function %q: %s", plan.ID, fmt.Sprintf(format, args...)) + } + if sig == nil { + return fail("requires a physical source signature") + } + if sig.Variadic() { + return fail("effective variadic coroutine ABI is not implemented") + } + if params := sig.TypeParams(); params != nil && params.Len() != 0 { + return fail("effective generic declaration has %d type parameters", params.Len()) + } + if params := sig.RecvTypeParams(); params != nil && params.Len() != 0 { + return fail("effective generic receiver has %d type parameters", params.Len()) + } + sig = coroPhysicalNormalizeSourceSignature(sig) + for i := 0; i < sig.Params().Len(); i++ { + if err := validateCoroPhysicalValueType(sig.Params().At(i).Type(), make(map[types.Type]bool)); err != nil { + return fail("parameter %d has unsupported type %s: %v", i, sig.Params().At(i).Type(), err) + } + } + for i := 0; i < sig.Results().Len(); i++ { + if err := validateCoroPhysicalValueType(sig.Results().At(i).Type(), make(map[types.Type]bool)); err != nil { + return fail("result %d has unsupported type %s: %v", i, sig.Results().At(i).Type(), err) + } + } + return nil +} + +// validateCoroPhysicalFunctionValueABI keeps function-valued transport on the +// one compilation-wide representation path. The generic LLGo type converter +// supplies the canonical two-pointer closure layout, while FuncRepABIV1's +// ValuePlan validation decides whether the first word is a direct entry or a +// descriptor. Accepting the width here must not create a second, unplanned +// function representation at a coroutine boundary. +func validateCoroPhysicalFunctionValueABI(plan coro.FunctionPlan, sig *types.Signature, plainDispatch bool) error { + if sig == nil || !coroPhysicalSignatureContainsFunctionValue(sig) || plainDispatch { + return nil + } + return fmt.Errorf( + "coroutine physical ABI: function %q: function-valued parameters/results require canonical ValuePlan validation and the descriptor/closure ABI", + plan.ID, + ) +} + +func coroPhysicalSignatureContainsFunctionValue(sig *types.Signature) bool { + for _, tuple := range []*types.Tuple{sig.Params(), sig.Results()} { + if tuple == nil { + continue + } + for i := 0; i < tuple.Len(); i++ { + if coroPhysicalTypeContainsFunctionValue(tuple.At(i).Type(), make(map[types.Type]bool)) { + return true + } + } + } + return false +} + +func coroPhysicalTypeContainsFunctionValue(typ types.Type, visiting map[types.Type]bool) bool { + if typ == nil { + return false + } + typ = types.Unalias(typ) + if visiting[typ] { + return false + } + visiting[typ] = true + defer delete(visiting, typ) + switch value := typ.(type) { + case *types.Signature: + return true + case *types.Named: + return coroPhysicalTypeContainsFunctionValue(value.Underlying(), visiting) + case *types.Struct: + for i := 0; i < value.NumFields(); i++ { + if coroPhysicalTypeContainsFunctionValue(value.Field(i).Type(), visiting) { + return true + } + } + case *types.Array, *types.Slice, *types.Chan: + var elem types.Type + switch container := value.(type) { + case *types.Array: + elem = container.Elem() + case *types.Slice: + elem = container.Elem() + case *types.Chan: + elem = container.Elem() + } + return coroPhysicalTypeContainsFunctionValue(elem, visiting) + case *types.Map: + return coroPhysicalTypeContainsFunctionValue(value.Key(), visiting) || + coroPhysicalTypeContainsFunctionValue(value.Elem(), visiting) + case *types.Tuple: + for i := 0; i < value.Len(); i++ { + if coroPhysicalTypeContainsFunctionValue(value.At(i).Type(), visiting) { + return true + } + } + } + return false +} + +// validateCoroPhysicalValueType proves only that a source value has a stable +// LLGo by-value representation that can be copied through the typed coroutine +// result slot. It does not authorize any SSA producer/consumer instruction: +// those remain governed by the physical-body allowlist and ValuePlan checks. +func validateCoroPhysicalValueType(typ types.Type, visiting map[types.Type]bool) error { + if typ == nil { + return fmt.Errorf("nil type") + } + typ = types.Unalias(typ) + if visiting[typ] { + return nil + } + visiting[typ] = true + defer delete(visiting, typ) + + switch value := typ.(type) { + case *types.Named: + return validateCoroPhysicalValueType(value.Underlying(), visiting) + case *types.Basic: + if value.Kind() == types.Invalid || value.Info()&types.IsUntyped != 0 { + return fmt.Errorf("invalid or untyped basic kind %s", value) + } + return nil + case *types.Pointer, *types.Map, *types.Chan, *types.Interface, *types.Slice, *types.Signature: + // These are target-width opaque pointers or LLGo's stable descriptor / + // closure aggregates. Their referent/method/call signature is logical + // identity, not an inline extension of the transported value layout. + return nil + case *types.Struct: + for i := 0; i < value.NumFields(); i++ { + if err := validateCoroPhysicalValueType(value.Field(i).Type(), visiting); err != nil { + return fmt.Errorf("field %d: %w", i, err) + } + } + return nil + case *types.Array: + if value.Len() < 0 { + return fmt.Errorf("negative array length %d", value.Len()) + } + return validateCoroPhysicalValueType(value.Elem(), visiting) + case *types.TypeParam: + return fmt.Errorf("uninstantiated type parameter") + case *types.Tuple: + return fmt.Errorf("tuple is valid only as the outer result list") + case *types.Union: + return fmt.Errorf("union has no runtime value representation") + default: + return fmt.Errorf("unsupported type class %T", typ) + } +} + +func coroLeafABIDirective(fn *ssa.Function) string { + decl, _ := fn.Syntax().(*ast.FuncDecl) + if decl == nil || decl.Doc == nil { + return "" + } + for _, comment := range decl.Doc.List { + text := strings.TrimSpace(comment.Text) + for _, prefix := range []string{ + "//go:linkname", "//llgo:link", "// llgo:link", "//export", "//go:wasmexport", "//go:wasmimport", + } { + if text == prefix || strings.HasPrefix(text, prefix+" ") { + return text + } + } + if strings.HasPrefix(text, "//go:cgo_") { + return text + } + } + return "" +} + +// coroRawABIDirective separates an exact source-level Go symbol alias or +// visibility declaration from a physical ABI crossing. A bodyful +// //go:linkname definition is managed when the prepared emission universe has +// either activated an exact bodyless Go declaration -> definition alias for +// the same final symbol and structural signature, retained such an exact +// pending pair from an ordinary non-metadata input, or frozen a strict +// visibility-only certificate for an unredirected two-field directive. In +// each case every in-program Go call resolves to the managed primary +// (including its $coro spelling), so publishing an unrelated legacy +// RawPlainEntry would be both unnecessary and incorrect. +// +// All exports, cgo/wasm/custom links, malformed or duplicate go:linkname text, +// and unpaired redirecting/two-argument go:linkname definitions remain +// raw/unproven boundaries. This is deliberately fail-closed for assembly or +// out-of-universe consumers. +func coroRawABIDirective(fn *ssa.Function, universe *EmissionUniverse) (string, error) { + decl, _ := fn.Syntax().(*ast.FuncDecl) + if decl == nil || decl.Doc == nil { + return "", nil + } + managedDirective, exactManagedSyntax := attachedManagedGoLinknameDirective(decl) + managedDefinition := false + managedVisibility := false + if exactManagedSyntax { + var err error + managedDefinition, err = universe.exactManagedGoLinknameDefinition(fn) + if err != nil { + return "", err + } + _, managedVisibility, err = universe.coroGoLinknameVisibilityCertificate(fn) + if err != nil { + return "", err + } + } + for _, comment := range decl.Doc.List { + if comment == nil { + continue + } + text := strings.TrimSpace(comment.Text) + if text == "//go:linkname" || strings.HasPrefix(text, "//go:linkname ") { + if exactManagedSyntax && (managedDefinition || managedVisibility) && text == managedDirective { + continue + } + return text, nil + } + for _, prefix := range []string{ + "//llgo:link", "// llgo:link", "//export", "//go:wasmexport", "//go:wasmimport", + } { + if text == prefix || strings.HasPrefix(text, prefix+" ") { + return text, nil + } + } + if strings.HasPrefix(text, "//go:cgo_") { + return text, nil + } + } + return "", nil +} + +func attachedManagedGoLinknameDirective(decl *ast.FuncDecl) (string, bool) { + if decl == nil || decl.Body == nil || decl.Doc == nil || decl.Name == nil { + return "", false + } + _, localName := astFuncName("", decl) + var found string + for _, comment := range decl.Doc.List { + if comment == nil { + continue + } + fields := strings.Fields(comment.Text) + if len(fields) == 0 || fields[0] != "//go:linkname" { + continue + } + if found != "" || len(fields) != 2 && len(fields) != 3 || fields[1] != localName { + return "", false + } + found = strings.TrimSpace(comment.Text) + } + return found, found != "" +} + +func validateCoroPhysicalConsumers(plan *coro.SSAPlan, childAwait bool) error { + return validateCoroPhysicalConsumersCapabilities(plan, nil, childAwait, false, false) +} + +func validateCoroPhysicalConsumersCapabilities( + plan *coro.SSAPlan, + universe *EmissionUniverse, + childAwait, staticSpawn, managedDispatch bool, +) error { + coroutineIDs := make(map[coro.FunctionID]struct{}) + for _, function := range plan.Functions() { + if function.Plan.Emission == coro.EmitCoroutine { + coroutineIDs[function.Plan.ID] = struct{}{} + } + } + for _, function := range plan.Functions() { + if function.Plan.Emission != coro.EmitPlain && function.Plan.Emission != coro.EmitCoroutine { + continue + } + fn := function.Function + unevaluated, _ := universe.frozenUnsafeSizeAlignUnevaluatedSSA(fn) + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if _, omitted := unevaluated[instr]; omitted { + continue + } + if store, ok := instr.(*ssa.Store); ok && plan.ElidesConditionalManagedStore(store) { + // This occurrence is a frozen closed-cell publication whose + // target has no live consumer. Code generation omits it before + // resolving the otherwise non-emitted function operand. + continue + } + if spawn, ok := instr.(*ssa.Go); ok { + if !staticSpawn { + return coroLeafInstructionError(fn, function.Plan, instr, "goroutine spawn requires scheduler root lowering") + } + callPlan, found := plan.CallPlan(spawn) + if !found { + return coroLeafInstructionError(fn, function.Plan, instr, "goroutine spawn has no compilation CallPlan") + } + switch callPlan.Rep { + case coro.DirectCoro: + if _, _, err := resolveCoroDirectStaticSpawn(plan, spawn, managedDispatch); err != nil { + return coroLeafInstructionError(fn, function.Plan, instr, "unsupported closed static spawn: "+err.Error()) + } + case coro.Dispatch: + if !managedDispatch { + return coroLeafInstructionError(fn, function.Plan, instr, "managed descriptor spawn requires the v1 descriptor dispatch capability") + } + if _, err := plan.ResolveManagedDispatchSpawn(spawn); err != nil { + return coroLeafInstructionError(fn, function.Plan, instr, "unsupported managed descriptor spawn: "+err.Error()) + } + default: + return coroLeafInstructionError(fn, function.Plan, instr, "goroutine spawn has unsupported representation "+callPlan.Rep.String()) + } + continue + } + if call, ok := instr.(ssa.CallInstruction); ok { + if plan.ElidesCall(call) { + continue + } + // SSA builtins are compiler-lowered operations, not managed + // function consumers. AnalyzeSSA deliberately does not create + // CallPlans for them, so keep the physical-ABI check focused on + // every non-builtin call instruction. + if common := call.Common(); common != nil { + if _, builtin := common.Value.(*ssa.Builtin); builtin { + continue + } + if function.Plan.Emission == coro.EmitCoroutine && common.IsInvoke() { + if _, err := resolveCoroClosedInterfacePlainCall(plan, call); err != nil { + if !childAwait { + return coroLeafInstructionError(fn, function.Plan, instr, "unsupported interface invoke: "+err.Error()) + } + if callPlan, found := plan.CallPlan(call); found && callPlan.Open && + callPlan.Unresolved == coro.UnknownManagedInterfaceDispatch { + if !managedDispatch { + return coroLeafInstructionError(fn, function.Plan, instr, + "managed interface invoke requires the v1 descriptor dispatch capability") + } + if err := validateCoroManagedInterfaceDispatchCall(plan, universe, fn, call, callPlan); err != nil { + return coroLeafInstructionError(fn, function.Plan, instr, + "invalid managed interface call: "+err.Error()) + } + continue + } + direct, ordinary := call.(*ssa.Call) + if !ordinary { + return coroLeafInstructionError(fn, function.Plan, instr, "coroutine interface dispatch requires an ordinary call") + } + dispatch, awaitErr := resolveCoroInterfaceDispatchPlan(plan, universe, direct) + if awaitErr != nil || !coroInterfaceDispatchNeedsAwait(dispatch) { + if awaitErr == nil { + awaitErr = fmt.Errorf("closed interface dispatch has no coroutine target") + } + return coroLeafInstructionError(fn, function.Plan, instr, "unsupported interface invoke: "+awaitErr.Error()) + } + } + } + } + callPlan, found := plan.CallPlan(call) + if !found { + return coroLeafInstructionError(fn, function.Plan, instr, "call has no compilation CallPlan") + } + if callPlan.Transport == coro.RawCCodePointer { + return coroLeafInstructionError(fn, function.Plan, instr, + "managed coroutine raw C code-pointer call requires an explicit event, worker, or trusted inline recipe") + } + hasCoroutineTarget := false + for _, target := range callPlan.Targets { + targetFn, found := plan.Function(target) + if !found || targetFn == nil { + return coroLeafInstructionError(fn, function.Plan, instr, fmt.Sprintf("call target %q is absent from the compilation plan", target)) + } + targetPlan, found := plan.FunctionPlan(targetFn) + if !found || targetPlan.ID != target { + return coroLeafInstructionError(fn, function.Plan, instr, fmt.Sprintf("call target %q has no canonical function plan", target)) + } + if targetPlan.Emission == coro.EmitNone { + return coroLeafInstructionError(fn, function.Plan, instr, fmt.Sprintf("emitted body references non-emitted call target %q", target)) + } + if targetPlan.Emission == coro.EmitRawPlain { + return coroLeafInstructionError( + fn, function.Plan, instr, + fmt.Sprintf("managed body calls raw-plain-only target %q without a managed entry", target), + ) + } + if _, isCoroutine := coroutineIDs[target]; isCoroutine { + hasCoroutineTarget = true + break + } + } + if callPlan.Rep == coro.Dispatch && call.Common() != nil && + call.Common().StaticCallee() == nil && !call.Common().IsInvoke() { + if deferred, cleanup := call.(*ssa.Defer); cleanup { + if !managedDispatch { + return coroLeafInstructionError(fn, function.Plan, instr, "managed descriptor defer requires the v1 descriptor dispatch capability") + } + if !childAwait || function.Plan.Emission != coro.EmitCoroutine { + return coroLeafInstructionError(fn, function.Plan, instr, "managed descriptor defer requires coroutine child-await lowering") + } + if err := validateCoroManagedDispatchDefer(plan, fn, deferred, callPlan, universe); err != nil { + return coroLeafInstructionError(fn, function.Plan, instr, "invalid managed descriptor defer: "+err.Error()) + } + if _, _, kind, err := resolveCoroStaticCleanupTarget(plan, function.Plan, deferred, universe); err != nil || kind != coroStaticCleanupDispatch { + if err == nil { + err = fmt.Errorf("resolved cleanup kind is %d", kind) + } + return coroLeafInstructionError(fn, function.Plan, instr, "invalid managed descriptor cleanup plan: "+err.Error()) + } + continue + } + if callPlan.SyncDispatch { + if !managedDispatch { + return coroLeafInstructionError(fn, function.Plan, instr, "synchronous descriptor call requires the v1 plain dispatch capability") + } + if err := validateCoroPlainDispatchCall(plan, fn, call, callPlan, universe); err != nil { + return coroLeafInstructionError(fn, function.Plan, instr, "invalid synchronous descriptor call: "+err.Error()) + } + continue + } + if callPlan.Open && callPlan.Unresolved != coro.UnknownManagedDispatch { + return coroLeafInstructionError(fn, function.Plan, instr, fmt.Sprintf( + "open descriptor call has uncertified execution domain %v", callPlan.Unresolved, + )) + } + if !managedDispatch { + return coroLeafInstructionError(fn, function.Plan, instr, "open managed descriptor call requires the v1 descriptor dispatch capability") + } + if !childAwait || function.Plan.Emission != coro.EmitCoroutine { + return coroLeafInstructionError(fn, function.Plan, instr, "open managed descriptor call requires coroutine child-await lowering") + } + if err := validateCoroManagedDispatchCall(plan, fn, call, callPlan, universe); err != nil { + return coroLeafInstructionError(fn, function.Plan, instr, "invalid managed descriptor call: "+err.Error()) + } + continue + } + if hasCoroutineTarget { + if childAwait && function.Plan.Emission == coro.EmitCoroutine && call.Common() != nil && call.Common().IsInvoke() { + if direct, ok := call.(*ssa.Call); ok { + if dispatch, err := resolveCoroInterfaceDispatchPlan(plan, universe, direct); err == nil && coroInterfaceDispatchNeedsAwait(dispatch) { + continue + } + } + } + direct, ordinary := call.(*ssa.Call) + if childAwait && ordinary && function.Plan.Emission == coro.EmitCoroutine { + if _, _, err := resolveCoroStaticAwait(plan, function.Plan, direct, universe); err == nil { + // The static callee operand is represented by this exact + // CallPlan and is not an escaped function value. + continue + } + } + deferred, cleanup := call.(*ssa.Defer) + if childAwait && cleanup && function.Plan.Emission == coro.EmitCoroutine { + if _, _, kind, err := resolveCoroStaticCleanupTarget(plan, function.Plan, deferred, universe); err == nil && kind == coroStaticCleanupCoroutine { + // The physical-body preflight separately proves the + // frame-resident record and child no-unwind contract. + continue + } + } + return coroLeafInstructionError(fn, function.Plan, instr, "coroutine target requires a supported static child await or root lowering") + } + } + for _, operand := range instr.Operands(nil) { + if operand == nil || *operand == nil { + continue + } + target, ok := (*operand).(*ssa.Function) + if !ok { + continue + } + targetPlan, planned := plan.FunctionPlan(target) + if planned && targetPlan.Emission == coro.EmitNone { + return coroLeafInstructionError(fn, function.Plan, instr, fmt.Sprintf("emitted body references non-emitted function value %q", targetPlan.ID)) + } + if planned && targetPlan.Emission == coro.EmitCoroutine { + if managedDispatch && targetPlan.FuncRep == coro.Dispatch { + // The universal descriptor producer converts this exact + // function reference. Value/consumer validation below owns + // the rest of the two-pointer transport proof. + continue + } + if closure, exactClosure := instr.(*ssa.MakeClosure); exactClosure && closure.Fn == target && + childAwait && function.Plan.Emission == coro.EmitCoroutine && len(target.FreeVars) != 0 && + targetPlan.Primary == coro.PrimaryCoroutine && targetPlan.FuncRep == coro.DirectCoro { + value, exactValue := plan.ValuePlan(closure) + if exactValue && len(value.Funcs) == 1 && len(value.Funcs[0].Path) == 0 && + value.Funcs[0].Rep == coro.DirectCoro && !value.Funcs[0].MayBeNil && + len(value.Funcs[0].Targets) == 1 && value.Funcs[0].Targets[0] == targetPlan.ID { + // compileValue retags the physical (g,out,ctx,args) + // entry solely as a canonical (ctx,args) closure carrier; + // the exact static await consumes its env word and never + // calls through that temporary code word. + continue + } + } + return coroLeafInstructionError(fn, function.Plan, instr, "coroutine function value requires physical representation conversion") + } + } + } + } + } + return nil +} + +func coroDispatchCallHasCoroutineTarget(plan *coro.SSAPlan, call coro.SSACallPlan) bool { + if plan == nil { + return false + } + for _, id := range call.Targets { + target, ok := plan.Function(id) + if !ok || target == nil { + continue + } + targetPlan, ok := plan.FunctionPlan(target) + if ok && targetPlan.Emission == coro.EmitCoroutine { + return true + } + } + return false +} + +func coroLeafScalar(typ types.Type) bool { + basic, ok := typ.Underlying().(*types.Basic) + if !ok || basic.Kind() == types.Uintptr { + return false + } + info := basic.Info() + return info&(types.IsBoolean|types.IsInteger|types.IsFloat) != 0 +} + +func coroLeafInstructionError(fn *ssa.Function, plan coro.FunctionPlan, instr ssa.Instruction, reason string) error { + pos := fn.Prog.Fset.Position(instr.Pos()) + where := "unknown position" + if pos.IsValid() { + where = fmt.Sprintf("%s:%d:%d", pos.Filename, pos.Line, pos.Column) + } + name := "" + if fn != nil { + name = fn.String() + } + operation := coroInstructionOperation(instr) + return fmt.Errorf("coroutine physical ABI: function %q (%s): %T%s at %s: %s", plan.ID, name, instr, operation, where, reason) +} + +func coroInstructionOperation(instr ssa.Instruction) (operation string) { + // Tests and validation adapters may construct partially attached SSA + // instructions. x/tools String methods assume both a complete parent and + // complete operands, so diagnostics must tolerate either being absent. + if instr == nil || instr.Block() == nil || instr.Block().Parent() == nil { + return "" + } + defer func() { + if recover() != nil { + operation = "" + } + }() + return fmt.Sprintf(" %q", instr.String()) +} diff --git a/cl/coro_abi_test.go b/cl/coro_abi_test.go new file mode 100644 index 0000000000..a46f0917d2 --- /dev/null +++ b/cl/coro_abi_test.go @@ -0,0 +1,2832 @@ +//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 ( + "bytes" + "encoding/binary" + "encoding/hex" + "go/ast" + "go/types" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroLeafPhysicalABIPresplit(t *testing.T) { + prog, pkg := compileCoroLeafPhysicalABI(t, nil) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify coroutine leaf: %v\n%s", err, module.String()) + } + ir := module.String() + if module.NamedFunction("foo.Leaf").IsNil() == false { + t.Fatalf("physical coroutine retained legacy source ABI symbol:\n%s", ir) + } + leaf := module.NamedFunction("foo.Leaf$coro") + if leaf.IsNil() { + t.Fatalf("physical coroutine symbol is absent:\n%s", ir) + } + leafIR := leaf.String() + assertCoroV1TaskAwareFrameCalls(t, "Leaf", leafIR, prog.PointerSize()*8) + if !regexp.MustCompile(`define ptr @"?foo\.Leaf\$coro"?\(ptr [^,]+, ptr [^,]+, i32 `).MatchString(leafIR) { + t.Fatalf("coroutine leaf does not use (g, out, args...) -> handle ABI:\n%s", leafIR) + } + if got := strings.Count(leafIR, "call i8 @llvm.coro.suspend"); got != 2 { + t.Fatalf("coro.suspend calls = %d, want initial + final:\n%s", got, leafIR) + } + begin := strings.Index(leafIR, "call ptr @llvm.coro.begin") + publish := strings.Index(leafIR, "call void @"+coroFramePublishHookV1) + initialSuspend := strings.Index(leafIR, "call i8 @llvm.coro.suspend") + if begin < 0 || publish < 0 || initialSuspend < 0 || !(begin < publish && publish < initialSuspend) { + t.Fatalf("promise/header was not published after coro.begin and before initial suspend:\n%s", leafIR) + } + if !strings.Contains(leafIR, "store i32") { + t.Fatalf("coroutine result was not copied to the external result slot:\n%s", leafIR) + } + for _, symbol := range []string{coroFrameAllocHookV1, coroFrameFreeHookV1, coroDescriptorPrefixV1} { + if !strings.Contains(ir, symbol) { + t.Fatalf("coroutine module is missing versioned ABI symbol %q:\n%s", symbol, ir) + } + } + for _, forbidden := range []string{"@malloc", "@free(", "stacksave", "stackrestore", "pthread"} { + if strings.Contains(ir, forbidden) { + t.Fatalf("coroutine leaf introduced forbidden stack/runtime coupling %q:\n%s", forbidden, ir) + } + } +} + +func TestCoroLeafPhysicalABIZeroResult(t *testing.T) { + prog, pkg := compileCoroLeafPhysicalABISource(t, nil, `package foo +func Leaf() {} +`) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify zero-result coroutine leaf: %v\n%s", err, module.String()) + } + leaf := module.NamedFunction("foo.Leaf$coro") + if leaf.IsNil() || !regexp.MustCompile(`define ptr @"?foo\.Leaf\$coro"?\(ptr [^,]+, ptr [^)]+\)`).MatchString(leaf.String()) { + t.Fatalf("zero-result coroutine has the wrong physical ABI:\n%s", module.String()) + } +} + +func TestCoroLeafPhysicalABICoroSplit(t *testing.T) { + prog, pkg := compileCoroLeafPhysicalABI(t, nil) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify before CoroSplit: %v\n%s", err, module.String()) + } + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + options.SetVerifyEach(true) + const pipeline = "coro-early,cgscc(coro-split),coro-cleanup" + if err := module.RunPasses(pipeline, prog.TargetMachine(), options); err != nil { + t.Fatalf("run %s: %v\n%s", pipeline, err, module.String()) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify after CoroSplit: %v\n%s", err, module.String()) + } + ir := module.String() + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction("foo.Leaf$coro" + suffix).IsNil() { + t.Fatalf("CoroSplit did not create coroutine %s entry:\n%s", suffix, ir) + } + } + for _, intrinsic := range []string{"llvm.coro.id", "llvm.coro.begin", "llvm.coro.suspend", "llvm.coro.end"} { + if regexp.MustCompile(`call [^\n]*@` + regexp.QuoteMeta(intrinsic) + `\b`).MatchString(ir) { + t.Fatalf("post-split module still calls %s:\n%s", intrinsic, ir) + } + } + if !strings.Contains(module.NamedFunction("foo.Leaf$coro.resume").String(), "store i32") { + t.Fatalf("result-slot store did not move to the resume function:\n%s", ir) + } +} + +func TestCoroLeafPhysicalABIGlobalDebug(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkgWithMode(t, `package foo +func Leaf(value uint32) uint32 { + next := value + 1 + return next +} +`, ssa.SanityCheckFunctions|ssa.InstantiateGenerics|ssa.GlobalDebug) + leafSSA := ssaPkg.Func("Leaf") + foundDebugRef := false + for _, block := range leafSSA.Blocks { + for _, instruction := range block.Instrs { + if _, ok := instruction.(*ssa.DebugRef); ok { + foundDebugRef = true + } + } + } + if !foundDebugRef { + t.Fatal("GlobalDebug SSA did not contain a DebugRef") + } + + oldDebug, oldDebugSyms := enableDbg, enableDbgSyms + EnableDebug(true) + EnableDbgSyms(true) + defer func() { + EnableDebug(oldDebug) + EnableDbgSyms(oldDebugSyms) + }() + prog, pkg := compileCoroLeafPhysicalABIPackage(t, nil, ssaPkg, files) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify debug coroutine before CoroSplit: %v\n%s", err, module.String()) + } + ir := module.String() + if !strings.Contains(ir, "!dbg") { + t.Fatalf("debug coroutine omitted function/parameter metadata:\n%s", ir) + } + parameter := regexp.MustCompile(`(?m)^(!\d+) = !DILocalVariable\(name: "value", arg: 1,`).FindStringSubmatch(ir) + if len(parameter) != 2 { + t.Fatalf("debug coroutine omitted source parameter metadata:\n%s", ir) + } + location := regexp.MustCompile( + `(?m)(?:#dbg_(?:value|declare)|@llvm\.dbg\.(?:value|declare))\([^\n]*` + + regexp.QuoteMeta(parameter[1]) + `(?:,|\))`, + ) + if !location.MatchString(ir) { + t.Fatalf("debug coroutine parameter metadata has no location record:\n%s", ir) + } + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + options.SetVerifyEach(true) + if err := module.RunPasses("coro-early,cgscc(coro-split),coro-cleanup", prog.TargetMachine(), options); err != nil { + t.Fatalf("CoroSplit debug coroutine: %v\n%s", err, module.String()) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify debug coroutine after CoroSplit: %v\n%s", err, module.String()) + } +} + +func TestCoroLeafPhysicalABIUsesTargetPointerWidth(t *testing.T) { + llssa.Initialize(llssa.InitAll) + prog, pkg := compileCoroLeafPhysicalABI(t, &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if got := prog.PointerSize(); got != 4 { + t.Fatalf("wasm pointer size = %d, want 4", got) + } + ir := module.String() + for _, intrinsic := range []string{"size", "align"} { + if !strings.Contains(ir, "@llvm.coro."+intrinsic+".i32") { + t.Fatalf("wasm coroutine uses non-i32 %s intrinsic:\n%s", intrinsic, ir) + } + } + if !regexp.MustCompile(`@__llgo_coro_frame_descriptor_v1\.[0-9a-f]+ = linkonce_odr unnamed_addr constant \{ i32, i32, i64, i64, i32, i32 \}`).MatchString(ir) { + t.Fatalf("wasm descriptor does not use target-width size/alignment fields:\n%s", ir) + } +} + +func TestCoroChildAwaitPhysicalABIV1Presplit(t *testing.T) { + prog, pkg := compileCoroChildAwaitPhysicalABI(t, nil) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify child-await coroutines: %v\n%s", err, module.String()) + } + ir := module.String() + parent := requireCoroPhysicalFunction(t, module, "foo.Parent") + child := requireCoroPhysicalFunction(t, module, "foo.Child") + parentIR, childIR := parent.String(), child.String() + + if got := strings.Count(parentIR, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("Parent coro.suspend calls = %d, want initial + await + final:\n%s", got, parentIR) + } + if got := strings.Count(childIR, "call i8 @llvm.coro.suspend"); got != 2 { + t.Fatalf("Child coro.suspend calls = %d, want initial + final:\n%s", got, childIR) + } + for _, forbidden := range []string{"llvm.coro.resume", "llvm.coro.done", "llvm.coro.destroy"} { + if hasLLVMCall(parentIR, forbidden) { + t.Fatalf("Parent directly owns forbidden %s operation:\n%s", forbidden, parentIR) + } + } + + for _, hook := range []string{ + coroFrameAllocHookV1, + coroFramePublishHookV1, + coroAwaitPrepareHookV1, + coroAwaitConsumeHookV1, + coroPreemptPollHookV1, + coroRunDecisionTakeZeroHookV1, + coroCompletePrepareHookV2, + coroFrameFreeHookV1, + } { + if !strings.Contains(ir, hook) { + t.Fatalf("child-await module is missing PhysicalABIV1 hook %q:\n%s", hook, ir) + } + } + for _, forbidden := range []string{coroFrameAllocHook, coroFrameFreeHook, coroDescriptorPrefix} { + if strings.Contains(ir, forbidden) { + t.Fatalf("PhysicalABIV1 module leaked v0 ABI symbol %q:\n%s", forbidden, ir) + } + } + if got := strings.Count(ir, "call ptr @"+coroFrameAllocHookV1); got != 2 { + t.Fatalf("task-aware v1 frame allocations = %d, want Parent + Child:\n%s", got, ir) + } + if got := strings.Count(ir, "call void @"+coroFramePublishHookV1); got != 2 { + t.Fatalf("v1 frame publications = %d, want Parent + Child:\n%s", got, ir) + } + if got := strings.Count(ir, "call void @"+coroAwaitPrepareHookV1); got != 1 { + t.Fatalf("v1 await preparations = %d, want one Parent->Child handoff:\n%s", got, ir) + } + if got := strings.Count(ir, "call i32 @"+coroAwaitConsumeHookV1); got != 2 { + t.Fatalf("v1 await outcome consume sites = %d, want normal/cancellation reconciliation:\n%s", got, ir) + } + if got := strings.Count(ir, "call void @"+coroCompletePrepareHookV2); got != 2 { + t.Fatalf("v1 completion preparations = %d, want Parent + Child:\n%s", got, ir) + } + if got := strings.Count(ir, "call void @"+coroFrameFreeHookV1); got != 2 { + t.Fatalf("task-aware v1 frame frees = %d, want Parent + Child:\n%s", got, ir) + } + for name, body := range map[string]string{"Parent": parentIR, "Child": childIR} { + assertCoroV1TaskAwareFrameCalls(t, name, body, prog.PointerSize()*8) + assertCoroV1InitialPublish(t, name, body) + wantRunDecisions := 1 + if name == "Parent" { + wantRunDecisions = 2 + } + assertCoroScalarRunDecisionCalls(t, name, body, wantRunDecisions) + assertCoroV1InitialRunDecision(t, name, body) + assertCoroV1Completion(t, name, body) + } + assertCoroStaticChildAwait(t, parentIR) + + descriptor := regexp.MustCompile( + `@__llgo_coro_frame_descriptor_v1\.[0-9a-f]+ = linkonce_odr unnamed_addr constant \{ [^}]+ \} \{ i32 1,`, + ) + if got := len(descriptor.FindAllString(ir, -1)); got != 2 { + t.Fatalf("PhysicalABIV1 descriptors = %d, want Parent + Child:\n%s", got, ir) + } + for _, forbidden := range []string{"@malloc", "@free(", "stacksave", "stackrestore", "pthread"} { + if strings.Contains(ir, forbidden) { + t.Fatalf("child-await lowering introduced forbidden stack/runtime coupling %q:\n%s", forbidden, ir) + } + } +} + +func TestCoroChildAwaitPhysicalABIV1CoroSplit(t *testing.T) { + prog, pkg := compileCoroChildAwaitPhysicalABI(t, nil) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + runCoroABITestPipeline(t, prog, module) + ir := module.String() + for _, function := range []string{"foo.Parent$coro", "foo.Child$coro"} { + if module.NamedFunction(function).IsNil() { + t.Fatalf("post-split module lost ramp %q:\n%s", function, ir) + } + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(function + suffix).IsNil() { + t.Fatalf("CoroSplit did not create %s%s:\n%s", function, suffix, ir) + } + } + } + for _, intrinsic := range []string{ + "llvm.coro.id", "llvm.coro.begin", "llvm.coro.suspend", "llvm.coro.end", + "llvm.coro.resume", "llvm.coro.done", "llvm.coro.destroy", + } { + if hasLLVMCall(ir, intrinsic) { + t.Fatalf("post-split module still calls %s:\n%s", intrinsic, ir) + } + } + assertCoroRunDecisionResumeOnly(t, module, "foo.Parent$coro", 2) + assertCoroRunDecisionResumeOnly(t, module, "foo.Child$coro", 1) + parentResume := module.NamedFunction("foo.Parent$coro.resume").String() + if !regexp.MustCompile(`call ptr @"?foo\.Child\$coro"?\(`).MatchString(parentResume) { + t.Fatalf("Parent resume entry lost the static child ramp call:\n%s", parentResume) + } + for _, hook := range []string{coroAwaitPrepareHookV1, coroCompletePrepareHookV2} { + if !strings.Contains(parentResume, "call void @"+hook) { + t.Fatalf("Parent resume entry lost %s:\n%s", hook, parentResume) + } + } + if !strings.Contains(parentResume, "call i32 @"+coroAwaitConsumeHookV1) { + t.Fatalf("Parent resume entry lost %s:\n%s", coroAwaitConsumeHookV1, parentResume) + } + for _, forbidden := range []string{"llvm.coro.resume", "llvm.coro.done", "llvm.coro.destroy"} { + if hasLLVMCall(parentResume, forbidden) { + t.Fatalf("post-split Parent directly calls forbidden %s:\n%s", forbidden, parentResume) + } + } + for _, function := range []string{"foo.Parent$coro", "foo.Child$coro"} { + ramp := module.NamedFunction(function).String() + if !strings.Contains(ramp, "call void @"+coroFramePublishHookV1) { + t.Fatalf("%s ramp lost frame publication:\n%s", function, ramp) + } + destroy := module.NamedFunction(function + ".destroy").String() + if !strings.Contains(destroy, "call void @"+coroFrameFreeHookV1) { + t.Fatalf("%s destroy entry lost task-aware frame free:\n%s", function, destroy) + } + } +} + +func TestCoroScalarRunDecisionDoesNotGrowFrameNativeAndWasm(t *testing.T) { + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + baseline := compileCoroDecisionFrameProbe(t, test.target, false) + withScalarGate := compileCoroDecisionFrameProbe(t, test.target, true) + if withScalarGate != baseline { + t.Fatalf("scalar run-decision frame size = %d, want gate-off baseline %d", withScalarGate, baseline) + } + }) + } +} + +func TestCoroPreemptiveLoopPhysicalABIV1(t *testing.T) { + const source = `package foo +func Loop(limit uint32) uint32 { + var value uint32 + for value < limit { + value++ + } + return value +} +` + prog, ssaPkg, files, universe, plan := prepareCoroPreemptTestPlan( + t, + source, + []coroRootFactoryTestRoot{{name: "Loop", demand: coro.AsyncDemand}}, + nil, + -1, + ) + defer prog.Dispose() + loop := ssaPkg.Func("Loop") + loopPlan, ok := plan.FunctionPlan(loop) + if !ok || loopPlan.Emission != coro.EmitCoroutine || loopPlan.FuncRep != coro.DirectCoro || + !loopPlan.Exec.Contains(coro.NeedsPreempt) || !loopPlan.Effect.Contains(coro.YieldOnly) { + t.Fatalf("Loop plan = %+v, present=%t; want direct needs-preempt coroutine", loopPlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + body := requireCoroPhysicalFunction(t, module, "foo.Loop").String() + if !strings.Contains(body, "call i1 @"+coroPreemptPollHookV1) { + t.Fatalf("Loop lacks compiler-inserted preemption poll:\n%s", body) + } + if !strings.Contains(body, "call void @"+coroYieldPrepareHookV1) { + t.Fatalf("Loop lacks compiler-inserted scheduler yield handoff:\n%s", body) + } + if !regexp.MustCompile(`(?s)store i16 3,.*store i16 3,.*call void @` + regexp.QuoteMeta(coroYieldPrepareHookV1)).MatchString(body) { + t.Fatalf("Loop does not publish Yield/Suspended before its handoff:\n%s", body) + } + poll := strings.Index(body, "call i1 @"+coroPreemptPollHookV1) + handoff := strings.Index(body, "call void @"+coroYieldPrepareHookV1) + if poll < 0 || handoff < 0 || poll >= handoff || !strings.Contains(body[poll:handoff], "br i1") { + t.Fatalf("Loop yield handoff is not guarded by its preemption poll:\n%s", body) + } + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got < 3 { + t.Fatalf("Loop coroutine suspends = %d, want initial + yield + final:\n%s", got, body) + } + polls := strings.Count(body, "call i1 @"+coroPreemptPollHookV1) + assertCoroScalarRunDecisionCalls(t, "Loop", body, polls+1) + initialDecision := strings.Index(body, "call i32 @"+coroRunDecisionTakeZeroHookV1) + yieldSuspend := strings.Index(body[handoff:], "call i8 @llvm.coro.suspend") + if yieldSuspend < 0 { + t.Fatalf("Loop yield handoff has no suspend:\n%s", body) + } + yieldSuspend += handoff + yieldDecision := strings.Index(body[yieldSuspend:], "call i32 @"+coroRunDecisionTakeZeroHookV1) + if yieldDecision < 0 { + t.Fatalf("Loop resumed yield edge has no decision gate:\n%s", body) + } + yieldDecision += yieldSuspend + if initialDecision < 0 || yieldDecision <= yieldSuspend { + t.Fatalf("Loop decision gates are not on initial/resumed paths:\n%s", body) + } + runCoroABITestPipeline(t, prog, module) + post := module.String() + for _, suffix := range []string{".resume", ".destroy"} { + if fn := module.NamedFunction("foo.Loop$coro" + suffix); fn.IsNil() { + t.Fatalf("CoroSplit did not create Loop%s:\n%s", suffix, post) + } + } + assertCoroRunDecisionResumeOnly(t, module, "foo.Loop$coro", polls+1) +} + +func TestCoroProgramInitPhysicalABIV2(t *testing.T) { + const source = `package foo +import ( + "embed" + _ "unsafe" +) + +var State uint32 +var Files embed.FS + +func Plain() { State = 1 } +func Yield() { State = 2 } +func init() { + Plain() + Yield() +} +` + prog, ssaPkg, files, universe, plan := prepareCoroProgramInitTestPlan(t, source) + defer prog.Dispose() + packageInit := ssaPkg.Func("init") + initPlan, ok := plan.FunctionPlan(packageInit) + if !ok || initPlan.Emission != coro.EmitCoroutine || initPlan.FuncRep != coro.DirectCoro || initPlan.Demand != coro.AsyncDemand { + t.Fatalf("package init plan = %+v, present=%t; want async-only direct coroutine", initPlan, ok) + } + foundElidedUnsafeInit := false + for _, block := range packageInit.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Call.StaticCallee() == nil || call.Call.StaticCallee().Pkg == nil || + call.Call.StaticCallee().Pkg.Pkg.Path() != "unsafe" || call.Call.StaticCallee().Name() != "init" { + continue + } + foundElidedUnsafeInit = plan.ElidesCall(call) + if _, planned := plan.CallPlan(call); planned { + t.Fatal("frontend-elided unsafe.init unexpectedly has a CallPlan") + } + } + } + if !foundElidedUnsafeInit { + t.Fatal("package init fixture has no exact frontend-elided unsafe.init call") + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + embedMap := goembed.VarMap{ + "Files": {Files: []goembed.FileData{{Name: "asset.txt", Data: []byte("payload")}}}, + } + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, embedMap, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify coroutine package init: %v\n%s", err, module.String()) + } + + packageInitIR := requireCoroPhysicalFunction(t, module, "foo.init").String() + if !strings.Contains(packageInitIR, `load i1, ptr @"foo.init$guard"`) || + !strings.Contains(packageInitIR, `store i1 true, ptr @"foo.init$guard"`) { + t.Fatalf("package init coroutine lost its canonical guard load/store:\n%s", packageInitIR) + } + if !strings.Contains(module.String(), "asset.txt") || !strings.Contains(module.String(), "payload") { + t.Fatalf("package init coroutine did not apply compiler-generated embed initialization:\n%s", packageInitIR) + } + if !regexp.MustCompile(`call void @"?embed\.init"?\(`).MatchString(packageInitIR) { + t.Fatalf("package init lost its exact known-external no-suspend call:\n%s", packageInitIR) + } + declaredInit := requireCoroPhysicalFunction(t, module, "foo.init#1").String() + if !regexp.MustCompile(`call void @"?foo\.Plain"?\(`).MatchString(declaredInit) { + t.Fatalf("declared init lost its exact direct plain call:\n%s", declaredInit) + } + if !regexp.MustCompile(`call ptr @"?foo\.Yield\$coro"?\(`).MatchString(declaredInit) || + !strings.Contains(declaredInit, "call void @"+coroAwaitPrepareHookV1) { + t.Fatalf("declared init lost its static child await:\n%s", declaredInit) + } + runCoroABITestPipeline(t, prog, module) +} + +func TestCoroPhysicalValueTransportABIV1NativeAndWasm(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + pointerBits int + uintptrIR string + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}, pointerBits: 32, uintptrIR: "i32"}, + } { + t.Run(test.name, func(t *testing.T) { + prog, ssaPkg, files, universe, plan := prepareCoroPhysicalValueTransportABI(t, test.target) + defer prog.Dispose() + pointerBits := prog.PointerSize() * 8 + if test.pointerBits != 0 && pointerBits != test.pointerBits { + t.Fatalf("pointer width = %d, want %d", pointerBits, test.pointerBits) + } + uintptrIR := test.uintptrIR + if uintptrIR == "" { + uintptrIR = "i" + strconv.Itoa(pointerBits) + } + + child := ssaPkg.Func("Child") + callbackPlan, ok := plan.ValuePlan(child.Params[0]) + if !ok || len(callbackPlan.Funcs) != 1 || len(callbackPlan.Funcs[0].Path) != 0 || + callbackPlan.Funcs[0].Rep != coro.Dispatch { + t.Fatalf("Child callback ValuePlan = %+v, present=%t; want one canonical scalar Dispatch leaf", callbackPlan, ok) + } + parent := ssaPkg.Func("Parent") + var childCall *ssa.Call + for _, block := range parent.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Call.StaticCallee() == child { + childCall = call + } + } + } + if childCall == nil { + t.Fatal("Parent has no static Child call") + } + nilCallbackPlan, ok := plan.ValuePlan(childCall.Call.Args[0]) + if !ok || len(nilCallbackPlan.Funcs) != 1 || len(nilCallbackPlan.Funcs[0].Path) != 0 || + nilCallbackPlan.Funcs[0].Rep != coro.Dispatch || !nilCallbackPlan.Funcs[0].MayBeNil || + len(nilCallbackPlan.Funcs[0].Targets) != 0 { + t.Fatalf("nil callback ValuePlan = %+v, present=%t; want closed nil canonical Dispatch leaf", nilCallbackPlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify physical value transport before CoroSplit: %v\n%s", err, module.String()) + } + + childIR := requireCoroPhysicalFunction(t, module, "foo.Child").String() + parentIR := requireCoroPhysicalFunction(t, module, "foo.Parent").String() + pairIR := requireCoroPhysicalFunction(t, module, "foo.Pair").String() + if !regexp.MustCompile(`define ptr @"?foo\.Child\$coro"?\(ptr [^,]+, ptr [^,]+, \{ ptr, ptr \} [^,]+, ptr `).MatchString(childIR) { + t.Fatalf("Child callback/pointer parameters do not use LLGo's canonical two-pointer closure layout:\n%s", childIR) + } + if !regexp.MustCompile(`call ptr @"?foo\.Child\$coro"?\([^\n]*\{ ptr, ptr \} zeroinitializer, ptr `).MatchString(parentIR) { + t.Fatalf("Parent did not transport the nil callback through the typed canonical closure argument:\n%s", parentIR) + } + assertCoroResultSlotFields(t, "Pair before CoroSplit", pairIR, uintptrIR) + if !regexp.MustCompile(`store %foo\.Payload [^,]+, ptr `).MatchString(childIR) { + t.Fatalf("Child did not copy the complete named struct result into its typed result slot:\n%s", childIR) + } + + runCoroABITestPipeline(t, prog, module) + post := module.String() + for _, function := range []string{"foo.Child$coro", "foo.Parent$coro", "foo.Pair$coro"} { + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(function + suffix).IsNil() { + t.Fatalf("CoroSplit did not create %s%s:\n%s", function, suffix, post) + } + } + } + assertCoroResultSlotFields(t, "Pair after CoroSplit", module.NamedFunction("foo.Pair$coro.resume").String(), uintptrIR) + if !regexp.MustCompile(`store %foo\.Payload [^,]+, ptr `).MatchString(module.NamedFunction("foo.Child$coro.resume").String()) { + t.Fatalf("Child struct result store did not survive CoroSplit:\n%s", module.NamedFunction("foo.Child$coro.resume").String()) + } + }) + } +} + +func TestCoroAwaitResultReconstruction(t *testing.T) { + prog := newLLSSAProg(t) + defer prog.Dispose() + pkg := prog.NewPackage("awaitresult", "await/result") + module := pkg.Module() + defer module.Dispose() + physical := &context{prog: prog} + pointer := types.NewPointer(types.Typ[types.Uint32]) + for _, test := range []struct { + name string + results *types.Tuple + loads int + inserts int + }{ + {name: "zero", results: types.NewTuple()}, + {name: "one", results: types.NewTuple(types.NewVar(0, nil, "ptr", pointer)), loads: 1}, + {name: "many", results: types.NewTuple( + types.NewVar(0, nil, "ptr", pointer), + types.NewVar(0, nil, "count", types.Typ[types.Uintptr]), + ), loads: 2, inserts: 2}, + } { + resultCount := 0 + if test.results != nil { + resultCount = test.results.Len() + } + fields := make([]*types.Var, resultCount) + for i := range fields { + fields[i] = types.NewField(0, nil, test.results.At(i).Name(), test.results.At(i).Type(), false) + } + name := "await_" + test.name + fn := pkg.NewFunc(name, llssa.NoArgsNoRet, llssa.InGo) + b := fn.MakeBody(1) + slot := b.AllocaT(prog.Type(types.NewStruct(fields, nil), llssa.InGo)) + got := physical.loadCoroAwaitResult(b, slot, test.results) + switch resultCount { + case 0: + if !got.IsNil() { + t.Fatalf("zero-result await value type = %v, want llssa.Nil", got.RawType()) + } + case 1: + if got.IsNil() || !types.Identical(got.RawType(), prog.Type(pointer, llssa.InGo).RawType()) { + t.Fatalf("one-result await value type = %v, want field type %v", got.RawType(), pointer) + } + default: + if got.IsNil() || !types.Identical(got.RawType(), prog.Type(test.results, llssa.InGo).RawType()) { + t.Fatalf("multi-result await value type = %v, want source tuple %v", got.RawType(), test.results) + } + } + b.Return() + b.EndBuild() + b.Dispose() + body := module.NamedFunction(name).String() + if got := strings.Count(body, "load "); got != test.loads { + t.Fatalf("%s await result loads = %d, want %d:\n%s", test.name, got, test.loads, body) + } + if got := strings.Count(body, "insertvalue "); got != test.inserts { + t.Fatalf("%s await result tuple inserts = %d, want %d:\n%s", test.name, got, test.inserts, body) + } + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify await result reconstruction: %v\n%s", err, module.String()) + } +} + +func assertCoroResultSlotFields(t *testing.T, name, body, uintptrIR string) { + t.Helper() + resultType := regexp.QuoteMeta("{ ptr, " + uintptrIR + " }") + for index, storeType := range []string{"ptr", uintptrIR} { + field := regexp.MustCompile( + `(?m)^\s*(%[-a-zA-Z$._0-9]+) = getelementptr inbounds(?: (?:nuw|nusw))* ` + resultType + + `, ptr [^,]+, i32 0, i32 ` + strconv.Itoa(index) + `\s*$`, + ).FindStringSubmatch(body) + if len(field) != 2 || !regexp.MustCompile(`(?m)^\s*store `+storeType+` [^,]+, ptr `+regexp.QuoteMeta(field[1])+`(?:,|\s*$)`).MatchString(body) { + t.Fatalf("%s has no typed store for result field %d (%s):\n%s", name, index, storeType, body) + } + } +} + +func TestCoroStaticPlainCallExecutionConstraints(t *testing.T) { + for _, test := range []struct { + name string + exec coro.ExecFlags + wantErr string + }{ + {name: "thread affine rejected", exec: coro.ThreadAffine, wantErr: "thread-affine"}, + {name: "IRQ unsafe allowed on ordinary G", exec: coro.IRQUnsafe}, + } { + t.Run(test.name, func(t *testing.T) { + const source = `package foo +func Plain() {} +func Root() { Plain() } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root, plain := ssaPkg.Func("Root"), ssaPkg.Func("Plain") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch fn { + case root: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + case plain: + return coro.SSAFunctionPolicy{Exec: test.exec}, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + }, + }) + if err != nil { + t.Fatal(err) + } + var plainCall *ssa.Call + for _, instruction := range root.Blocks[0].Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Call.StaticCallee() == plain { + plainCall = call + break + } + } + if plainCall == nil { + t.Fatal("Root has no static Plain call") + } + _, _, resolveErr := resolveCoroStaticPlainCall(plan, plainCall) + if test.wantErr == "" && resolveErr != nil { + t.Fatalf("ordinary-G direct plain target rejected: %v", resolveErr) + } + if test.wantErr != "" && (resolveErr == nil || !strings.Contains(resolveErr.Error(), test.wantErr)) { + t.Fatalf("direct plain target error = %v, want %q", resolveErr, test.wantErr) + } + if test.wantErr == "" { + rootPlan, ok := plan.FunctionPlan(root) + if !ok { + t.Fatal("Root has no function plan") + } + if err := validateCoroPhysicalABI(root, rootPlan, plan, true, true); err != nil { + t.Fatalf("ordinary-G IRQ-unsafe CFG preflight rejected: %v", err) + } + } + }) + } +} + +func TestCoroStaticPlainCallAcceptsOnlyExactTrustedInlineForeignEdge(t *testing.T) { + const source = `package foo +import _ "unsafe" +//llgo:coro contract foreign.v1 progress=unknown affinity=unknown reentry=unknown memory=unknown inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none inline-memory=borrow-until-return +//go:linkname Foreign C.trusted_inline_physical_probe +func Foreign(int) int +//llgo:coro contract foreign.v1 scope=wrapper progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +func Root(value int) int { return Foreign(value) } +func Outer(value int) int { return Root(value) + 1 } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root, outer, foreign := ssaPkg.Func("Root"), ssaPkg.Func("Outer"), ssaPkg.Func("Foreign") + var foreignCall *ssa.Call + for _, instruction := range root.Blocks[0].Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Call.StaticCallee() == foreign { + foreignCall = call + break + } + } + if foreignCall == nil { + t.Fatal("Root has no static Foreign call") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + foreignCertificate, certified, err := universe.CoroCallableContractCertificate(foreign) + if err != nil || !certified || !foreignCertificate.HasTrustedInlineContract { + t.Fatalf("Foreign callable certificate = %+v, %t, %v", foreignCertificate, certified, err) + } + defaultForeignExec := coro.CallableContractExecConstraints(foreignCertificate.Contract) + if defaultForeignExec != coro.ThreadAffine|coro.OpaqueExec || + coro.CallableContractExecConstraints(foreignCertificate.TrustedInlineContract) != 0 { + t.Fatalf("Foreign contract projections = default:%s selected:%s", defaultForeignExec, coro.CallableContractExecConstraints(foreignCertificate.TrustedInlineContract)) + } + rootCertificate, certified, err := universe.CoroCallableContractCertificate(root) + if err != nil || !certified || rootCertificate.Scope != coro.CallableContractScopeWrapper { + t.Fatalf("Root callable certificate = %+v, %t, %v", rootCertificate, certified, err) + } + config := coro.SSAConfig{ + EmissionUniverse: ssaUniverse, FunctionIDs: functionIDs, MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == foreign { + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: coro.ExternalUnknownForeign, OverrideExternal: true, + Exec: coro.BlockForeign | coro.IRQUnsafe | defaultForeignExec, CallableContractCertificate: foreignCertificate, + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + } + auto, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, config) + if err != nil { + t.Fatal(err) + } + if _, _, err := resolveCoroStaticPlainCall(auto, foreignCall); err == nil { + t.Fatal("ordinary Auto edge to unknown blocking foreign target was accepted inline") + } + config.ClassifyFunction = func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch fn { + case foreign: + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: coro.ExternalUnknownForeign, OverrideExternal: true, + Exec: coro.BlockForeign | coro.IRQUnsafe | defaultForeignExec, CallableContractCertificate: foreignCertificate, + }, nil + case root: + return coro.SSAFunctionPolicy{CallableContractCertificate: rootCertificate}, nil + case outer: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + } + config.ClassifyTrustedInlineCall = universe.CoroTrustedInlineCallCertificate + trusted, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: outer, Demand: coro.AsyncDemand}}, config) + if err != nil { + t.Fatal(err) + } + target, targetPlan, err := resolveCoroStaticPlainCall(trusted, foreignCall) + if err != nil { + t.Fatalf("exact TrustedInline edge rejected: %v", err) + } + if target != foreign || targetPlan.External != coro.ExternalUnknownForeign || + targetPlan.Exec != coro.BlockForeign|coro.IRQUnsafe|coro.ThreadAffine|coro.OpaqueExec { + t.Fatalf("trusted target = %v, %+v", target, targetPlan) + } + outerPlan, ok := trusted.FunctionPlan(outer) + if !ok { + t.Fatal("trusted Outer has no function plan") + } + if err := validateCoroPhysicalABI(outer, outerPlan, trusted, true, true); err != nil { + t.Fatalf("trusted-inline physical preflight rejected: %v", err) + } + compilation := &Compilation{CoroPlan: trusted, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify trusted-inline coroutine: %v\n%s", err, module.String()) + } + rootBody := module.NamedFunction("foo.Root") + if rootBody.IsNil() || !strings.Contains(rootBody.String(), "@trusted_inline_physical_probe") { + t.Fatalf("trusted-inline wrapper does not directly call its exact target:\n%s", module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Outer").String() + if !strings.Contains(body, "@foo.Root") { + t.Fatalf("coroutine caller does not use the bounded plain wrapper:\n%s", body) + } + if strings.Contains(rootBody.String(), "@"+coroWorkerParkHookV1) || strings.Contains(body, "@"+coroWorkerParkHookV1) { + t.Fatalf("trusted-inline path unexpectedly uses worker lowering:\n%s\n%s", rootBody.String(), body) + } + runCoroABITestPipeline(t, prog, module) +} + +func TestCoroPreemptiveStraightLineBudgetPhysicalABIV1(t *testing.T) { + source := "package foo\nfunc Heavy(value uint32) uint32 {\n" + + strings.Repeat("value++\n", 150) + + "return value\n}\n" + prog, ssaPkg, files, universe, plan := prepareCoroPreemptTestPlan( + t, + source, + []coroRootFactoryTestRoot{{name: "Heavy", demand: coro.AsyncDemand}}, + nil, + 16, + ) + defer prog.Dispose() + heavy := ssaPkg.Func("Heavy") + heavyPlan, ok := plan.FunctionPlan(heavy) + if !ok || !heavyPlan.Exec.Contains(coro.NeedsPreempt) || !heavyPlan.Effect.Contains(coro.YieldOnly) { + t.Fatalf("Heavy plan = %+v, present=%t; want instruction-budget preemption", heavyPlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + body := requireCoroPhysicalFunction(t, module, "foo.Heavy").String() + if got := strings.Count(body, "call void @"+coroYieldPrepareHookV1); got < 2 { + t.Fatalf("Heavy compiler yield handoffs = %d, want at least two periodic cuts:\n%s", got, body) + } + runCoroABITestPipeline(t, prog, module) +} + +func TestCoroPreemptiveInstructionBudgetBoundary(t *testing.T) { + source := "package foo\nfunc AtLimit(value uint32) uint32 {\n" + + strings.Repeat("value++\n", coroPreemptInstructionBudget-1) + + "return value\n}\nfunc OverLimit(value uint32) uint32 {\n" + + strings.Repeat("value++\n", coroPreemptInstructionBudget) + + "return value\n}\n" + prog, ssaPkg, files, universe, plan := prepareCoroPreemptTestPlan( + t, + source, + []coroRootFactoryTestRoot{ + {name: "AtLimit", demand: coro.AsyncDemand}, + {name: "OverLimit", demand: coro.AsyncDemand}, + }, + nil, + 16, + ) + defer prog.Dispose() + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if got := strings.Count(requireCoroPhysicalFunction(t, module, "foo.AtLimit").String(), "call i1 @"+coroPreemptPollHookV1); got != 1 { + t.Fatalf("AtLimit preemption polls = %d, want block-zero chain-boundary poll only", got) + } + if got := strings.Count(requireCoroPhysicalFunction(t, module, "foo.OverLimit").String(), "call i1 @"+coroPreemptPollHookV1); got != 2 { + t.Fatalf("OverLimit preemption polls = %d, want block-zero plus one instruction-budget poll", got) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify instruction-budget boundary coroutines: %v\n%s", err, module.String()) + } +} + +func TestCoroChildAwaitPhysicalABIV1Wasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + prog, pkg := compileCoroChildAwaitPhysicalABI(t, &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if got := prog.PointerSize(); got != 4 { + t.Fatalf("wasm pointer size = %d, want 4", got) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify wasm child-await coroutines: %v\n%s", err, module.String()) + } + ir := module.String() + for _, intrinsic := range []string{"size", "align"} { + if !strings.Contains(ir, "@llvm.coro."+intrinsic+".i32") { + t.Fatalf("wasm child-await coroutine uses non-i32 %s intrinsic:\n%s", intrinsic, ir) + } + } + if !regexp.MustCompile( + `@__llgo_coro_frame_descriptor_v1\.[0-9a-f]+ = linkonce_odr unnamed_addr constant \{ i32, i32, i64, i64, i32, i32 \} \{ i32 1, i32 [^,]+, i64 [^,]+, i64 [^,]+, i32 [^,]+, i32 [^}]+ \}`, + ).MatchString(ir) { + t.Fatalf("wasm PhysicalABIV1 descriptor does not use i32 size/alignment fields:\n%s", ir) + } + parentIR := requireCoroPhysicalFunction(t, module, "foo.Parent").String() + assertCoroV1TaskAwareFrameCalls(t, "wasm Parent", parentIR, 32) + if !regexp.MustCompile(`call ptr @llvm\.coro\.promise\(ptr [^,]+, i32 4, i1 false\)`).MatchString(parentIR) { + t.Fatalf("wasm child header lookup does not use wasm32 ABI alignment and from=false:\n%s", parentIR) + } + assertCoroStaticChildAwait(t, parentIR) + runCoroABITestPipeline(t, prog, module) + post := module.String() + for _, function := range []string{"foo.Parent$coro", "foo.Child$coro"} { + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(function + suffix).IsNil() { + t.Fatalf("wasm CoroSplit did not create %s%s:\n%s", function, suffix, post) + } + } + } + assertCoroRunDecisionResumeOnly(t, module, "foo.Parent$coro", 2) + assertCoroRunDecisionResumeOnly(t, module, "foo.Child$coro", 1) +} + +func TestCoroStacklessProfileFailsClosed(t *testing.T) { + for _, test := range []struct { + name string + compilation *Compilation + want string + }{ + { + name: "unknown profile", + compilation: &Compilation{CoroProfile: coro.RuntimeProfile(255)}, + want: "unknown coroutine runtime profile", + }, + { + name: "capabilities without profile", + compilation: &Compilation{ + CoroTargetCapabilities: CoroNativeTargetCapabilities(), + }, + want: "target capabilities require the stackless runtime profile", + }, + { + name: "native fleet without worker", + compilation: &Compilation{ + CoroProfile: CoroProfileStackless, + CoroTargetCapabilities: coro.TargetCapabilities(2), + }, + want: "invalid coroutine target capability set", + }, + } { + t.Run(test.name, func(t *testing.T) { + err := test.compilation.preflightCoroPlan() + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("profile validation error = %v, want substring %q", err, test.want) + } + }) + } +} + +func TestCoroExplicitAsyncRootFactoryV1Presplit(t *testing.T) { + prog, pkg := compileCoroChildAwaitPhysicalABI(t, nil) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify explicit async root factory: %v\n%s", err, module.String()) + } + ir := module.String() + parent := requireCoroPhysicalFunction(t, module, "foo.Parent") + child := requireCoroPhysicalFunction(t, module, "foo.Child") + hash, factory := requireSingleCoroRootFactoryV1(t, module) + parentHash := requireCoroFrameDescriptorHash(t, "Parent", parent.String()) + childHash := requireCoroFrameDescriptorHash(t, "Child", child.String()) + if hash != parentHash { + t.Fatalf("root factory hash = %s, want explicit Parent frame hash %s", hash, parentHash) + } + if childHash == parentHash { + t.Fatalf("propagated Child and explicit Parent unexpectedly share ABI hash %s", childHash) + } + if !module.NamedFunction(coroRootFactoryPrefix+childHash).IsNil() || + strings.Contains(ir, coroRootFactoryDescriptorPrefix+childHash) { + t.Fatalf("propagated AsyncDemand Child incorrectly received a root factory/descriptor:\n%s", ir) + } + assertCoroRootFactoryV1Body(t, factory.String()) + assertCoroRootFactoryV1Descriptor(t, ir, hash, parentHash, prog.PointerSize()*8) + assertCoroRootDescriptorLLVMUsed(t, module, hash) + if got := len(regexp.MustCompile(`define ptr @"?`+regexp.QuoteMeta(coroRootFactoryPrefix)+`[0-9a-f]{32}"?\(`).FindAllString(ir, -1)); got != 1 { + t.Fatalf("root factory definitions = %d, want only explicit Parent:\n%s", got, ir) + } + if got := len(regexp.MustCompile(`@`+regexp.QuoteMeta(coroRootFactoryDescriptorPrefix)+`[0-9a-f]{32} =`).FindAllString(ir, -1)); got != 1 { + t.Fatalf("root factory descriptors = %d, want only explicit Parent:\n%s", got, ir) + } +} + +func TestCoroRootPackageAnchorV1CanonicalRegistry(t *testing.T) { + const source = `package foo +func AlphaChild(value uint32) uint32 { return value + 1 } +func Alpha(value uint32) uint32 { return AlphaChild(value) + 1 } +func ZebraChild(value uint32) uint32 { return value + 2 } +func Zebra(value uint32) uint32 { return ZebraChild(value) + 1 } +` + prog, ssaPkg, files, universe, plan := prepareCoroRootFactoryTestPlan( + t, source, + []coroRootFactoryTestRoot{ + {name: "Zebra", demand: coro.AsyncDemand}, + {name: "Alpha", demand: coro.AsyncDemand}, + }, + []string{"AlphaChild", "ZebraChild"}, + ) + defer prog.Dispose() + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify root package anchor: %v\n%s", err, module.String()) + } + + anchor := requireSingleCoroRootPackageAnchorV1(t, module) + if got := pkg.CoroRootPackageAnchor(); got != anchor.Name() { + t.Fatalf("package anchor = %q, want %q", got, anchor.Name()) + } + initializer := anchor.Initializer() + if initializer.IsAConstantStruct().IsNil() || initializer.OperandsCount() != 6 { + t.Fatalf("anchor initializer is not a six-field constant struct: %v", initializer) + } + if got := initializer.Operand(0).ZExtValue(); got != uint64(coroRootPackageAnchorVersionV1) { + t.Fatalf("anchor version = %d, want %d", got, coroRootPackageAnchorVersionV1) + } + if got := initializer.Operand(4).ZExtValue(); got != 2 { + t.Fatalf("anchor descriptor count = %d, want 2", got) + } + suffix := strings.TrimPrefix(anchor.Name(), coroRootPackageAnchorPrefix) + decoded, err := hex.DecodeString(suffix) + if err != nil || len(decoded) != 16 { + t.Fatalf("anchor suffix %q is not a 128-bit hex ABI hash: %v", suffix, err) + } + if got, want := initializer.Operand(2).ZExtValue(), binary.BigEndian.Uint64(decoded[:8]); got != want { + t.Fatalf("anchor hashLo = %#x, want symbol hash %#x", got, want) + } + if got, want := initializer.Operand(3).ZExtValue(), binary.BigEndian.Uint64(decoded[8:]); got != want { + t.Fatalf("anchor hashHi = %#x, want symbol hash %#x", got, want) + } + + entries := module.NamedGlobal(anchor.Name() + ".entries") + if entries.IsNil() || entries.Initializer().IsAConstantArray().IsNil() { + t.Fatalf("anchor entries array is absent or non-constant:\n%s", module.String()) + } + entryValues := entries.Initializer() + rootPlans := plan.Roots() + if len(rootPlans) != 2 || entryValues.OperandsCount() != len(rootPlans) { + t.Fatalf("root plans/entries = %d/%d, want 2/2", len(rootPlans), entryValues.OperandsCount()) + } + for i, root := range rootPlans { + function := module.NamedFunction("foo." + root.Function.Name() + coroPrimarySuffix) + if function.IsNil() { + t.Fatalf("root coroutine %q is absent:\n%s", root.ID, module.String()) + } + hash := requireCoroFrameDescriptorHash(t, root.Function.Name(), function.String()) + want := coroRootFactoryDescriptorPrefix + hash + if got := stripCoroRootPackageConstantPointer(entryValues.Operand(i)).Name(); got != want { + t.Fatalf("anchor entries[%d] = %q, want FunctionID-ordered root %q descriptor %q", i, got, root.ID, want) + } + } + for _, name := range []string{"AlphaChild", "ZebraChild"} { + child := module.NamedFunction("foo." + name + coroPrimarySuffix) + if child.IsNil() { + t.Fatalf("propagated coroutine %q is absent", name) + } + hash := requireCoroFrameDescriptorHash(t, name, child.String()) + if !module.NamedGlobal(coroRootFactoryDescriptorPrefix+hash).IsNil() || + !module.NamedFunction(coroRootFactoryPrefix+hash).IsNil() { + t.Fatalf("propagated async function %q received a root factory/descriptor:\n%s", name, module.String()) + } + } + assertCoroRootPackageAnchorLLVMUsed(t, module, anchor) +} + +func TestCoroRootPackageAnchorV1AbsentWithoutExplicitRoots(t *testing.T) { + prog, ssaPkg, files, universe, plan := prepareCoroRootFactoryTestPlan( + t, `package foo; func Plain(value uint32) uint32 { return value + 1 }`, nil, nil, + ) + defer prog.Dispose() + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if got := pkg.CoroRootPackageAnchor(); got != "" { + t.Fatalf("rootless package anchor = %q, want none", got) + } + if anchors := coroRootPackageAnchorsV1(module); len(anchors) != 0 { + t.Fatalf("rootless package emitted %d anchor(s):\n%s", len(anchors), module.String()) + } + if strings.Contains(module.String(), coroRootPackageAnchorPrefix) { + t.Fatalf("rootless package IR contains a root anchor marker:\n%s", module.String()) + } +} + +func TestCoroRootPackageAnchorV1StableAcrossCacheRegistration(t *testing.T) { + compile := func(cacheHit bool, planDigest, source string) string { + t.Helper() + prog, ssaPkg, files, universe, plan := prepareCoroRootFactoryTestPlan( + t, source, + []coroRootFactoryTestRoot{{name: "Root", demand: coro.AsyncDemand}}, + []string{"Root"}, + ) + compilation := &Compilation{ + CoroPlan: plan, + CoroPlanDigest: planDigest, + EmissionUniverse: universe, + } + enableCoroChildAwaitCompilation(compilation) + installCoroLoweringFactsForTest(t, compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation, CacheHit: cacheHit}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + module := pkg.Module() + name := requireSingleCoroRootPackageAnchorV1(t, module).Name() + module.Dispose() + prog.Dispose() + return name + } + + const rootUint32 = `package foo; func Root(value uint32) uint32 { return value + 1 }` + const digest = "0000000000000000000000000000000000000000000000000000000000000000" + sourceAnchor := compile(false, digest, rootUint32) + cached := compile(true, digest, rootUint32) + if cached != sourceAnchor { + t.Fatalf("cache registration anchor = %q, source anchor = %q", cached, sourceAnchor) + } + fallbackA := compile(false, "", rootUint32) + fallbackB := compile(false, "", rootUint32) + if fallbackA != fallbackB { + t.Fatalf("digest-free direct compilation anchors are unstable: %q != %q", fallbackA, fallbackB) + } + const otherDigest = "1111111111111111111111111111111111111111111111111111111111111111" + if other := compile(false, otherDigest, rootUint32); other == sourceAnchor { + t.Fatalf("anchor %q did not include the canonical plan digest", other) + } + const rootUint64 = `package foo; func Root(value uint64) uint64 { return value + 1 }` + if changedABI := compile(false, digest, rootUint64); changedABI == sourceAnchor { + t.Fatalf("anchor %q did not include the root factory descriptor ABI hash", changedABI) + } +} + +func TestCoroExplicitAsyncRootFactoryV1CoroSplit(t *testing.T) { + prog, pkg := compileCoroChildAwaitPhysicalABI(t, nil) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + hash, _ := requireSingleCoroRootFactoryV1(t, module) + runCoroABITestPipeline(t, prog, module) + ir := module.String() + factoryName := coroRootFactoryPrefix + hash + factory := module.NamedFunction(factoryName) + if factory.IsNil() { + t.Fatalf("CoroSplit lost explicit root factory %q:\n%s", factoryName, ir) + } + assertCoroRootFactoryV1Body(t, factory.String()) + if !module.NamedFunction(factoryName+".resume").IsNil() || + !module.NamedFunction(factoryName+".destroy").IsNil() { + t.Fatalf("non-coroutine root factory was cloned into resume/destroy entries:\n%s", ir) + } + for _, function := range []string{"foo.Parent$coro", "foo.Child$coro"} { + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(function + suffix).IsNil() { + t.Fatalf("CoroSplit did not create %s%s:\n%s", function, suffix, ir) + } + } + } + if !strings.Contains(ir, coroRootFactoryDescriptorPrefix+hash) { + t.Fatalf("CoroSplit lost explicit root descriptor %q:\n%s", coroRootFactoryDescriptorPrefix+hash, ir) + } + assertCoroRootDescriptorLLVMUsed(t, module, hash) + runCoroABIGlobalDCE(t, prog, module) + if module.NamedGlobal(coroRootFactoryDescriptorPrefix+hash).IsNil() || + module.NamedFunction(factoryName).IsNil() { + t.Fatalf("GlobalDCE lost linker-retained root descriptor/factory:\n%s", module.String()) + } + assertCoroRootDescriptorLLVMUsed(t, module, hash) + assertCoroRootObjectRetained(t, prog, module, hash) +} + +func TestCoroExplicitAsyncRootFactoryV1Wasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + prog, pkg := compileCoroChildAwaitPhysicalABI(t, &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if got := prog.PointerSize(); got != 4 { + t.Fatalf("wasm pointer size = %d, want 4", got) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify wasm explicit async root factory: %v\n%s", err, module.String()) + } + ir := module.String() + hash, factory := requireSingleCoroRootFactoryV1(t, module) + parentHash := requireCoroFrameDescriptorHash(t, "wasm Parent", requireCoroPhysicalFunction(t, module, "foo.Parent").String()) + childHash := requireCoroFrameDescriptorHash(t, "wasm Child", requireCoroPhysicalFunction(t, module, "foo.Child").String()) + if hash != parentHash || childHash == hash { + t.Fatalf("wasm root hashes: factory=%s Parent=%s Child=%s", hash, parentHash, childHash) + } + assertCoroRootFactoryV1Body(t, factory.String()) + assertCoroRootFactoryV1Descriptor(t, ir, hash, parentHash, 32) + assertCoroRootDescriptorLLVMUsed(t, module, hash) + if !module.NamedFunction(coroRootFactoryPrefix+childHash).IsNil() || + strings.Contains(ir, coroRootFactoryDescriptorPrefix+childHash) { + t.Fatalf("wasm propagated Child incorrectly received a root factory/descriptor:\n%s", ir) + } + + runCoroABITestPipeline(t, prog, module) + post := module.String() + factoryName := coroRootFactoryPrefix + hash + postFactory := module.NamedFunction(factoryName) + if postFactory.IsNil() { + t.Fatalf("wasm CoroSplit lost root factory %q:\n%s", factoryName, post) + } + assertCoroRootFactoryV1Body(t, postFactory.String()) + if !module.NamedFunction(factoryName+".resume").IsNil() || + !module.NamedFunction(factoryName+".destroy").IsNil() { + t.Fatalf("wasm root factory was incorrectly coroutine-split:\n%s", post) + } + assertCoroRootDescriptorLLVMUsed(t, module, hash) + for _, function := range []string{"foo.Parent$coro", "foo.Child$coro"} { + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(function + suffix).IsNil() { + t.Fatalf("wasm CoroSplit did not create %s%s:\n%s", function, suffix, post) + } + } + } +} + +func TestCoroExplicitRootFactoryV1FailsClosed(t *testing.T) { + const childAwaitSource = `package foo +func Child(first uint8, second uint32) uint32 { return uint32(first) + second } +func Parent(first uint8, second uint32) uint32 { return Child(first, second) + 1 } +` + for _, test := range []struct { + name string + source string + roots []coroRootFactoryTestRoot + yieldOnly []string + want string + }{ + { + name: "sync explicit coroutine root", + source: childAwaitSource, + roots: []coroRootFactoryTestRoot{{name: "Parent", demand: coro.SyncDemand}}, + yieldOnly: []string{"Child"}, + want: "has synchronous demand without a planned raw plain entry, got root=sync total=sync", + }, + { + name: "both-demand explicit coroutine root", + source: childAwaitSource, + roots: []coroRootFactoryTestRoot{ + {name: "Parent", demand: coro.SyncDemand}, + {name: "Parent", demand: coro.AsyncDemand}, + }, + yieldOnly: []string{"Child"}, + want: "has synchronous demand without a planned raw plain entry, got root=both total=both", + }, + } { + t.Run(test.name, func(t *testing.T) { + prog, ssaPkg, files, universe, plan := prepareCoroRootFactoryTestPlan( + t, test.source, test.roots, test.yieldOnly, + ) + defer prog.Dispose() + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + observerCalls := 0 + compilation.CoroPlanObserver = func(*ssa.Package, *coro.SSAPlan) { observerCalls++ } + got, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("preflight result = %v, %v; want error containing %q", got, err, test.want) + } + if got != nil { + t.Fatal("root-factory preflight failure returned a partial package") + } + if observerCalls != 0 { + t.Fatalf("observer calls = %d, want pre-codegen rejection", observerCalls) + } + }) + } +} + +func TestCoroExplicitPlainRootKeepsSinglePlainBody(t *testing.T) { + const source = `package foo; func Plain(first uint8, second uint32) uint32 { return uint32(first) + second }` + for _, demand := range []coro.Demand{coro.SyncDemand, coro.AsyncDemand, coro.BothDemand} { + t.Run(demand.String(), func(t *testing.T) { + roots := []coroRootFactoryTestRoot{{name: "Plain", demand: demand}} + if demand == coro.BothDemand { + roots = []coroRootFactoryTestRoot{ + {name: "Plain", demand: coro.SyncDemand}, + {name: "Plain", demand: coro.AsyncDemand}, + } + } + prog, ssaPkg, files, universe, plan := prepareCoroRootFactoryTestPlan(t, source, roots, nil) + defer prog.Dispose() + plain := ssaPkg.Func("Plain") + function, ok := plan.FunctionPlan(plain) + if !ok || function.External != coro.Defined || function.Emission != coro.EmitPlain || + function.FuncRep != coro.DirectPlain || function.Demand != demand { + t.Fatalf("plain root plan = %+v, present=%t", function, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + if module.NamedFunction("foo.Plain").IsNil() { + t.Fatalf("plain root body is absent:\n%s", module.String()) + } + if !module.NamedFunction("foo.Plain" + coroPrimarySuffix).IsNil() { + t.Fatalf("plain root incorrectly gained a coroutine body:\n%s", module.String()) + } + if got := pkg.CoroRootPackageAnchor(); got != "" { + t.Fatalf("plain root package anchor = %q, want none", got) + } + if strings.Contains(module.String(), coroRootFactoryPrefix) || + strings.Contains(module.String(), coroRootFactoryDescriptorPrefix) { + t.Fatalf("plain root incorrectly gained a root factory or descriptor:\n%s", module.String()) + } + }) + } +} + +func TestCoroExplicitPlainRootMayUseDescriptorRepresentation(t *testing.T) { + const source = `package foo +var Saved func(uint32) uint32 +func Plain(value uint32) uint32 { + Saved = Plain + return value + 1 +} +` + prog, ssaPkg, files, universe, plan := prepareCoroRootFactoryTestPlan( + t, source, []coroRootFactoryTestRoot{{name: "Plain", demand: coro.SyncDemand}}, nil, + ) + defer prog.Dispose() + plain := ssaPkg.Func("Plain") + function, ok := plan.FunctionPlan(plain) + if !ok || function.Emission != coro.EmitPlain || function.Primary != coro.PrimaryPlain || function.FuncRep != coro.Dispatch { + t.Fatalf("descriptor-backed plain root plan = %+v, present=%t", function, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.FuncRepABI = coro.FuncRepABIV1 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if module.NamedFunction("foo.Plain").IsNil() { + t.Fatalf("descriptor-backed plain root body is absent:\n%s", module.String()) + } + if strings.Contains(module.String(), coroRootFactoryPrefix) || strings.Contains(module.String(), coroRootFactoryDescriptorPrefix) { + t.Fatalf("descriptor-backed plain root incorrectly gained a coroutine root factory:\n%s", module.String()) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify descriptor-backed plain root: %v\n%s", err, module.String()) + } +} + +func TestCoroExplicitPlainAsyncRootAcceptsPropagatedSyncDemand(t *testing.T) { + const source = `package foo +func Plain(value uint32) uint32 { return value + 1 } +func Caller() uint32 { return Plain(41) } +` + prog, ssaPkg, files, universe, plan := prepareCoroRootFactoryTestPlan( + t, source, + []coroRootFactoryTestRoot{ + {name: "Plain", demand: coro.AsyncDemand}, + {name: "Caller", demand: coro.SyncDemand}, + }, + nil, + ) + defer prog.Dispose() + plain := ssaPkg.Func("Plain") + function, ok := plan.FunctionPlan(plain) + if !ok || function.Demand != coro.BothDemand || function.Emission != coro.EmitPlain || + function.FuncRep != coro.DirectPlain { + t.Fatalf("propagated-demand plain root plan = %+v, present=%t", function, ok) + } + roots := plan.Roots() + foundExplicitAsync := false + for _, root := range roots { + if root.Function == plain { + foundExplicitAsync = root.Demand == coro.AsyncDemand + } + } + if !foundExplicitAsync { + t.Fatalf("plain explicit root set = %+v, want async-only root with total both demand", roots) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + if pkg.Module().NamedFunction("foo.Plain").IsNil() || + !pkg.Module().NamedFunction("foo.Plain"+coroPrimarySuffix).IsNil() { + t.Fatalf("propagated-demand plain root did not keep one plain body:\n%s", pkg.Module().String()) + } +} + +func TestCoroPhysicalConsumersAcceptBuiltinInPlainBody(t *testing.T) { + const source = `package foo +func Helper() {} +func Plain(values []int) int { return len(values) } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + plain := ssaPkg.Func("Plain") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: plain, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + var builtinCall ssa.CallInstruction + for _, block := range plain.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if !ok || call.Common() == nil { + continue + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if ok && builtin.Name() == "len" { + builtinCall = call + } + } + } + if builtinCall == nil { + t.Fatal("Plain has no SSA len builtin call") + } + if _, found := plan.CallPlan(builtinCall); found { + t.Fatal("AnalyzeSSA unexpectedly created a CallPlan for len") + } + pkg, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + }, + }) + if err != nil { + t.Fatalf("compile active physical ABI plain builtin: %v", err) + } + if pkg.Module().NamedFunction("foo.Plain").IsNil() { + t.Fatalf("plain builtin body was not emitted:\n%s", pkg.String()) + } + + // The exemption is exact: a non-builtin CallInstruction introduced after + // analysis still has no CallPlan and must remain fail-closed. + helper := ssaPkg.Func("Helper") + plain.Blocks[0].Instrs = append(plain.Blocks[0].Instrs, &ssa.Call{ + Call: ssa.CallCommon{Value: helper}, + }) + err = validateCoroPhysicalConsumers(plan, false) + if err == nil || !strings.Contains(err.Error(), "call has no compilation CallPlan") { + t.Fatalf("non-builtin call without CallPlan error = %v", err) + } +} + +func TestCoroPhysicalABICacheRegistrationPreservesPhysicalMetadata(t *testing.T) { + const source = `package foo +func Leaf(value uint32) uint32 { return value + 1 } +` + compile := func(cacheHit bool) (string, int) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + prog.EnableFuncInfoMetadata(true) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + leaf := ssaPkg.Func("Leaf") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: leaf, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == leaf { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + observerCalls := 0 + compilation := &Compilation{ + CoroPlan: plan, + CoroPlanObserver: func(*ssa.Package, *coro.SSAPlan) { observerCalls++ }, + + CoroPlanDigest: strings.Repeat("0", 64), + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + } + installCoroLoweringFactsForTest(t, compilation) + pkg, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: compilation, + CacheHit: cacheHit, + }) + if err != nil { + t.Fatal(err) + } + return pkg.String(), observerCalls + } + + sourceIR, sourceObserverCalls := compile(false) + if sourceObserverCalls != 1 { + t.Fatalf("source observer calls = %d, want 1", sourceObserverCalls) + } + cachedIR, cachedObserverCalls := compile(true) + if cachedObserverCalls != 0 { + t.Fatalf("cache registration observer calls = %d, want 0", cachedObserverCalls) + } + if cachedIR != sourceIR { + t.Fatalf("cache registration changed plan-aware frontend metadata:\nsource:\n%s\ncached:\n%s", sourceIR, cachedIR) + } + for _, required := range []string{"$coro", "llvm.coro.", coroFrameAllocHookV1, coroFrameFreeHookV1, coroDescriptorPrefixV1} { + if !strings.Contains(cachedIR, required) { + t.Fatalf("cache registration is missing physical coroutine marker %q:\n%s", required, cachedIR) + } + } + if !strings.Contains(cachedIR, `!"foo.Leaf$coro"`) { + t.Fatalf("cache registration funcinfo does not name the archived coroutine symbol:\n%s", cachedIR) + } +} + +func compileCoroLeafPhysicalABI(t *testing.T, target *llssa.Target) (llssa.Program, llssa.Package) { + t.Helper() + return compileCoroLeafPhysicalABISource(t, target, `package foo +func Leaf(value uint32) uint32 { return value + 1 } +`) +} + +func compileCoroLeafPhysicalABISource(t *testing.T, target *llssa.Target, source string) (llssa.Program, llssa.Package) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + return compileCoroLeafPhysicalABIPackage(t, target, ssaPkg, files) +} + +func compileCoroLeafPhysicalABIPackage(t *testing.T, target *llssa.Target, ssaPkg *ssa.Package, files []*ast.File) (llssa.Program, llssa.Package) { + t.Helper() + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + leaf := ssaPkg.Func("Leaf") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: leaf, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == leaf { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + pkg, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg +} + +func compileCoroChildAwaitPhysicalABI(t *testing.T, target *llssa.Target) (llssa.Program, llssa.Package) { + t.Helper() + prog, ssaPkg, files, universe, plan := prepareCoroChildAwaitPhysicalABI(t, target) + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + } + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg +} + +func prepareCoroChildAwaitPhysicalABI(t *testing.T, target *llssa.Target) ( + llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan, +) { + t.Helper() + const source = `package foo +func Child(first uint8, second uint32) uint32 { return uint32(first) + second } +func Parent(first uint8, second uint32) uint32 { return Child(first, second) + 1 } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + parent, child := ssaPkg.Func("Parent"), ssaPkg.Func("Child") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: parent, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == child { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + for name, fn := range map[string]*ssa.Function{"Parent": parent, "Child": child} { + function, ok := plan.FunctionPlan(fn) + if !ok || function.Primary != coro.PrimaryCoroutine || function.FuncRep != coro.DirectCoro || function.Demand != coro.AsyncDemand { + prog.Dispose() + t.Fatalf("%s child-await plan = %+v, present=%t; want async-only direct coroutine", name, function, ok) + } + } + return prog, ssaPkg, files, universe, plan +} + +func prepareCoroPhysicalValueTransportABI(t *testing.T, target *llssa.Target) ( + llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan, +) { + t.Helper() + const source = `package foo + +type Payload struct { + Ptr *uint32 + Count uintptr + Label string + Bytes []byte + Slots [2]uintptr +} + +func Child(callback func(*uint32), ptr *uint32, value Payload) Payload { + return value +} + +func Parent(ptr *uint32, value Payload) Payload { + return Child(nil, ptr, value) +} + +func Pair(ptr *uint32, count uintptr) (*uint32, uintptr) { + return ptr, count +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + parent, child, pair := ssaPkg.Func("Parent"), ssaPkg.Func("Child"), ssaPkg.Func("Pair") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: parent, Demand: coro.AsyncDemand}, + {Function: pair, Demand: coro.AsyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == child || fn == pair { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + for name, fn := range map[string]*ssa.Function{"Parent": parent, "Child": child, "Pair": pair} { + function, ok := plan.FunctionPlan(fn) + if !ok || function.Primary != coro.PrimaryCoroutine || function.FuncRep != coro.DirectCoro || function.Demand != coro.AsyncDemand { + prog.Dispose() + t.Fatalf("%s value-transport plan = %+v, present=%t; want async-only direct coroutine", name, function, ok) + } + } + return prog, ssaPkg, files, universe, plan +} + +func enableCoroChildAwaitCompilation(compilation *Compilation) { + compilation.CoroProfile = CoroProfileStackless + compilation.CoroABI = coro.PhysicalABIV1 + compilation.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 +} + +func enableCoroPreemptCompilation(compilation *Compilation) { + enableCoroChildAwaitCompilation(compilation) +} + +func requireCoroPhysicalFunction(t *testing.T, module llvm.Module, sourceName string) llvm.Value { + t.Helper() + if legacy := module.NamedFunction(sourceName); !legacy.IsNil() { + t.Fatalf("coroutine retained legacy source ABI symbol %q:\n%s", sourceName, module.String()) + } + physical := module.NamedFunction(sourceName + "$coro") + if physical.IsNil() { + t.Fatalf("coroutine physical symbol %q is absent:\n%s", sourceName+"$coro", module.String()) + } + return physical +} + +func assertCoroV1TaskAwareFrameCalls(t *testing.T, name, body string, pointerBits int) { + t.Helper() + integer := "i" + strconv.Itoa(pointerBits) + alloc := regexp.MustCompile( + `call ptr @` + regexp.QuoteMeta(coroFrameAllocHookV1) + + `\(ptr [^,]+, ` + integer + ` [^,]+, ` + integer + ` [^,]+, ptr @__llgo_coro_frame_descriptor_v1\.[0-9a-f]+\)`, + ) + if !alloc.MatchString(body) { + t.Fatalf("%s lacks task-aware v1 frame allocation:\n%s", name, body) + } + free := regexp.MustCompile( + `call void @` + regexp.QuoteMeta(coroFrameFreeHookV1) + + `\(ptr [^,]+, ptr [^,]+, ` + integer + ` [^,]+, ` + integer + ` [^,]+, ptr @__llgo_coro_frame_descriptor_v1\.[0-9a-f]+\)`, + ) + if !free.MatchString(body) { + t.Fatalf("%s lacks task-aware v1 frame free:\n%s", name, body) + } +} + +func assertCoroV0HeaderStateZero(t *testing.T, body string) { + t.Helper() + for _, field := range []struct { + index int + type_ string + name string + }{ + {index: coroHeaderSuspendReason, type_: "i16", name: "suspend reason"}, + {index: coroHeaderLifecycle, type_: "i16", name: "lifecycle"}, + {index: coroHeaderStateID, type_: "i32", name: "state ID"}, + } { + addresses := regexp.MustCompile( + `(?m)^\s*(%[-a-zA-Z$._0-9]+) = getelementptr[^\n{]* \{ ptr, ptr, ptr, ptr, ptr, i16, i16, i32, i32 \}, ptr [^,]+, i32 0, i32 `+strconv.Itoa(field.index)+`\s*$`, + ).FindAllStringSubmatch(body, -1) + if len(addresses) == 0 { + t.Fatalf("v0 coroutine has no header %s store:\n%s", field.name, body) + } + for _, address := range addresses { + store := regexp.MustCompile( + `(?m)^\s*store ` + field.type_ + ` ([^,]+), ptr ` + regexp.QuoteMeta(address[1]) + `(?:,|\s*$)`, + ).FindStringSubmatch(body) + if len(store) != 2 { + t.Fatalf("v0 coroutine header %s address %s has no store:\n%s", field.name, address[1], body) + } + if store[1] != "0" { + t.Fatalf("v0 coroutine header %s = %s, want reserved zero state:\n%s", field.name, store[1], body) + } + } + } +} + +func assertCoroV1InitialPublish(t *testing.T, name, body string) { + t.Helper() + begin := strings.Index(body, "call ptr @llvm.coro.begin") + publish := strings.Index(body, "call void @"+coroFramePublishHookV1) + suspend := strings.Index(body, "call i8 @llvm.coro.suspend") + if begin < 0 || publish < 0 || suspend < 0 || !(begin < publish && publish < suspend) { + t.Fatalf("%s does not publish its v1 frame after coro.begin and before initial suspend:\n%s", name, body) + } + call := regexp.MustCompile( + `call void @` + regexp.QuoteMeta(coroFramePublishHookV1) + `\(ptr [^,]+, ptr [^,]+, ptr [^,]+, ptr [^)]+\)`, + ) + if !call.MatchString(body) { + t.Fatalf("%s frame publication lacks (task, handle, header, storage):\n%s", name, body) + } +} + +func assertCoroScalarRunDecisionCalls(t *testing.T, name, body string, want int) { + t.Helper() + callPrefix := "call i32 @" + coroRunDecisionTakeZeroHookV1 + if got := strings.Count(body, callPrefix); got != want { + t.Fatalf("%s run-decision calls = %d, want %d:\n%s", name, got, want, body) + } + dispatch := regexp.MustCompile( + `(?m)(%[-a-zA-Z$._0-9]+) = call i32 @` + regexp.QuoteMeta(coroRunDecisionTakeZeroHookV1) + + `\(ptr [^)]+\)\n\s+(%[-a-zA-Z$._0-9]+) = icmp ne i32 (%[-a-zA-Z$._0-9]+), 0`, + ) + matches := dispatch.FindAllStringSubmatch(body, -1) + if got := len(matches); got != want { + t.Fatalf("%s scalar zero-ticket dispatches = %d, want %d:\n%s", name, got, want, body) + } + completion := "" + searchOffset := 0 + for _, match := range matches { + if match[1] != match[3] { + t.Fatalf("%s scalar run-decision result does not directly control its branch: %v:\n%s", name, match, body) + } + relative := strings.Index(body[searchOffset:], match[0]) + if relative < 0 { + t.Fatalf("%s scalar run-decision block cannot be located:\n%s", name, body) + } + startOfMatch := searchOffset + relative + searchOffset = startOfMatch + len(match[0]) + rest := body[searchOffset:] + end := len(rest) + if next := regexp.MustCompile(`(?m)^[-a-zA-Z$._0-9]+:`).FindStringIndex(rest); next != nil { + end = next[0] + } + block := body[startOfMatch : searchOffset+end] + branch := regexp.MustCompile( + `(?m)^\s+br i1 ` + regexp.QuoteMeta(match[2]) + + `, label %([-a-zA-Z$._0-9]+), label %[-a-zA-Z$._0-9]+\s*$`, + ).FindStringSubmatch(block) + if len(branch) != 2 { + t.Fatalf("%s scalar run-decision result does not control its block terminator:\n%s", name, block) + } + label := branch[1] + ":" + start := strings.Index(body, "\n"+label) + if start < 0 { + t.Fatalf("%s cancellation target %q is absent:\n%s", name, branch[1], body) + } + start++ + targetRest := body[start+len(label):] + targetEnd := len(targetRest) + if next := regexp.MustCompile(`(?m)^[-a-zA-Z$._0-9]+:`).FindStringIndex(targetRest); next != nil { + targetEnd = next[0] + } + targetBlock := body[start : start+len(label)+targetEnd] + branches := regexp.MustCompile(`(?m)^\s+br label %([-a-zA-Z$._0-9]+)\s*$`).FindAllStringSubmatch(targetBlock, -1) + if len(branches) != 1 { + t.Fatalf("%s cancellation target %q does not unconditionally enter cleanup:\n%s", name, branch[1], targetBlock) + } + if completion == "" { + completion = branches[0][1] + } else if branches[0][1] != completion { + t.Fatalf("%s cancellation gates reach different cleanup entries %s and %s:\n%s", + name, completion, branches[0][1], body) + } + } + if completion == "" { + t.Fatalf("%s has no cancellation cleanup destination:\n%s", name, body) + } +} + +func assertCoroCancellationTerminalStatusPublication(t *testing.T, function llvm.Value) { + t.Helper() + if function.IsNil() { + t.Fatal("cannot inspect cancellation terminal status in a nil function") + } + var terminalPointer llvm.Value + completeCalls := 0 + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || instruction.CalledValue().Name() != coroCompletePrepareHookV2 { + continue + } + completeCalls++ + if got := instruction.OperandsCount() - 1; got != 4 { + t.Fatalf("%s completion arguments = %d, want (g,handle,header,status):\n%s", + function.Name(), got, instruction.String()) + } + status := instruction.Operand(3) + if status.InstructionOpcode() != llvm.Load || status.Type().TypeKind() != llvm.IntegerTypeKind || + status.Type().IntTypeWidth() != 32 { + t.Fatalf("%s completion status is not loaded from frame-local storage:\n%s", function.Name(), instruction.String()) + } + terminalPointer = status.Operand(0) + } + } + if completeCalls != 1 || terminalPointer.IsNil() { + t.Fatalf("%s completion publication calls = %d, want one frame-local status load:\n%s", + function.Name(), completeCalls, function.String()) + } + stores := make(map[uint64]llvm.BasicBlock) + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Store || instruction.Operand(1) != terminalPointer { + continue + } + value := instruction.Operand(0) + if value.Type().TypeKind() != llvm.IntegerTypeKind || value.Type().IntTypeWidth() != 32 || + value.IsAConstantInt().IsNil() { + continue + } + status := value.ZExtValue() + if status == coroAwaitCompletionAbort || status == coroAwaitCompletionShutdown { + stores[status] = block + } + } + } + abort, abortOK := stores[coroAwaitCompletionAbort] + shutdown, shutdownOK := stores[coroAwaitCompletionShutdown] + if !abortOK || !shutdownOK || abort == shutdown { + t.Fatalf("%s lacks distinct frame-local Abort/Shutdown stores:\n%s", function.Name(), function.String()) + } + for status, block := range stores { + terminator := block.LastInstruction() + if terminator.IsNil() || terminator.InstructionOpcode() != llvm.Br || terminator.SuccessorsCount() != 1 || + !coroTestBlockCanReachDirectCall(terminator.Successor(0), coroCompletePrepareHookV2) { + t.Fatalf("%s status %d does not converge on shared cleanup/completion:\n%s", + function.Name(), status, block.AsValue().String()) + } + } +} + +func coroFrameAllocationSize(t *testing.T, ramp llvm.Value, pointerBits int) uint64 { + t.Helper() + if ramp.IsNil() { + t.Fatal("cannot inspect frame allocation of nil coroutine ramp") + } + pattern := regexp.MustCompile( + `call ptr @` + regexp.QuoteMeta(coroFrameAllocHookV1) + + `\(ptr [^,]+, i` + strconv.Itoa(pointerBits) + ` ([0-9]+),`, + ) + match := pattern.FindStringSubmatch(ramp.String()) + if len(match) != 2 { + t.Fatalf("%s has no constant PhysicalABIV1 frame allocation:\n%s", ramp.Name(), ramp.String()) + } + got, err := strconv.ParseUint(match[1], 10, 64) + if err != nil { + t.Fatalf("parse %s frame size %q: %v", ramp.Name(), match[1], err) + } + return got +} + +func compileCoroDecisionFrameProbe(t *testing.T, target *llssa.Target, scalarGate bool) uint64 { + t.Helper() + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + defer prog.Dispose() + pkg := prog.NewPackage("coro_decision_frame_probe", "llgo/test/coro-decision-frame-probe") + defer pkg.Module().Dispose() + ctx := &context{ + prog: prog, + pkg: pkg, + compilation: &Compilation{ + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, CoroProfile: CoroProfileStackless, + }, + } + sourceSignature := types.NewSignatureType(nil, nil, nil, nil, nil, false) + abi := newCoroPhysicalABI(ctx, plannedFunctionSymbol{ + plan: coro.FunctionPlan{ID: "llgo.test.coro-decision-frame-probe"}, + }, sourceSignature) + if !scalarGate { + abi.runDecisionTakeZeroHook = "" + } + const name = "coro_decision_frame_probe$coro" + ctx.fn = pkg.NewFunc(name, abi.physicalSig, llssa.InGo) + b := ctx.fn.MakeBody(1) + defer b.Dispose() + body := ctx.beginCoroBody(b, abi, nil) + body.completion = ctx.fn.MakeBlock() + body.finalSuspend = ctx.fn.MakeBlock() + body.bindCancellationCompletion(b) + b.SetBlock(body.coro.InitialResumeBlock()) + body.activate(b) + b.Jump(body.completion) + b.SetBlock(body.completion) + body.complete(b) + b.SetBlock(body.finalSuspend) + body.finish(b) + b.EndBuild() + module := pkg.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify target=%v scalar-gate=%t probe before CoroSplit: %v\n%s", target, scalarGate, err, module.String()) + } + runCoroABITestPipeline(t, prog, module) + ramp := module.NamedFunction(name) + if scalarGate { + assertCoroRunDecisionResumeOnly(t, module, name, 1) + } else if functionHasReachableDirectCall(module.NamedFunction(name+".resume"), coroRunDecisionTakeZeroHookV1) { + t.Fatalf("gate-off frame probe retained scalar run-decision call:\n%s", module.String()) + } + return coroFrameAllocationSize(t, ramp, prog.PointerSize()*8) +} + +func assertCoroRunDecisionResumeOnly(t *testing.T, module llvm.Module, rampName string, want int) { + t.Helper() + for _, name := range []string{rampName, rampName + ".destroy"} { + function := module.NamedFunction(name) + if function.IsNil() { + t.Fatalf("post-CoroSplit module has no function %q:\n%s", name, module.String()) + } + if functionHasReachableDirectCall(function, coroRunDecisionTakeZeroHookV1) { + t.Fatalf("run-decision gate is reachable outside the resume entry in %s:\n%s", name, function.String()) + } + } + resumeName := rampName + ".resume" + resume := module.NamedFunction(resumeName) + if resume.IsNil() { + t.Fatalf("post-CoroSplit module has no function %q:\n%s", resumeName, module.String()) + } + assertCoroScalarRunDecisionCalls(t, resumeName, resume.String(), want) +} + +// functionHasReachableDirectCall follows only executable CFG edges. LLVM's +// coro-split clones case-0 resume blocks into .destroy, then makes those blocks +// dead by replacing llvm.coro.suspend with the constant destroy result 1. +// Frontend test functions are optnone, so simplifycfg intentionally retains +// that textual dead clone; it must not be mistaken for an executable gate. +func functionHasReachableDirectCall(function llvm.Value, callee string) bool { + entry := function.EntryBasicBlock() + if entry.IsNil() { + return false + } + type cfgEdge struct { + block llvm.BasicBlock + predecessor llvm.BasicBlock + } + type cfgState struct { + cfgEdge + constants map[llvm.Value]uint64 + } + seen := make(map[cfgEdge][]map[llvm.Value]uint64) + pending := []cfgState{{cfgEdge: cfgEdge{block: entry}, constants: make(map[llvm.Value]uint64)}} + for len(pending) != 0 { + state := pending[len(pending)-1] + pending = pending[:len(pending)-1] + alreadySeen := false + for _, constants := range seen[state.cfgEdge] { + if sameCoroCFGConstants(constants, state.constants) { + alreadySeen = true + break + } + } + if alreadySeen { + continue + } + seen[state.cfgEdge] = append(seen[state.cfgEdge], state.constants) + constants := copyCoroCFGConstants(state.constants) + for instruction := state.block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if !instruction.IsAPHINode().IsNil() { + value, ok := coroCFGPHIIncomingConstant(instruction, state.predecessor, constants) + if ok { + constants[instruction] = value + } else { + delete(constants, instruction) + } + } + if (!instruction.IsACallInst().IsNil() || !instruction.IsAInvokeInst().IsNil()) && + instruction.CalledValue().Name() == callee { + return true + } + } + terminator := state.block.LastInstruction() + for _, successor := range executableTerminatorSuccessors(terminator, constants) { + pending = append(pending, cfgState{ + cfgEdge: cfgEdge{block: successor, predecessor: state.block}, + constants: constants, + }) + } + } + return false +} + +func executableTerminatorSuccessors(terminator llvm.Value, constants map[llvm.Value]uint64) []llvm.BasicBlock { + count := terminator.SuccessorsCount() + if count == 0 { + return nil + } + if terminator.InstructionOpcode() == llvm.Br && count == 2 { + if condition, ok := coroCFGConstant(terminator.Operand(0), constants); ok { + if condition != 0 { + return []llvm.BasicBlock{terminator.Successor(0)} + } + return []llvm.BasicBlock{terminator.Successor(1)} + } + } + if terminator.InstructionOpcode() == llvm.Switch { + if condition, ok := coroCFGConstant(terminator.Operand(0), constants); ok { + selected := 0 + for successor := 1; successor < count; successor++ { + if terminator.GetSwitchCaseValue(successor).ZExtValue() == condition { + selected = successor + break + } + } + return []llvm.BasicBlock{terminator.Successor(selected)} + } + } + successors := make([]llvm.BasicBlock, count) + for successor := range successors { + successors[successor] = terminator.Successor(successor) + } + return successors +} + +func coroCFGPHIIncomingConstant( + phi llvm.Value, + predecessor llvm.BasicBlock, + constants map[llvm.Value]uint64, +) (uint64, bool) { + if predecessor.IsNil() { + return 0, false + } + for incoming := 0; incoming < phi.IncomingCount(); incoming++ { + if phi.IncomingBlock(incoming) == predecessor { + return coroCFGConstant(phi.IncomingValue(incoming), constants) + } + } + return 0, false +} + +func coroCFGConstant(value llvm.Value, constants map[llvm.Value]uint64) (uint64, bool) { + if !value.IsAConstantInt().IsNil() { + return value.ZExtValue(), true + } + constant, ok := constants[value] + return constant, ok +} + +func copyCoroCFGConstants(constants map[llvm.Value]uint64) map[llvm.Value]uint64 { + copy := make(map[llvm.Value]uint64, len(constants)) + for value, constant := range constants { + copy[value] = constant + } + return copy +} + +func sameCoroCFGConstants(left, right map[llvm.Value]uint64) bool { + if len(left) != len(right) { + return false + } + for value, constant := range left { + if other, ok := right[value]; !ok || other != constant { + return false + } + } + return true +} + +func assertCoroV1InitialRunDecision(t *testing.T, name, body string) { + t.Helper() + initialSuspend := strings.Index(body, "call i8 @llvm.coro.suspend") + if initialSuspend < 0 { + t.Fatalf("%s initial resume has no initial suspend:\n%s", name, body) + } + decisionRelative := strings.Index(body[initialSuspend:], "call i32 @"+coroRunDecisionTakeZeroHookV1) + if decisionRelative < 0 { + t.Fatalf("%s initial resume has no run-decision gate:\n%s", name, body) + } + decision := initialSuspend + decisionRelative + activate := regexp.MustCompile(`(?s)store i16 0,.*store i16 2,`).FindStringIndex(body[decision:]) + if activate == nil { + t.Fatalf("%s run-decision gate is not before initial frame activation:\n%s", name, body) + } +} + +func assertCoroV1Completion(t *testing.T, name, body string) { + t.Helper() + complete := strings.Index(body, "call void @"+coroCompletePrepareHookV2) + finalSuspend := strings.Index(body, "@llvm.coro.suspend(token none, i1 true)") + if complete < 0 || finalSuspend < 0 || complete >= finalSuspend { + t.Fatalf("%s does not prepare completion before final suspend:\n%s", name, body) + } + segment := body[:complete] + state := regexp.MustCompile(`(?s)store i16 2,.*store i16 4,.*store i32 [1-9][0-9]*,`) + if !state.MatchString(segment) { + t.Fatalf("%s does not publish final reason/lifecycle/stateID before completion preparation:\n%s", name, body) + } +} + +func assertCoroStaticChildAwait(t *testing.T, parent string) { + t.Helper() + childCall := regexp.MustCompile(`call ptr @"?foo\.Child\$coro"?\(`).FindStringIndex(parent) + publish := strings.Index(parent, "call void @"+coroFramePublishHookV1) + initialSuspend := strings.Index(parent, "call i8 @llvm.coro.suspend") + await := strings.Index(parent, "call void @"+coroAwaitPrepareHookV1) + if childCall == nil || publish < 0 || initialSuspend < 0 || await < 0 || + !(publish < initialSuspend && initialSuspend < childCall[0] && childCall[0] < await) { + t.Fatalf("Parent hook order is not frame_publish -> initial suspend -> Child -> await_prepare:\n%s", parent) + } + prefix := parent[childCall[0]:await] + promiseResult := regexp.MustCompile(`(%[-a-zA-Z$._0-9]+) = call ptr @llvm\.coro\.promise\(ptr [^,]+, i32 [0-9]+, i1 false\)`).FindStringSubmatch(prefix) + parentHandle := regexp.MustCompile(`(%[-a-zA-Z$._0-9]+) = call ptr @llvm\.coro\.begin`).FindStringSubmatch(parent) + if len(promiseResult) != 2 || len(parentHandle) != 2 { + t.Fatalf("Parent child-await lacks named child promise or parent handle:\n%s", parent) + } + parentLink := regexp.MustCompile( + `(?s)getelementptr [^\n]+, ptr ` + regexp.QuoteMeta(promiseResult[1]) + + `, i32 0, i32 1\s+store ptr ` + regexp.QuoteMeta(parentHandle[1]) + `,`, + ) + if !parentLink.MatchString(prefix) { + t.Fatalf("Parent does not store its handle into child.parent before handoff:\n%s", prefix) + } + state := regexp.MustCompile(`(?s)store i16 1,.*store i16 3,.*store i32 1,`) + if !state.MatchString(prefix) { + t.Fatalf("Parent does not publish Call/Suspended/stateID=1 before await_prepare:\n%s", prefix) + } + awaitSuspend := strings.Index(parent[await:], "call i8 @llvm.coro.suspend") + if awaitSuspend < 0 { + t.Fatalf("Parent does not suspend after await_prepare:\n%s", parent) + } + awaitSuspend += await + decisionRelative := strings.Index(parent[awaitSuspend:], "call i32 @"+coroRunDecisionTakeZeroHookV1) + if decisionRelative < 0 { + t.Fatalf("Parent does not take its run decision after await resume:\n%s", parent) + } + decision := awaitSuspend + decisionRelative + consumeRelative := strings.Index(parent[decision:], "call i32 @"+coroAwaitConsumeHookV1) + if consumeRelative < 0 { + t.Fatalf("Parent does not consume its child outcome after await resume:\n%s", parent) + } + complete := strings.Index(parent[awaitSuspend:], "call void @"+coroCompletePrepareHookV2) + if complete < 0 { + t.Fatalf("Parent does not complete after its await resume:\n%s", parent) + } + complete += awaitSuspend + resumeContinuation := parent[decision:] + if !regexp.MustCompile(`(?s)call i32 @` + regexp.QuoteMeta(coroRunDecisionTakeZeroHookV1) + + `.*store i16 0,.*store i16 2,.*call i32 @` + regexp.QuoteMeta(coroAwaitConsumeHookV1) + `.*load i32,`).MatchString(resumeContinuation) { + t.Fatalf("Parent await run-decision gate does not precede activation and result continuation:\n%s", parent) + } + completionState := regexp.MustCompile(`(?s)store i16 2,.*store i16 4,.*store i32 2,`) + if !completionState.MatchString(parent[awaitSuspend:complete]) { + t.Fatalf("Parent does not publish FrameComplete/FinalSuspended/stateID=2 after await:\n%s", parent[awaitSuspend:complete]) + } +} + +func runCoroABITestPipeline(t *testing.T, prog llssa.Program, module llvm.Module) { + t.Helper() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify before CoroSplit: %v\n%s", err, module.String()) + } + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + options.SetVerifyEach(true) + const pipeline = "coro-early,cgscc(coro-split),coro-cleanup" + if err := module.RunPasses(pipeline, prog.TargetMachine(), options); err != nil { + t.Fatalf("run %s: %v\n%s", pipeline, err, module.String()) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify after CoroSplit: %v\n%s", err, module.String()) + } +} + +func runCoroABIGlobalDCE(t *testing.T, prog llssa.Program, module llvm.Module) { + t.Helper() + options := llvm.NewPassBuilderOptions() + defer options.Dispose() + options.SetVerifyEach(true) + if err := module.RunPasses("globaldce", prog.TargetMachine(), options); err != nil { + t.Fatalf("run globaldce: %v\n%s", err, module.String()) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify after globaldce: %v\n%s", err, module.String()) + } +} + +type coroRootFactoryTestRoot struct { + name string + demand coro.Demand +} + +func prepareCoroRootFactoryTestPlan( + t *testing.T, + source string, + testRoots []coroRootFactoryTestRoot, + yieldOnly []string, +) (llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan) { + return prepareCoroRootFactoryTestPlanWithMaxPlainInstructions(t, source, testRoots, yieldOnly, -1) +} + +func prepareCoroRootFactoryTestPlanWithMaxPlainInstructions( + t *testing.T, + source string, + testRoots []coroRootFactoryTestRoot, + yieldOnly []string, + maxPlainInstructions int, +) (llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan) { + return prepareCoroRootFactoryTestPlanWithScheduler( + t, source, testRoots, yieldOnly, maxPlainInstructions, coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + ) +} + +func prepareCoroPreemptTestPlan( + t *testing.T, + source string, + testRoots []coroRootFactoryTestRoot, + yieldOnly []string, + maxPlainInstructions int, +) (llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan) { + return prepareCoroRootFactoryTestPlanWithScheduler( + t, source, testRoots, yieldOnly, maxPlainInstructions, coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + ) +} + +func prepareCoroProgramInitTestPlan( + t *testing.T, source string, +) (llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + packageInit := ssaPkg.Func("init") + yield := ssaPkg.Func("Yield") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: packageInit, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch { + case fn == yield: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + case fn.Pkg != nil && fn.Pkg.Pkg.Path() == "embed" && fn.Name() == "init": + // The fixture does not compile the standard embed package, but its + // package initializer is an exact frozen no-suspend external edge. + return coro.SSAFunctionPolicy{ + Effect: coro.NoSuspend, External: coro.ExternalKnown, OverrideExternal: true, + }, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + return callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init", nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, ssaPkg, files, universe, plan +} + +func prepareCoroRootFactoryTestPlanWithScheduler( + t *testing.T, + source string, + testRoots []coroRootFactoryTestRoot, + yieldOnly []string, + maxPlainInstructions int, + schedulerABI string, +) (llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = schedulerABI + functionIDs.ArchiveReady = true + roots := make(coro.Roots, len(testRoots)) + for i, root := range testRoots { + fn := ssaPkg.Func(root.name) + if fn == nil { + prog.Dispose() + t.Fatalf("test root %q is absent", root.name) + } + roots[i] = coro.Root{Function: fn, Demand: root.demand} + } + yieldSet := make(map[*ssa.Function]bool, len(yieldOnly)) + for _, name := range yieldOnly { + fn := ssaPkg.Func(name) + if fn == nil { + prog.Dispose() + t.Fatalf("yield-only function %q is absent", name) + } + yieldSet[fn] = true + } + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: maxPlainInstructions, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if yieldSet[fn] { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, ssaPkg, files, universe, plan +} + +func requireSingleCoroRootFactoryV1(t *testing.T, module llvm.Module) (string, llvm.Value) { + t.Helper() + ir := module.String() + pattern := regexp.MustCompile( + `(?m)^define ptr @"?` + regexp.QuoteMeta(coroRootFactoryPrefix) + `([0-9a-f]{32})"?\(ptr [^,]+, ptr [^,]+, ptr [^)]+\)`, + ) + matches := pattern.FindAllStringSubmatch(ir, -1) + if len(matches) != 1 { + t.Fatalf("root factory definitions = %d, want exactly one explicit factory:\n%s", len(matches), ir) + } + hash := matches[0][1] + factory := module.NamedFunction(coroRootFactoryPrefix + hash) + if factory.IsNil() { + t.Fatalf("root factory %q is absent despite its definition:\n%s", coroRootFactoryPrefix+hash, ir) + } + return hash, factory +} + +func coroRootPackageAnchorsV1(module llvm.Module) []llvm.Value { + var anchors []llvm.Value + for global := module.FirstGlobal(); !global.IsNil(); global = llvm.NextGlobal(global) { + if strings.HasPrefix(global.Name(), coroRootPackageAnchorPrefix) && + !strings.HasSuffix(global.Name(), ".entries") { + anchors = append(anchors, global) + } + } + return anchors +} + +func requireSingleCoroRootPackageAnchorV1(t *testing.T, module llvm.Module) llvm.Value { + t.Helper() + anchors := coroRootPackageAnchorsV1(module) + if len(anchors) != 1 { + t.Fatalf("root package anchors = %d, want exactly one:\n%s", len(anchors), module.String()) + } + anchor := anchors[0] + if !anchor.IsGlobalConstant() || anchor.Linkage() != llvm.ExternalLinkage || + anchor.Visibility() != llvm.HiddenVisibility { + t.Fatalf("root package anchor is not an external hidden constant: %v", anchor) + } + return anchor +} + +func stripCoroRootPackageConstantPointer(value llvm.Value) llvm.Value { + for !value.IsAConstantExpr().IsNil() && value.OperandsCount() == 1 { + value = value.Operand(0) + } + return value +} + +func assertCoroRootPackageAnchorLLVMUsed(t *testing.T, module llvm.Module, anchor llvm.Value) { + t.Helper() + used := module.NamedGlobal("llvm.used") + if used.IsNil() || used.Initializer().IsNil() { + t.Fatalf("root package anchor is not protected by llvm.used:\n%s", module.String()) + } + for i := 0; i < used.Initializer().OperandsCount(); i++ { + if stripCoroRootPackageConstantPointer(used.Initializer().Operand(i)).C == anchor.C { + return + } + } + t.Fatalf("llvm.used does not retain root package anchor %q:\n%s", anchor.Name(), module.String()) +} + +func requireCoroFrameDescriptorHash(t *testing.T, name, body string) string { + t.Helper() + matches := regexp.MustCompile( + `@`+regexp.QuoteMeta(coroDescriptorPrefixV1)+`([0-9a-f]{32})`, + ).FindAllStringSubmatch(body, -1) + if len(matches) == 0 { + t.Fatalf("%s has no PhysicalABIV1 frame descriptor:\n%s", name, body) + } + hash := matches[0][1] + for _, match := range matches[1:] { + if match[1] != hash { + t.Fatalf("%s references multiple frame descriptor hashes %s and %s:\n%s", name, hash, match[1], body) + } + } + return hash +} + +func assertCoroRootFactoryV1Body(t *testing.T, body string) { + t.Helper() + if !regexp.MustCompile( + `define ptr @"?` + regexp.QuoteMeta(coroRootFactoryPrefix) + `[0-9a-f]{32}"?\(ptr %0, ptr %1, ptr %2\)`, + ).MatchString(body) { + t.Fatalf("root factory does not use (g, out, startup) -> handle ABI:\n%s", body) + } + loads := regexp.MustCompile( + `(?s)(%[-a-zA-Z$._0-9]+) = getelementptr inbounds[^\n{]*\{ i8, i32 \}, ptr %2, i32 0, i32 0\s+` + + `(%[-a-zA-Z$._0-9]+) = load i8, ptr [^,]+, align 1.*?` + + `(%[-a-zA-Z$._0-9]+) = getelementptr inbounds[^\n{]*\{ i8, i32 \}, ptr %2, i32 0, i32 1\s+` + + `(%[-a-zA-Z$._0-9]+) = load i32, ptr [^,]+, align 4`, + ).FindStringSubmatch(body) + if len(loads) != 5 { + t.Fatalf("root factory does not load typed {uint8,uint32} startup arguments:\n%s", body) + } + call := regexp.MustCompile( + `call ptr @"?foo\.Parent\$coro"?\(ptr %0, ptr %1, i8 ` + regexp.QuoteMeta(loads[2]) + + `, i32 ` + regexp.QuoteMeta(loads[4]) + `\)`, + ) + if !call.MatchString(body) { + t.Fatalf("root factory does not pass (g, out, typed startup args) to Parent$coro exactly:\n%s", body) + } + if got := len(regexp.MustCompile(`\bcall\b`).FindAllString(body, -1)); got != 1 { + t.Fatalf("root factory calls = %d, want only Parent$coro:\n%s", got, body) + } + for _, forbidden := range []string{ + "llvm.coro.", "coro.suspend", ".resume", ".destroy", "clone", + `@"foo.Parent"(`, "@foo.Parent(", `@"foo.Child$coro"(`, "@foo.Child$coro(", + } { + if strings.Contains(body, forbidden) { + t.Fatalf("root factory contains forbidden coroutine/clone/plain-primary marker %q:\n%s", forbidden, body) + } + } +} + +func assertCoroRootFactoryV1Descriptor(t *testing.T, ir, hash, parentHash string, pointerBits int) { + t.Helper() + if hash != parentHash { + t.Fatalf("root factory hash %s does not match Parent physical ABI hash %s", hash, parentHash) + } + uintptrType := "i" + strconv.Itoa(pointerBits) + rootPattern := regexp.MustCompile( + `@` + regexp.QuoteMeta(coroRootFactoryDescriptorPrefix+hash) + + ` = linkonce_odr unnamed_addr constant \{ i32, i32, i64, i64, ptr, ` + uintptrType + `, ` + uintptrType + `, ` + uintptrType + `, ` + uintptrType + ` \} ` + + `\{ i32 1, i32 0, i64 ([^,]+), i64 ([^,]+), ptr @"?` + regexp.QuoteMeta(coroRootFactoryPrefix+hash) + `"?, ` + + uintptrType + ` 8, ` + uintptrType + ` 4, ` + uintptrType + ` 4, ` + uintptrType + ` 4 \}`, + ) + root := rootPattern.FindStringSubmatch(ir) + if len(root) != 3 { + t.Fatalf("root descriptor lacks v1/hash/factory/startup(8,4)/result(4,4) target layout:\n%s", ir) + } + framePattern := regexp.MustCompile( + `@` + regexp.QuoteMeta(coroDescriptorPrefixV1+parentHash) + + ` = linkonce_odr unnamed_addr constant \{ [^}]+ \} \{ i32 1, i32 0, i64 ([^,]+), i64 ([^,]+),`, + ) + frame := framePattern.FindStringSubmatch(ir) + if len(frame) != 3 { + t.Fatalf("Parent frame descriptor %q is absent:\n%s", coroDescriptorPrefixV1+parentHash, ir) + } + if root[1] != frame[1] || root[2] != frame[2] { + t.Fatalf("root descriptor hash words = (%s,%s), Parent frame hash words = (%s,%s)", root[1], root[2], frame[1], frame[2]) + } +} + +func assertCoroRootDescriptorLLVMUsed(t *testing.T, module llvm.Module, hash string) { + t.Helper() + used := module.NamedGlobal("llvm.used") + if used.IsNil() { + t.Fatalf("root descriptor is not protected from final-link dead stripping by llvm.used:\n%s", module.String()) + } + if got := used.Linkage(); got != llvm.AppendingLinkage { + t.Fatalf("llvm.used linkage = %v, want appending", got) + } + if got := used.Section(); got != "llvm.metadata" { + t.Fatalf("llvm.used section = %q, want llvm.metadata", got) + } + name := coroRootFactoryDescriptorPrefix + hash + var usedLine string + for _, line := range strings.Split(module.String(), "\n") { + if strings.HasPrefix(line, "@llvm.used =") { + usedLine = line + break + } + } + if usedLine == "" || (!strings.Contains(usedLine, "ptr @"+name) && + !strings.Contains(usedLine, `ptr @"`+name+`"`)) { + t.Fatalf("llvm.used does not retain root descriptor %q: %s", name, usedLine) + } +} + +func assertCoroRootObjectRetained(t *testing.T, prog llssa.Program, module llvm.Module, hash string) { + t.Helper() + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit root-retention object: %v\n%s", err, module.String()) + } + defer object.Dispose() + for _, name := range []string{ + coroRootFactoryDescriptorPrefix + hash, + coroRootFactoryPrefix + hash, + } { + if !bytes.Contains(object.Bytes(), []byte(name)) { + t.Fatalf("object symbol table lost linker-retained root symbol %q", name) + } + } +} + +func hasLLVMCall(ir, intrinsic string) bool { + return regexp.MustCompile(`call [^\n]*@` + regexp.QuoteMeta(intrinsic) + `\b`).MatchString(ir) +} diff --git a/cl/coro_assembly_nosuspend.go b/cl/coro_assembly_nosuspend.go new file mode 100644 index 0000000000..a02f39f652 --- /dev/null +++ b/cl/coro_assembly_nosuspend.go @@ -0,0 +1,196 @@ +/* + * 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 ( + "encoding/hex" + "fmt" + "go/ast" + "sort" + "strings" + "unicode/utf8" + + "golang.org/x/tools/go/ssa" +) + +// CoroAssemblyNoSuspendProof is a build-owned proof over one target-selected, +// post-CABI translated Plan9 assembly definition and its complete direct-call +// closure. EmissionPackage accepts these records only as inputs; cl binds them +// again to an exact bodyless Go declaration and frozen physical symbol. +type CoroAssemblyNoSuspendProof struct { + PhysicalSymbol string + ABISignature string + CallClosure []string + ClosureSHA256 string +} + +// CoroAssemblyNoSuspendCertificate is the immutable frontend certificate for +// one retained physical Go-ABI assembly call. The call is never elided and +// remains IRQUnsafe; the certificate proves only that it cannot suspend. +type CoroAssemblyNoSuspendCertificate struct { + ID string + PhysicalSymbol string + ABISignature string + ClosureSHA256 string +} + +func cloneCoroAssemblyNoSuspendProofs(proofs []CoroAssemblyNoSuspendProof) (map[string]CoroAssemblyNoSuspendProof, error) { + if len(proofs) == 0 { + return nil, nil + } + result := make(map[string]CoroAssemblyNoSuspendProof, len(proofs)) + for index, proof := range proofs { + if proof.PhysicalSymbol == "" || !utf8.ValidString(proof.PhysicalSymbol) || strings.IndexByte(proof.PhysicalSymbol, 0) >= 0 { + return nil, fmt.Errorf("assembly no-suspend proof %d has an invalid physical symbol", index) + } + if proof.ABISignature == "" || !utf8.ValidString(proof.ABISignature) || strings.IndexByte(proof.ABISignature, 0) >= 0 { + return nil, fmt.Errorf("assembly no-suspend proof for %q has an invalid ABI signature", proof.PhysicalSymbol) + } + digest, err := hex.DecodeString(proof.ClosureSHA256) + if err != nil || len(digest) != 32 || proof.ClosureSHA256 != strings.ToLower(proof.ClosureSHA256) { + return nil, fmt.Errorf("assembly no-suspend proof for %q has an invalid SHA-256 closure identity", proof.PhysicalSymbol) + } + if len(proof.CallClosure) == 0 { + return nil, fmt.Errorf("assembly no-suspend proof for %q has an empty call closure", proof.PhysicalSymbol) + } + closure := append([]string(nil), proof.CallClosure...) + containsRoot := false + for closureIndex, name := range closure { + if name == "" || !utf8.ValidString(name) || strings.IndexByte(name, 0) >= 0 { + return nil, fmt.Errorf("assembly no-suspend proof for %q has an invalid closure symbol at %d", proof.PhysicalSymbol, closureIndex) + } + if closureIndex != 0 && closure[closureIndex-1] >= name { + return nil, fmt.Errorf("assembly no-suspend proof for %q has a non-canonical call closure", proof.PhysicalSymbol) + } + containsRoot = containsRoot || name == proof.PhysicalSymbol + } + if !containsRoot { + return nil, fmt.Errorf("assembly no-suspend proof for %q omits its root from the call closure", proof.PhysicalSymbol) + } + if _, duplicate := result[proof.PhysicalSymbol]; duplicate { + return nil, fmt.Errorf("duplicate assembly no-suspend proof for physical symbol %q", proof.PhysicalSymbol) + } + proof.CallClosure = closure + result[proof.PhysicalSymbol] = proof + } + return result, nil +} + +func (u *EmissionUniverse) freezeCoroAssemblyNoSuspendCertificates() error { + used := make(map[string]*ssa.Function) + for _, fn := range u.functions { + if fn == nil || fn.Pkg == nil || fn.Parent() != nil || functionNeedsLinkOnce(fn) || len(fn.Blocks) != 0 { + continue + } + declaration, _ := fn.Syntax().(*ast.FuncDecl) + if declaration == nil || declaration.Body != nil { + continue + } + owners := u.sortedUseOwners(fn) + if len(owners) != 1 { + continue + } + owner := owners[0] + ownerKey := emissionFunctionOwnerKey{function: fn, owner: owner} + if u.functionKinds[ownerKey] != goFunc { + continue + } + kind, symbol, managedSignature, ok := splitManagedSymbolKey(u.finalKeys[ownerKey]) + if !ok || kind != goFunc { + continue + } + if physical := u.physicalNames[ownerKey]; physical != "" { + symbol = physical + } + proof, proved := owner.assemblyNoSuspend[symbol] + if !proved { + continue + } + usageKey := owner.identity + "\x00" + symbol + if previous := used[usageKey]; previous != nil && previous != fn { + return fmt.Errorf("prepare emission universe: assembly no-suspend proof for %q matches multiple bodyless Go declarations", symbol) + } + used[usageKey] = fn + linkIdentity := u.linkIdentities[fn] + if linkIdentity == "" { + return fmt.Errorf("prepare emission universe: assembly no-suspend declaration %q has no frozen link identity", fn.Name()) + } + target := u.prog.TargetSpec() + fields := []string{ + "llgo-coro-assembly-nosuspend-v0", + owner.identity, + owner.pkgPath, + linkIdentity, + symbol, + managedSignature, + proof.ABISignature, + proof.ClosureSHA256, + target.Triple, + target.CPU, + target.Features, + target.TargetABI, + u.prog.DataLayout(), + } + fields = append(fields, proof.CallClosure...) + u.assemblyNoSuspend[fn] = CoroAssemblyNoSuspendCertificate{ + ID: framedEmissionKey(fields...), + PhysicalSymbol: symbol, + ABISignature: proof.ABISignature, + ClosureSHA256: proof.ClosureSHA256, + } + } + return nil +} + +// CoroAssemblyNoSuspendCertificate returns the exact frozen translated- +// assembly certificate for fn. Ordinary bodyless Go declarations remain +// uncertified and therefore retain the conservative opaque boundary. +func (u *EmissionUniverse) CoroAssemblyNoSuspendCertificate(fn *ssa.Function) (certificate CoroAssemblyNoSuspendCertificate, certified bool, err error) { + if u == nil { + return CoroAssemblyNoSuspendCertificate{}, false, fmt.Errorf("coroutine assembly no-suspend certificate: nil emission universe") + } + if fn == nil { + return CoroAssemblyNoSuspendCertificate{}, false, fmt.Errorf("coroutine assembly no-suspend certificate: nil function") + } + canonical := u.canonicalAlias(fn) + if canonical == nil { + return CoroAssemblyNoSuspendCertificate{}, false, fmt.Errorf("coroutine assembly no-suspend certificate: function has cyclic canonical aliases") + } + if _, required := u.required[canonical]; !required { + return CoroAssemblyNoSuspendCertificate{}, false, fmt.Errorf("coroutine assembly no-suspend certificate: function %q is absent from the frozen emission universe", canonical.Name()) + } + certificate, certified = u.assemblyNoSuspend[canonical] + return certificate, certified, nil +} + +func sortedCoroAssemblyNoSuspendProofs(proofs map[string]CoroAssemblyNoSuspendProof) []CoroAssemblyNoSuspendProof { + if len(proofs) == 0 { + return nil + } + keys := make([]string, 0, len(proofs)) + for symbol := range proofs { + keys = append(keys, symbol) + } + sort.Strings(keys) + result := make([]CoroAssemblyNoSuspendProof, 0, len(keys)) + for _, symbol := range keys { + proof := proofs[symbol] + proof.CallClosure = append([]string(nil), proof.CallClosure...) + result = append(result, proof) + } + return result +} diff --git a/cl/coro_assembly_nosuspend_test.go b/cl/coro_assembly_nosuspend_test.go new file mode 100644 index 0000000000..18b5df7b94 --- /dev/null +++ b/cl/coro_assembly_nosuspend_test.go @@ -0,0 +1,117 @@ +//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/ast" + "strings" + "testing" + + llssa "github.com/goplus/llgo/ssa" +) + +func TestEmissionUniverseFreezesExactAssemblyNoSuspendProof(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/asmleaf", `package asmleaf +func Leaf(value int) int +func Call(value int) int { return Leaf(value) } +`) + testProg.ssa.Build() + + physical := "example.com/emission/asmleaf.Leaf" + proof := CoroAssemblyNoSuspendProof{ + PhysicalSymbol: physical, + ABISignature: `{"args":["i64"],"results":["i64"]}`, + CallClosure: []string{physical}, + ClosureSHA256: strings.Repeat("1a", 32), + } + prog := llssa.NewProgram(nil) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{ + SSA: pkg.ssa, Files: []*ast.File{pkg.file}, Identity: pkg.types.Path(), + AssemblyNoSuspendProofs: []CoroAssemblyNoSuspendProof{proof}, + }}) + if err != nil { + t.Fatal(err) + } + leaf := pkg.ssa.Func("Leaf") + certificate, ok, err := universe.CoroAssemblyNoSuspendCertificate(leaf) + if err != nil { + t.Fatal(err) + } + if !ok || certificate.ID == "" || certificate.PhysicalSymbol != physical || + certificate.ABISignature != proof.ABISignature || certificate.ClosureSHA256 != proof.ClosureSHA256 { + t.Fatalf("assembly certificate = %+v, %t; want exact frozen proof", certificate, ok) + } + if _, ok, err := universe.CoroAssemblyNoSuspendCertificate(pkg.ssa.Func("Call")); err != nil || ok { + t.Fatalf("bodyful Call certificate = _, %t, %v; want false, nil", ok, err) + } + + proof.CallClosure[0] = "mutated" + certificateAfterMutation, ok, err := universe.CoroAssemblyNoSuspendCertificate(leaf) + if err != nil || !ok || certificateAfterMutation != certificate { + t.Fatalf("certificate changed after caller mutation: %+v, %t, %v", certificateAfterMutation, ok, err) + } +} + +func TestEmissionUniverseAssemblyNoSuspendProofFailsClosed(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/asmfail", `package asmfail +func Leaf() +`) + testProg.ssa.Build() + physical := "example.com/emission/asmfail.Leaf" + prog := llssa.NewProgram(nil) + defer prog.Dispose() + + for _, test := range []struct { + name string + proof CoroAssemblyNoSuspendProof + want string + }{ + { + name: "invalid digest", + proof: CoroAssemblyNoSuspendProof{PhysicalSymbol: physical, ABISignature: `{}`, + CallClosure: []string{physical}, ClosureSHA256: "not-a-digest"}, + want: "invalid SHA-256", + }, + { + name: "unsorted closure", + proof: CoroAssemblyNoSuspendProof{PhysicalSymbol: physical, ABISignature: `{}`, + CallClosure: []string{physical, "aaa"}, ClosureSHA256: strings.Repeat("00", 32)}, + want: "non-canonical call closure", + }, + { + name: "missing root", + proof: CoroAssemblyNoSuspendProof{PhysicalSymbol: physical, ABISignature: `{}`, + CallClosure: []string{"other"}, ClosureSHA256: strings.Repeat("00", 32)}, + want: "omits its root", + }, + } { + t.Run(test.name, func(t *testing.T) { + _, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{ + SSA: pkg.ssa, Files: []*ast.File{pkg.file}, Identity: pkg.types.Path(), + AssemblyNoSuspendProofs: []CoroAssemblyNoSuspendProof{test.proof}, + }}) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("PrepareEmissionUniverse error = %v; want %q", err, test.want) + } + }) + } +} diff --git a/cl/coro_await.go b/cl/coro_await.go new file mode 100644 index 0000000000..e28ab2db83 --- /dev/null +++ b/cl/coro_await.go @@ -0,0 +1,623 @@ +/* + * 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 ( + "fmt" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroAwaitCompletionReturn uint64 = 1 + coroAwaitCompletionPanic uint64 = 2 + coroAwaitCompletionAbort uint64 = 3 + coroAwaitCompletionShutdown uint64 = 4 + coroAwaitCompletionReturnRecovered uint64 = 5 + + coroAwaitRecoverNone uint64 = 0 + coroAwaitRecoverDirect uint64 = 1 +) + +// resolveCoroStaticAwait proves the exact subset implemented by the physical +// child-await lowering. The returned function is the canonical target recorded +// by the whole-program plan, not an identity inferred from an SSA display name. +func resolveCoroStaticAwait(plan *coro.SSAPlan, caller coro.FunctionPlan, call ssa.CallInstruction, universe *EmissionUniverse) (*ssa.Function, coro.FunctionPlan, error) { + if plan == nil || call == nil || call.Common() == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("requires a compilation CallPlan") + } + common := call.Common() + if common.IsInvoke() { + return nil, coro.FunctionPlan{}, fmt.Errorf("requires a non-invoke call") + } + callPlan, ok := plan.CallPlan(call) + if !ok { + return nil, coro.FunctionPlan{}, fmt.Errorf("call has no compilation CallPlan") + } + if callPlan.Kind != coro.CallDirect || callPlan.Rep != coro.DirectCoro || callPlan.Open || callPlan.MayBeNil || len(callPlan.Targets) != 1 { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "requires one closed non-nil direct coroutine target, got kind=%v representation=%s open=%t may-be-nil=%t targets=%d", + callPlan.Kind, callPlan.Rep, callPlan.Open, callPlan.MayBeNil, len(callPlan.Targets), + ) + } + target, ok := plan.Function(callPlan.Targets[0]) + if !ok || target == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("direct coroutine target %q is absent from the compilation plan", callPlan.Targets[0]) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.ID != callPlan.Targets[0] { + return nil, coro.FunctionPlan{}, fmt.Errorf("direct coroutine target %q has no canonical function plan", callPlan.Targets[0]) + } + if err := validateCoroAwaitTarget(caller, targetPlan); err != nil { + return nil, coro.FunctionPlan{}, err + } + if target.Signature != nil && target.Signature.Recv() != nil { + if err := validateCoroStaticMethodCallOperands(call, target, universe); err != nil { + return nil, coro.FunctionPlan{}, err + } + } else if len(target.FreeVars) != 0 { + closure, exact := common.Value.(*ssa.MakeClosure) + closureTarget, targetExact := func() (*ssa.Function, bool) { + if !exact || closure == nil { + return nil, false + } + fn, ok := closure.Fn.(*ssa.Function) + return fn, ok + }() + if !targetExact || closureTarget != target || len(closure.Bindings) != len(target.FreeVars) { + return nil, coro.FunctionPlan{}, fmt.Errorf("closed captured coroutine target requires its exact MakeClosure environment") + } + } else if common.StaticCallee() == nil { + if common.Method != nil || target.Signature == nil || target.Signature.Variadic() || len(common.Args) != target.Signature.Params().Len() { + return nil, coro.FunctionPlan{}, fmt.Errorf("closed direct coroutine target has an incompatible dynamic call shape") + } + for index, argument := range common.Args { + if argument == nil || !types.Identical(argument.Type(), target.Signature.Params().At(index).Type()) { + return nil, coro.FunctionPlan{}, fmt.Errorf("closed direct coroutine operand %d does not match the target parameter ABI", index) + } + } + } + return target, targetPlan, nil +} + +// validateCoroStaticMethodCallOperands freezes the x/tools receiver convention +// at the exact call boundary. A declared receiver is target.Params[0] and the +// same SSA value is common.Args[0]; bound method values, closures, invokes, and +// synthetic receiver adapters do not satisfy this shape. +func validateCoroStaticMethodCallOperands(call ssa.CallInstruction, target *ssa.Function, universe *EmissionUniverse) error { + if call == nil || call.Common() == nil || target == nil || target.Signature == nil || target.Signature.Recv() == nil { + return fmt.Errorf("static coroutine method requires an exact declared method target") + } + common := call.Common() + raw, exactValue := common.Value.(*ssa.Function) + if common.IsInvoke() || common.StaticCallee() == nil || !exactValue || raw != common.StaticCallee() { + return fmt.Errorf("static coroutine method requires an exact function operand, not an invoke or method value") + } + rawNormalized := coroPhysicalNormalizeSourceSignature(raw.Signature) + if raw.Signature == nil || raw.Signature.Recv() == nil || rawNormalized.Params().Len() != len(raw.Params) || len(common.Args) != len(raw.Params) { + return fmt.Errorf("static coroutine method source operand has no exact receiver-first SSA shape") + } + for index, parameter := range raw.Params { + if parameter == nil || common.Args[index] == nil || + !types.Identical(parameter.Type(), rawNormalized.Params().At(index).Type()) || + !types.Identical(common.Args[index].Type(), parameter.Type()) { + return fmt.Errorf("static coroutine method source operand %d does not match its exact SSA parameter", index) + } + } + normalized := coroPhysicalNormalizeSourceSignature(target.Signature) + var targetContext *context + if universe != nil { + if canonical := universe.canonicalAlias(raw); canonical == nil || canonical != target { + return fmt.Errorf("static coroutine method target is not the frozen canonical alias of its source operand") + } + effectiveRaw, err := universe.coroPhysicalSourceSignature(raw) + if err != nil { + return fmt.Errorf("derive source static coroutine method signature: %w", err) + } + normalized, err = universe.coroPhysicalSourceSignature(target) + if err != nil { + return fmt.Errorf("derive canonical static coroutine method signature: %w", err) + } + if !coroInterfaceDispatchSignaturesIdentical(effectiveRaw, normalized) { + return fmt.Errorf("static coroutine method source ABI %s does not match canonical target ABI %s", effectiveRaw, normalized) + } + targetContext, err = universe.functionABIContext(target, universe.ownerOf(target)) + if err != nil { + return fmt.Errorf("derive static coroutine method target ABI: %w", err) + } + } else if raw != target { + return fmt.Errorf("static coroutine method alias requires a frozen emission universe") + } + if normalized.Params().Len() != len(target.Params) || len(common.Args) != len(target.Params) { + return fmt.Errorf( + "static coroutine method receiver/argument shape mismatch: normalized=%d SSA-params=%d call-args=%d", + normalized.Params().Len(), len(target.Params), len(common.Args), + ) + } + for index, parameter := range target.Params { + if parameter == nil || common.Args[index] == nil { + return fmt.Errorf("static coroutine method operand %d is incomplete", index) + } + normalizedType := normalized.Params().At(index).Type() + parameterType := parameter.Type() + if targetContext != nil { + parameterType = targetContext.patchType(parameterType) + } + if !types.Identical(parameterType, normalizedType) { + return fmt.Errorf( + "static coroutine method canonical operand %d does not match the normalized receiver/parameter ABI (normalized=%s SSA-parameter=%s)", + index, normalizedType, parameterType, + ) + } + } + return nil +} + +func validateCoroAwaitTarget(caller, target coro.FunctionPlan) error { + if caller.Emission != coro.EmitCoroutine { + return fmt.Errorf("caller emission is %s, want coroutine", caller.Emission) + } + if target.External != coro.Defined || target.Emission != coro.EmitCoroutine || + (target.FuncRep != coro.DirectCoro && target.FuncRep != coro.Dispatch) || !target.Demand.Contains(coro.AsyncDemand) { + return fmt.Errorf( + "target %q has no defined coroutine entry with async demand (external=%s emission=%s representation=%s demand=%s)", + target.ID, target.External, target.Emission, target.FuncRep, target.Demand, + ) + } + return nil +} + +// compileCoroStaticAwait lowers a source-style synchronous call into one +// stackless child handoff. It creates the child only to its initial suspend; +// this function never resumes or destroys a handle. Those operations belong to +// the scheduler after the parent's resume episode has returned. +func (p *context) compileCoroStaticAwait( + b llssa.Builder, call *ssa.Call, instructionPlan coroPhysicalInstructionPlan, +) llssa.Expr { + if !p.hasCoroPhysicalBody() || call == nil || instructionPlan.control != coroPhysicalControlDirectAwait { + panic("coroutine child await escaped its frozen physical control recipe") + } + // Keep the ordinary call lowerer's frontend-elided package-init rule ahead + // of coroutine CallPlan dispatch. fnIgnore is not a variadic arity; passing + // it to compileValues would subtract two operands from a zero-argument call. + if p.funcKind(call.Call.Value) == fnIgnore { + panic("coroutine child await selected a frontend-elided initializer") + } + callee := instructionPlan.controlTarget + if callee == nil || instructionPlan.controlTargetID == "" { + panic("coroutine child await has an incomplete frozen physical control recipe") + } + p.recordCallerLocationForCall(b, &call.Call) + p.emitPCLineLabel(b, call.Pos()) + + // Preserve Go's left-to-right argument evaluation before publishing any + // child or parent scheduler state. + args := p.compileValues(b, call.Call.Args, p.funcKind(call.Call.Value)) + var closureContext llssa.Expr + if len(callee.FreeVars) != 0 { + closure, exact := call.Call.Value.(*ssa.MakeClosure) + if !exact { + panic("coroutine child await lost its exact captured closure") + } + closureValue := p.compileValue(b, closure) + closureContext = b.Field(closureValue, 1) + } + keepaliveSlots := p.compileCoroCallKeepaliveSlots(b, call) + return p.compileCoroTargetAwaitWithContextAndRecovery(b, callee, closureContext, args, nil, keepaliveSlots) +} + +// compileCoroTargetAwait lowers one already-resolved exact managed target. +// args must have been evaluated in source order before this function is called. +// It is shared by source SSA calls and compiler-inserted runtime helper calls. +func (p *context) compileCoroTargetAwait(b llssa.Builder, callee *ssa.Function, args []llssa.Expr) llssa.Expr { + return p.compileCoroTargetAwaitWithContextAndRecovery(b, callee, llssa.Nil, args, nil, nil) +} + +func (p *context) compileCoroTargetAwaitWithKeepalive( + b llssa.Builder, callee *ssa.Function, args, keepalive []llssa.Expr, +) llssa.Expr { + return p.compileCoroTargetAwaitWithContextAndRecovery(b, callee, llssa.Nil, args, nil, keepalive) +} + +func (p *context) compileCoroTargetAwaitWithContext( + b llssa.Builder, callee *ssa.Function, closureContext llssa.Expr, args []llssa.Expr, +) llssa.Expr { + return p.compileCoroTargetAwaitWithContextAndRecovery(b, callee, closureContext, args, nil, nil) +} + +func (p *context) compileCoroCleanupTargetAwait( + b llssa.Builder, callee *ssa.Function, args []llssa.Expr, cleanup *coroStaticCleanupState, +) llssa.Expr { + body := p.coroBody() + if cleanup == nil || body == nil || body.cleanup != cleanup { + panic("coroutine cleanup await requires the active static cleanup drainer") + } + return p.compileCoroTargetAwaitWithContextAndRecovery(b, callee, llssa.Nil, args, cleanup, nil) +} + +func (p *context) compileCoroTargetAwaitWithContextAndRecovery( + b llssa.Builder, callee *ssa.Function, closureContext llssa.Expr, args []llssa.Expr, + cleanup *coroStaticCleanupState, keepaliveSlots []llssa.Expr, +) llssa.Expr { + return p.compileCoroTargetEntryAwaitWithContextAndRecovery( + b, p.mustFunctionSymbol(callee), closureContext, args, cleanup, keepaliveSlots, + ) +} + +// compileCoroTargetEntryAwaitWithContextAndRecovery consumes an already +// resolved physical symbol role. Most callers use the generic wrapper above; +// patch initialization passes its exact private original-init role so neither +// declaration nor body materialization can silently resolve back to public +// init. +func (p *context) compileCoroTargetEntryAwaitWithContextAndRecovery( + b llssa.Builder, entry plannedFunctionSymbol, closureContext llssa.Expr, args []llssa.Expr, + cleanup *coroStaticCleanupState, keepaliveSlots []llssa.Expr, +) llssa.Expr { + callee := entry.function + body := p.coroBody() + if body == nil || p.compilation == nil || p.compilation.CoroPlan == nil { + panic("coroutine child await requires an active physical coroutine body") + } + if b.Func != p.fn { + panic("coroutine child await builder does not belong to the active physical coroutine function") + } + callerPlan, ok := p.compilation.CoroPlan.FunctionPlan(p.goFn) + if !ok { + panic("coroutine child await: current function has no compilation plan") + } + targetPlan, ok := p.compilation.CoroPlan.FunctionPlan(callee) + if !ok { + panic("coroutine child await: target has no compilation plan") + } + if err := validateCoroAwaitTarget(callerPlan, targetPlan); err != nil { + panic(fmt.Sprintf("coroutine child await: function %q: %v", callerPlan.ID, err)) + } + + if p.emissionUniverse == nil { + panic("coroutine child await requires a prepared emission universe") + } + sourceSig, err := p.emissionUniverse.coroPhysicalSourceSignature(callee) + if err != nil { + panic(fmt.Sprintf("coroutine child await: derive target %q ABI: %v", entry.plan.ID, err)) + } + physicalArgs := args + if len(callee.FreeVars) != 0 { + if closureContext.IsNil() { + panic(fmt.Sprintf("coroutine child await: captured target %q has no exact closure context", entry.plan.ID)) + } + sourceSig, err = p.emissionUniverse.coroPhysicalEntrySourceSignature(callee) + if err != nil { + panic(fmt.Sprintf("coroutine child await: derive captured target %q ABI: %v", entry.plan.ID, err)) + } + physicalArgs = make([]llssa.Expr, 0, len(args)+1) + physicalArgs = append(physicalArgs, closureContext) + physicalArgs = append(physicalArgs, args...) + } else if !closureContext.IsNil() { + panic(fmt.Sprintf("coroutine child await: non-captured target %q received a closure context", entry.plan.ID)) + } + abi := newCoroPhysicalABI(p, entry, sourceSig) + if len(physicalArgs) != sourceSig.Params().Len() { + panic(fmt.Sprintf( + "coroutine child await: target %q arguments=%d do not match normalized source parameters=%d", + entry.plan.ID, len(physicalArgs), sourceSig.Params().Len(), + )) + } + childFn, _, kind := p.compileFunctionEntry(entry) + if kind != goFunc { + panic(fmt.Sprintf("coroutine child await: target %q did not resolve to a Go entry", entry.plan.ID)) + } + + resultType := p.prog.Type(abi.resultSlotType, llssa.InGo) + resultSlot := p.coroFrameAlloca(resultType) + callArgs := make([]llssa.Expr, 0, len(physicalArgs)+2) + callArgs = append(callArgs, + body.task, + b.Convert(p.prog.VoidPtr(), resultSlot), + ) + callArgs = append(callArgs, physicalArgs...) + child := b.Call(childFn.Expr, callArgs...) + if child.Type == nil || !types.Identical(child.RawType(), types.Typ[types.UnsafePointer]) { + var childType types.Type + if child.Type != nil { + childType = child.RawType() + } + panic(fmt.Sprintf( + "coroutine child await: caller %q target %q symbol %q returned %v; declaration=%v, want unsafe.Pointer physical handle", + callerPlan.ID, targetPlan.ID, childFn.Name(), childType, childFn.Expr.RawType(), + )) + } + return p.awaitCoroChildWithRecovery(b, child, resultSlot, sourceSig.Results(), cleanup, keepaliveSlots) +} + +// compileCoroPatchInitAwait lowers the compiler-inserted call from a patch +// package initializer to the original package initializer. Both source +// signatures are func(), but their managed entries are physical coroutines; +// this edge therefore uses the same scheduler-owned child transaction as an +// ordinary static synchronous-style call. +func (p *context) compileCoroPatchInitAwait(b llssa.Builder) { + if !p.hasCoroPhysicalBody() || b == nil || b.Func != p.fn { + panic("coroutine patch initializer await requires an active physical body") + } + if p.emissionUniverse == nil || p.compilation.CoroPlan == nil || p.goFn == nil { + panic("coroutine patch initializer await requires a frozen exact plan") + } + original, frozen, err := p.emissionUniverse.ResolveCoroLoweredCall(p.goFn, coroPatchOriginalInitCall) + if err != nil { + panic(fmt.Errorf("coroutine patch initializer edge: %w", err)) + } + planned, exact := p.compilation.CoroPlan.ResolveLoweredCall(p.goFn, coroPatchOriginalInitCall) + if !frozen || original == nil || !exact || planned != original { + panic("coroutine patch initializer edge disagrees between the frozen emission universe and SSA plan") + } + entry := p.mustPatchOriginalInitFunctionSymbol(original) + if entry.function != original || !entry.patchOriginalInit { + panic("coroutine patch initializer edge lost its exact private original-init role") + } + result := p.compileCoroTargetEntryAwaitWithContextAndRecovery(b, entry, llssa.Nil, nil, nil, nil) + if !result.IsNil() { + panic("coroutine original package initializer returned a value") + } +} + +// coroFrameAlloca emits storage in the physical ramp entry so the definition +// dominates every selected dispatch branch and every post-suspend resume edge. +// LLVM CoroSplit then owns deciding which live slots become fields of the +// stackless frame. Emitting an alloca at a dynamic call site is invalid: that +// block executes only in the pre-suspend activation and does not dominate the +// generated resume function after coroutine splitting. +func (p *context) coroFrameAlloca(typ llssa.Type) llssa.Expr { + if !p.hasCoroPhysicalBody() || p.fn == nil || typ == nil { + panic("coroutine frame alloca requires an active physical body and type") + } + entry := p.fn.Block(0) + alloc := p.fn.NewBuilder() + defer alloc.Dispose() + alloc.SetBlockEx(entry, llssa.AtStart, true) + return alloc.AllocaT(typ) +} + +// coroFrameAlloc emits zero-initialized function-lifetime storage in the +// physical ramp entry. Source SSA stack Allocs normally live in source block +// zero, but that block is no longer the LLVM entry of a physical coroutine: +// cancellation and static-cleanup dispatch may enter one of its continuations +// without a CFG edge from the source block. Keeping the allocation (and its +// one-time Go zero initialization) in the ramp makes its address dominate all +// such compiler-owned entries while still leaving CoroSplit to retain only +// values that are actually live across a suspension. +func (p *context) coroFrameAlloc(typ llssa.Type) llssa.Expr { + if !p.hasCoroPhysicalBody() || p.fn == nil || typ == nil { + panic("coroutine frame allocation requires an active physical body and type") + } + entry := p.fn.Block(0) + alloc := p.fn.NewBuilder() + defer alloc.Dispose() + alloc.SetBlockEx(entry, llssa.AtStart, true) + return alloc.Alloc(typ, false) +} + +// awaitCoroChild completes the scheduler-owned half of one already-created +// child transaction. Exact static calls, interface dispatch, and the universal +// function descriptor all converge here, so registration, parent suspension, +// activation, and result reconstruction cannot drift between call shapes. +func (p *context) awaitCoroChild( + b llssa.Builder, child, resultSlot llssa.Expr, results *types.Tuple, +) llssa.Expr { + return p.awaitCoroChildWithRecovery(b, child, resultSlot, results, nil, nil) +} + +func (p *context) awaitCoroChildWithKeepalive( + b llssa.Builder, child, resultSlot llssa.Expr, results *types.Tuple, keepaliveSlots []llssa.Expr, +) llssa.Expr { + return p.awaitCoroChildWithRecovery(b, child, resultSlot, results, nil, keepaliveSlots) +} + +func (p *context) awaitCoroChildWithRecovery( + b llssa.Builder, child, resultSlot llssa.Expr, results *types.Tuple, + cleanup *coroStaticCleanupState, keepaliveSlots []llssa.Expr, +) llssa.Expr { + body := p.coroBody() + if body == nil { + panic("coroutine child await requires an active PhysicalABIV1 body") + } + if b.Func != p.fn || child.IsNil() || resultSlot.IsNil() { + panic("coroutine child await requires a child handle and result slot in the active function") + } + childHeader := b.CoroPromise(child, coroHeaderType(p.prog)) + b.Store(b.FieldAddr(childHeader, coroHeaderParent), body.coro.Handle()) + recoverMode := p.prog.IntVal(coroAwaitRecoverNone, p.prog.Uint32()) + recoverType := p.prog.Nil(p.prog.VoidPtr()) + recoverData := p.prog.Nil(p.prog.VoidPtr()) + if cleanup != nil { + recoverMode, recoverType, recoverData = cleanup.recoverAwaitArguments(p, b) + } + body.suspendForChild(b) + + if body.abi.awaitPrepareHook == "" { + panic("coroutine child await has no scheduler handoff hook") + } + publish := p.pkg.NewFunc(body.abi.awaitPrepareHook, coroAwaitPrepareSignature(), llssa.InC) + b.Call( + publish.Expr, + body.task, + body.coro.Handle(), + child, + recoverMode, + recoverType, + recoverData, + ) + if body.abi.awaitConsumeHook == "" { + panic("coroutine child await has no outcome consume hook") + } + typeWord := p.coroFrameAlloca(p.prog.VoidPtr()) + dataWord := p.coroFrameAlloca(p.prog.VoidPtr()) + b.Store(typeWord, p.prog.Nil(p.prog.VoidPtr())) + b.Store(dataWord, p.prog.Nil(p.prog.VoidPtr())) + consume := p.pkg.NewFunc(body.abi.awaitConsumeHook, coroAwaitConsumeSignature(), llssa.InC) + + // A task-cancellation decision is taken before the site's ordinary resumed + // continuation. Give child-await a per-site gate so cancellation still + // consumes the now-dead child's CompletionRecord before entering shared + // cleanup; otherwise an older deferred child could collide with the stale + // transaction. When cleanup is active, the gate retains Abort/Shutdown as + // the base while reconciling a concurrent deferred-child recovery/panic as + // the overlay. This preserves both cancellation and Go panic ordering. + canceled := p.fn.MakeBlock() + body.coro.SuspendCurrentBlockWithResumeDispatch(func(gate llssa.Builder, normal llssa.BasicBlock) { + body.dispatchZeroRunDecisionTo(gate, normal, canceled) + }) + body.activate(b) + cancelBuilder := p.fn.NewBuilder() + cancelBuilder.SetBlock(canceled) + body.activate(cancelBuilder) + cancelStatus := cancelBuilder.Call( + consume.Expr, + body.task, + body.coro.Handle(), + cancelBuilder.Convert(p.prog.VoidPtr(), typeWord), + cancelBuilder.Convert(p.prog.VoidPtr(), dataWord), + ) + p.emitCoroKeepaliveSlots(cancelBuilder, keepaliveSlots) + if ownerCleanup := body.cleanup; ownerCleanup == nil { + cancelBuilder.Jump(body.completion) + } else { + ownerCleanup.setCancellationBase(cancelBuilder) + returnedCancel := p.fn.MakeBlock() + panickedCancel := p.fn.MakeBlock() + abortedCancel := p.fn.MakeBlock() + shutdownCancel := p.fn.MakeBlock() + drainCancel := p.fn.MakeBlock() + invalidCancel := p.fn.MakeBlock() + cancelDispatch := cancelBuilder.Switch(cancelStatus, invalidCancel) + cancelDispatch.Case(p.prog.IntVal(coroAwaitCompletionReturn, p.prog.Uint32()), returnedCancel) + cancelDispatch.Case(p.prog.IntVal(coroAwaitCompletionPanic, p.prog.Uint32()), panickedCancel) + cancelDispatch.Case(p.prog.IntVal(coroAwaitCompletionAbort, p.prog.Uint32()), abortedCancel) + cancelDispatch.Case(p.prog.IntVal(coroAwaitCompletionShutdown, p.prog.Uint32()), shutdownCancel) + var recoveredCancel llssa.BasicBlock + if cleanup != nil { + if cleanup != ownerCleanup { + panic("deferred child cancellation recovery escaped its owner cleanup") + } + recoveredCancel = p.fn.MakeBlock() + cancelDispatch.Case(p.prog.IntVal(coroAwaitCompletionReturnRecovered, p.prog.Uint32()), recoveredCancel) + } + cancelDispatch.End(cancelBuilder) + + cancelBuilder.SetBlockEx(returnedCancel, llssa.AtEnd, false) + cancelBuilder.Jump(drainCancel) + cancelBuilder.SetBlockEx(panickedCancel, llssa.AtEnd, false) + ownerCleanup.setPanicOverlay(cancelBuilder, cancelBuilder.Load(typeWord), cancelBuilder.Load(dataWord)) + cancelBuilder.Jump(drainCancel) + cancelBuilder.SetBlockEx(abortedCancel, llssa.AtEnd, false) + cancelBuilder.Jump(drainCancel) + cancelBuilder.SetBlockEx(shutdownCancel, llssa.AtEnd, false) + cancelBuilder.Jump(drainCancel) + cancelBuilder.SetBlockEx(invalidCancel, llssa.AtEnd, false) + cancelBuilder.Unreachable() + if cleanup != nil { + cancelBuilder.SetBlockEx(recoveredCancel, llssa.AtEnd, false) + cleanup.reconcileDeferredChildReturn(p, cancelBuilder, coroAwaitCompletionReturnRecovered) + cancelBuilder.Jump(drainCancel) + } + cancelBuilder.SetBlockEx(drainCancel, llssa.AtEnd, false) + ownerCleanup.resume(cancelBuilder) + } + cancelBuilder.Dispose() + + // The child allocation is gone before this continuation is resumed. Its + // terminal outcome therefore lives in scheduler-owned parent metadata, not + // in the result slot or child promise. Consume exactly once before reading + // results or allowing another child transaction to start. + status := b.Call( + consume.Expr, + body.task, + body.coro.Handle(), + b.Convert(p.prog.VoidPtr(), typeWord), + b.Convert(p.prog.VoidPtr(), dataWord), + ) + p.emitCoroKeepaliveSlots(b, keepaliveSlots) + returned := p.fn.MakeBlock() + panicked := p.fn.MakeBlock() + aborted := p.fn.MakeBlock() + shutdown := p.fn.MakeBlock() + invalid := p.fn.MakeBlock() + dispatch := b.Switch(status, invalid) + dispatch.Case(p.prog.IntVal(coroAwaitCompletionReturn, p.prog.Uint32()), returned) + dispatch.Case(p.prog.IntVal(coroAwaitCompletionPanic, p.prog.Uint32()), panicked) + dispatch.Case(p.prog.IntVal(coroAwaitCompletionAbort, p.prog.Uint32()), aborted) + dispatch.Case(p.prog.IntVal(coroAwaitCompletionShutdown, p.prog.Uint32()), shutdown) + var recovered llssa.BasicBlock + if cleanup != nil { + recovered = p.fn.MakeBlock() + dispatch.Case(p.prog.IntVal(coroAwaitCompletionReturnRecovered, p.prog.Uint32()), recovered) + } + dispatch.End(b) + + b.SetBlockEx(panicked, llssa.AtEnd, false) + if body.panicPrepare.IsNil() { + // A compilation without the ExplicitStatus identity cannot produce a + // managed child panic. Treat an injected/corrupt status as unreachable + // instead of falling back to legacy stack unwinding. + b.Unreachable() + } else if body.cleanup == nil { + body.panic(b, b.Load(typeWord), b.Load(dataWord)) + } else if cleanup != nil { + cleanup.replacePanic(b, b.Load(typeWord), b.Load(dataWord)) + } else { + body.cleanup.enterPanic(b, b.Load(typeWord), b.Load(dataWord)) + } + + b.SetBlockEx(aborted, llssa.AtEnd, false) + body.enterCancellation(b, coroAwaitCompletionAbort) + b.SetBlockEx(shutdown, llssa.AtEnd, false) + body.enterCancellation(b, coroAwaitCompletionShutdown) + + b.SetBlockEx(invalid, llssa.AtEnd, false) + b.Unreachable() + if cleanup != nil { + b.SetBlockEx(recovered, llssa.AtEnd, false) + cleanup.reconcileDeferredChildReturn(p, b, coroAwaitCompletionReturnRecovered) + b.Jump(returned) + } + b.SetBlockContinuation(returned) + return p.loadCoroAwaitResult(b, resultSlot, results) +} + +// loadCoroAwaitResult reconstructs the exact source call value after the +// scheduler has resumed the parent. Multi-result calls are one SSA tuple value, +// not a result-slot struct: preserving that distinction keeps the ordinary +// Extract lowering and ValuePlan paths identical to a synchronous Go call. +func (p *context) loadCoroAwaitResult(b llssa.Builder, resultSlot llssa.Expr, results *types.Tuple) llssa.Expr { + count := 0 + if results != nil { + count = results.Len() + } + switch count { + case 0: + return llssa.Nil + case 1: + return b.Load(b.FieldAddr(resultSlot, 0)) + default: + fields := make([]llssa.Expr, results.Len()) + for i := range fields { + fields[i] = b.Load(b.FieldAddr(resultSlot, i)) + } + return b.Aggregate(p.prog.Type(results, llssa.InGo), fields...) + } +} diff --git a/cl/coro_bound_method.go b/cl/coro_bound_method.go new file mode 100644 index 0000000000..8bcce3d729 --- /dev/null +++ b/cl/coro_bound_method.go @@ -0,0 +1,277 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + "golang.org/x/tools/go/ssa" +) + +// validateCoroExactBoundMethodWrapper recognizes only x/tools/ssa's canonical +// method-value closure body. The wrapper has one captured receiver and a +// tail-call to that exact method; this makes it an ordinary captured function +// producer for the universal {descriptor,env} ABI. Other synthetic functions +// remain outside descriptor transport. +func validateCoroExactBoundMethodWrapper(fn *ssa.Function) error { + if fn == nil || fn.Pkg != nil || fn.Parent() != nil || fn.Syntax() != nil { + return fmt.Errorf("requires one top-level syntax-free generated wrapper") + } + object, ok := fn.Object().(*types.Func) + if !ok || object == nil { + return fmt.Errorf("has no exact method object") + } + method, ok := object.Type().(*types.Signature) + if !ok || method.Recv() == nil || fn.Signature == nil || fn.Signature.Recv() != nil { + return fmt.Errorf("does not drop exactly one declared method receiver") + } + if fn.Name() != object.Name()+"$bound" || fn.Synthetic != fmt.Sprintf("bound method wrapper for %s", object) { + return fmt.Errorf("has non-canonical bound-method identity") + } + if len(fn.FreeVars) != 1 || fn.FreeVars[0] == nil || fn.FreeVars[0].Parent() != fn || + !types.Identical(fn.FreeVars[0].Type(), method.Recv().Type()) { + return fmt.Errorf("does not capture exactly the declared receiver") + } + if fn.Signature.Variadic() != method.Variadic() || + !types.Identical(fn.Signature.Params(), method.Params()) || + !types.Identical(fn.Signature.Results(), method.Results()) { + return fmt.Errorf("callable signature does not equal the receiver-free method signature") + } + if len(fn.Params) != fn.Signature.Params().Len() { + return fmt.Errorf("SSA parameters do not match the callable signature") + } + + var call *ssa.Call + var ret *ssa.Return + extracts := make(map[int]*ssa.Extract) + for _, block := range fn.Blocks { + if block == nil { + return fmt.Errorf("contains a nil basic block") + } + for _, instruction := range block.Instrs { + switch instruction := instruction.(type) { + case *ssa.DebugRef: + case *ssa.Call: + if call != nil { + return fmt.Errorf("contains more than one call") + } + call = instruction + case *ssa.Extract: + if _, duplicate := extracts[instruction.Index]; duplicate { + return fmt.Errorf("contains a duplicate result extract") + } + extracts[instruction.Index] = instruction + case *ssa.Return: + if ret != nil { + return fmt.Errorf("contains more than one return") + } + ret = instruction + default: + return fmt.Errorf("contains non-tail-wrapper instruction %T", instruction) + } + } + } + if len(fn.Blocks) != 1 || call == nil || ret == nil || call.Block() != ret.Block() { + return fmt.Errorf("is not one single-block tail call") + } + common := call.Common() + if common == nil { + return fmt.Errorf("tail call has no CallCommon") + } + receiver := fn.FreeVars[0] + if types.IsInterface(method.Recv().Type()) { + if !common.IsInvoke() || common.Value != receiver || common.Method != object || len(common.Args) != len(fn.Params) { + return fmt.Errorf("interface receiver does not use the exact method invoke") + } + for index := range fn.Params { + if common.Args[index] != fn.Params[index] { + return fmt.Errorf("interface method argument %d is not the wrapper parameter", index) + } + } + } else { + if common.IsInvoke() || common.Method != nil || common.StaticCallee() == nil || len(common.Args) != len(fn.Params)+1 || common.Args[0] != receiver { + return fmt.Errorf("concrete receiver does not use one exact receiver-first static call") + } + for index := range fn.Params { + if common.Args[index+1] != fn.Params[index] { + return fmt.Errorf("concrete method argument %d is not the wrapper parameter", index) + } + } + } + + return validateCoroExactTailCallResults(fn, call, ret, extracts) +} + +// validateCoroExactMethodExpressionThunk recognizes the direct method- +// expression thunk synthesized by x/tools/ssa for T.Method. Unlike a bound +// method value, the receiver is the first ordinary parameter and there is no +// captured environment. Restricting this certificate to an exact receiver +// type deliberately leaves promoted-field and implicit-indirection wrappers +// closed until their additional nil/selection operations have their own +// audited recipe. +func validateCoroExactMethodExpressionThunk(fn *ssa.Function) error { + if fn == nil || fn.Pkg != nil || fn.Parent() != nil || fn.Syntax() != nil { + return fmt.Errorf("requires one top-level syntax-free generated thunk") + } + object, ok := fn.Object().(*types.Func) + if !ok || object == nil { + return fmt.Errorf("has no exact method object") + } + method, ok := object.Type().(*types.Signature) + if !ok || method.Recv() == nil || fn.Signature == nil || fn.Signature.Recv() != nil { + return fmt.Errorf("does not expose exactly one method receiver parameter") + } + if fn.Name() != object.Name()+"$thunk" || fn.Synthetic != fmt.Sprintf("thunk for %s", object) { + return fmt.Errorf("has non-canonical method-expression identity") + } + if len(fn.FreeVars) != 0 { + return fmt.Errorf("method-expression thunk unexpectedly captures an environment") + } + params := fn.Signature.Params() + methodParams := method.Params() + if params == nil || params.Len() != methodParams.Len()+1 || + !types.Identical(params.At(0).Type(), method.Recv().Type()) || + fn.Signature.Variadic() != method.Variadic() || + !types.Identical(fn.Signature.Results(), method.Results()) { + return fmt.Errorf("callable signature is not receiver-first method signature") + } + for index := 0; index < methodParams.Len(); index++ { + if !types.Identical(params.At(index+1).Type(), methodParams.At(index).Type()) { + return fmt.Errorf("callable parameter %d does not match method parameter", index+1) + } + } + if len(fn.Params) != params.Len() || len(fn.Locals) > 1 { + return fmt.Errorf("SSA parameters or receiver spill do not match the callable signature: params=%d/%d locals=%d", len(fn.Params), params.Len(), len(fn.Locals)) + } + + var receiverAlloc *ssa.Alloc + var receiverStore *ssa.Store + var receiverLoad *ssa.UnOp + var call *ssa.Call + var ret *ssa.Return + extracts := make(map[int]*ssa.Extract) + for _, block := range fn.Blocks { + if block == nil { + return fmt.Errorf("contains a nil basic block") + } + for _, instruction := range block.Instrs { + switch instruction := instruction.(type) { + case *ssa.DebugRef: + case *ssa.Alloc: + if receiverAlloc != nil { + return fmt.Errorf("contains more than one receiver allocation") + } + receiverAlloc = instruction + case *ssa.Store: + if receiverStore != nil { + return fmt.Errorf("contains more than one receiver store") + } + receiverStore = instruction + case *ssa.UnOp: + if instruction.Op != token.MUL || receiverLoad != nil { + return fmt.Errorf("contains a non-canonical receiver load") + } + receiverLoad = instruction + case *ssa.Call: + if call != nil { + return fmt.Errorf("contains more than one call") + } + call = instruction + case *ssa.Extract: + if _, duplicate := extracts[instruction.Index]; duplicate { + return fmt.Errorf("contains a duplicate result extract") + } + extracts[instruction.Index] = instruction + case *ssa.Return: + if ret != nil { + return fmt.Errorf("contains more than one return") + } + ret = instruction + default: + return fmt.Errorf("contains non-direct-thunk instruction %T", instruction) + } + } + } + if len(fn.Blocks) != 1 || call == nil || ret == nil || call.Block() != ret.Block() { + return fmt.Errorf("is not one single-block receiver-spill tail call") + } + var receiver ssa.Value = fn.Params[0] + if receiverAlloc != nil || receiverStore != nil || receiverLoad != nil || len(fn.Locals) != 0 { + if receiverAlloc == nil || receiverStore == nil || receiverLoad == nil || len(fn.Locals) != 1 || + fn.Locals[0] != receiverAlloc || receiverStore.Addr != receiverAlloc || + receiverStore.Val != fn.Params[0] || receiverLoad.X != receiverAlloc { + return fmt.Errorf("has an incomplete or non-canonical receiver spill") + } + receiver = receiverLoad + } + common := call.Common() + if common == nil { + return fmt.Errorf("tail call has no CallCommon") + } + if types.IsInterface(method.Recv().Type()) { + if !common.IsInvoke() || common.Value != receiver || common.Method != object || len(common.Args) != len(fn.Params)-1 { + return fmt.Errorf("interface receiver does not use the exact method invoke") + } + for index := 1; index < len(fn.Params); index++ { + if common.Args[index-1] != fn.Params[index] { + return fmt.Errorf("interface method argument %d is not the thunk parameter", index) + } + } + } else { + callee := common.StaticCallee() + if common.IsInvoke() || common.Method != nil || callee == nil || callee.Object() != object || + len(common.Args) != len(fn.Params) || common.Args[0] != receiver { + return fmt.Errorf("concrete receiver does not use one exact receiver-first static call") + } + for index := 1; index < len(fn.Params); index++ { + if common.Args[index] != fn.Params[index] { + return fmt.Errorf("concrete method argument %d is not the thunk parameter", index) + } + } + } + return validateCoroExactTailCallResults(fn, call, ret, extracts) +} + +func validateCoroExactTailCallResults(fn *ssa.Function, call *ssa.Call, ret *ssa.Return, extracts map[int]*ssa.Extract) error { + results := fn.Signature.Results().Len() + if len(ret.Results) != results { + return fmt.Errorf("tail return count %d does not match signature count %d", len(ret.Results), results) + } + switch results { + case 0: + if len(extracts) != 0 { + return fmt.Errorf("zero-result wrapper contains result extracts") + } + case 1: + if len(extracts) != 0 || ret.Results[0] != call { + return fmt.Errorf("single-result wrapper does not return its exact call") + } + default: + if len(extracts) != results { + return fmt.Errorf("multi-result wrapper extract count %d does not match %d", len(extracts), results) + } + for index, result := range ret.Results { + extract := extracts[index] + if extract == nil || extract.Tuple != call || extract.Index != index || result != extract { + return fmt.Errorf("multi-result wrapper return %d is not its exact call extract", index) + } + } + } + return nil +} diff --git a/cl/coro_call_site_plan.go b/cl/coro_call_site_plan.go new file mode 100644 index 0000000000..c1033f95cf --- /dev/null +++ b/cl/coro_call_site_plan.go @@ -0,0 +1,256 @@ +/* + * 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 ( + "fmt" + "slices" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// 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 +// once, writes the result into each owner-scoped SitePlan, and rejects any +// owner-dependent semantic result for one logical call occurrence. Physical +// function projections are added transactionally after the SSAPlan fixed point. +func (ir *coroProgramIR) freezeCallSites(u *EmissionUniverse) error { + if ir == nil || u == nil { + return fmt.Errorf("coroutine call SitePlan freeze requires one ProgramIR and emission universe") + } + if ir.callsFrozen { + return fmt.Errorf("coroutine call SitePlans were frozen more than once") + } + for _, function := range u.functions { + if function == nil { + continue + } + for _, owner := range u.sortedUseOwners(function) { + key := emissionFunctionOwnerKey{function: function, owner: owner} + ctx, err := u.functionABIContext(function, owner) + if err != nil { + return fmt.Errorf("function %q call SitePlan context: %w", function.Name(), err) + } + _, _, functionKind := ctx.funcName(function) + if functionKind != goFunc { + continue + } + if _, frozen := ir.siteOwners[key]; !frozen { + return fmt.Errorf("function %q call SitePlan has no frozen owner", function.Name()) + } + byInstruction := ir.sitePlans[key] + if byInstruction == nil { + byInstruction = make(map[ssa.Instruction]coroEmissionSitePlan) + ir.sitePlans[key] = byInstruction + } + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if !ok || call.Common() == nil { + continue + } + site := byInstruction[instruction] + noInit := FrontendElidesNoInitCall(call) + patchRedirect := false + var frozenPatchRedirect coroPatchInitRedirect + var classifyErr error + logicalName, patchTarget, redirected, redirectErr := u.CoroPatchInitRedirect(call) + if redirectErr != nil { + classifyErr = fmt.Errorf("classify frozen patch initializer replacement: %w", redirectErr) + } else if redirected { + patchRedirect = true + frozenPatchRedirect = coroPatchInitRedirect{logicalName: logicalName, target: patchTarget} + } + semantics, intrinsic, opcode := CoroIntrinsicCallUnsupported, false, 0 + var workerCertificate CoroWorkerSyscallCertificate + workerCertified := false + if !noInit && !patchRedirect && classifyErr == nil { + callee := call.Common().StaticCallee() + if callee != nil { + opcode, intrinsic, classifyErr = u.coroIntrinsicOpcode(callee) + } + if classifyErr == nil && intrinsic && isLLGoSyscallIntrinsic(opcode) && u.CoroWorkerEnabled() { + if direct, ok := call.(*ssa.Call); ok && direct.Common() != nil && !direct.Common().IsInvoke() && + direct.Parent() != nil && u.canonicalAlias(direct.Parent()) == direct.Parent() { + workerCertificate, workerCertified = u.workerSyscalls[direct] + } + } + if classifyErr == nil { + semantics, intrinsic, classifyErr = u.classifyCoroIntrinsicCallSite( + ctx, site, call, opcode, intrinsic, workerCertificate, workerCertified, + ) + } + } + plan := CoroCallSitePlan{ + IntrinsicSemantics: semantics, + Intrinsic: intrinsic, + } + switch { + case noInit: + plan.Elision = CoroCallElidedNoInit + case patchRedirect: + plan.Elision = CoroCallElidedPatchRedirect + case intrinsic && classifyErr == nil && semantics.ElidesManagedCall(): + plan.Elision = CoroCallElidedIntrinsic + } + if plan.ElidesCall() && intrinsic && classifyErr == nil && workerCertified { + if workerCertificate.ID == "" { + classifyErr = fmt.Errorf("freeze intrinsic elision certificate: certified call has an empty identity") + } else { + plan.ElisionCertificate = workerCertificate.ID + } + } + frozenCall := coroFrozenCallSitePlan{ + plan: plan, + opcode: opcode, + workerCertificate: workerCertificate, + workerCertified: workerCertified, + patchRedirect: frozenPatchRedirect, + patchAttempted: redirected || redirectErr != nil, + } + if workerCertified { + frozenCall.workerOwners = cloneCoroWorkerOwnerSet(u.workerSyscallOwners[call]) + frozenCall.workerIncoming = cloneCoroWorkerIncomingEdges(u.workerSyscallIncoming[call]) + } + if classifyErr != nil { + frozenCall.failure = classifyErr.Error() + } + if previous, exists := ir.callPlans[call]; exists { + if !sameCoroFrozenCallSitePlan(previous, frozenCall) { + return fmt.Errorf("function %q call %q has owner-dependent frozen SitePlans", function.Name(), call.String()) + } + frozenCall = previous + } + ir.callPlans[call] = frozenCall + site.callPlan = frozenCall + site.hasCallPlan = true + byInstruction[instruction] = site + } + } + } + } + ir.callsFrozen = true + // These maps are mutable builder scratch. All production call-site + // certificate payloads now live in ProgramIR; retaining a second readable + // store would permit future consumers to bypass the frozen SitePlan. + u.workerSyscalls = nil + u.workerSyscallOwners = nil + u.workerSyscallIncoming = nil + u.patchInitRedirects = nil + return nil +} + +func cloneCoroWorkerOwnerSet(source map[*ssa.Function]none) map[*ssa.Function]none { + if len(source) == 0 { + return nil + } + result := make(map[*ssa.Function]none, len(source)) + for function := range source { + result[function] = none{} + } + return result +} + +func cloneCoroWorkerIncomingEdges(source []coroWorkerSyscallIncomingEdge) []coroWorkerSyscallIncomingEdge { + if len(source) == 0 { + return nil + } + result := append([]coroWorkerSyscallIncomingEdge(nil), source...) + for index := range result { + result[index].targetKeys = append([]string(nil), result[index].targetKeys...) + } + return result +} + +func sameCoroFrozenCallSitePlan(first, second coroFrozenCallSitePlan) bool { + if first.plan != second.plan || first.failure != second.failure || first.opcode != second.opcode || + first.workerCertificate != second.workerCertificate || first.workerCertified != second.workerCertified || + first.patchRedirect != second.patchRedirect || first.patchAttempted != second.patchAttempted || + len(first.workerOwners) != len(second.workerOwners) || len(first.workerIncoming) != len(second.workerIncoming) { + return false + } + for function := range first.workerOwners { + if _, exists := second.workerOwners[function]; !exists { + return false + } + } + for index, left := range first.workerIncoming { + right := second.workerIncoming[index] + if left.call != right.call || left.carrier != right.carrier || left.parameter != right.parameter || + left.certified != right.certified || left.reason != right.reason || + left.foreignPointerResultMask != right.foreignPointerResultMask || + left.resultProjectionID != right.resultProjectionID || left.stableIdentity != right.stableIdentity || + !slices.Equal(left.targetKeys, right.targetKeys) { + return false + } + } + return true +} + +// CoroCallSitePlan returns the single frozen frontend call decision used by +// whole-program analysis, ABI closure checks, physical preflight, and +// emission. Invalid intrinsic shapes are retained as exact failed plans so all +// consumers report the same builder-owned diagnostic without rescanning SSA. +func (u *EmissionUniverse) CoroCallSitePlan(call ssa.CallInstruction) (CoroCallSitePlan, bool, error) { + if u == nil || u.coroProgramIR == nil { + return CoroCallSitePlan{}, false, fmt.Errorf("emission universe has no coroutine ProgramIR") + } + frozen, ok, err := u.coroProgramIR.callSitePlan(call) + if err != nil || !ok { + return CoroCallSitePlan{}, ok, err + } + if frozen.failure != "" { + return frozen.plan, true, fmt.Errorf("%s", frozen.failure) + } + return frozen.plan, true, nil +} + +// CoroLocalBodyFacts returns the ProgramIR-owned local semantic projection for +// one exact canonical function. Production whole-program analysis consumes +// this callback instead of rescanning raw SSA for Effect/Exec facts. +func (u *EmissionUniverse) CoroLocalBodyFacts(function *ssa.Function) (coro.SSAFunctionBodyFacts, error) { + if u == nil || u.coroProgramIR == nil { + return coro.SSAFunctionBodyFacts{}, fmt.Errorf("coroutine local body facts require a prepared ProgramIR") + } + canonical, frozen := u.Resolve(function) + if !frozen || canonical == nil || canonical != function { + return coro.SSAFunctionBodyFacts{}, fmt.Errorf("coroutine local body facts require one exact canonical function") + } + return u.coroProgramIR.functionLocalBodyFacts(function) +} + +type coroCallSitePlanReader interface { + CoroCallSitePlan(ssa.CallInstruction) (CoroCallSitePlan, bool, error) +} + +// coroIntrinsicCallSiteSemantics is the package-local compatibility projection +// of the frozen call SitePlan. It performs no opcode or operand classification. +func coroIntrinsicCallSiteSemantics(reader coroCallSitePlanReader, call ssa.CallInstruction) (CoroIntrinsicCallSemantics, bool, error) { + if reader == nil { + return CoroIntrinsicCallUnsupported, false, fmt.Errorf("coroutine intrinsic projection requires a call SitePlan reader") + } + plan, found, err := reader.CoroCallSitePlan(call) + if err != nil { + return plan.IntrinsicSemantics, plan.Intrinsic, err + } + if !found { + return CoroIntrinsicCallUnsupported, false, nil + } + return plan.IntrinsicSemantics, plan.Intrinsic, nil +} diff --git a/cl/coro_call_site_plan_compat_test.go b/cl/coro_call_site_plan_compat_test.go new file mode 100644 index 0000000000..eaac599e77 --- /dev/null +++ b/cl/coro_call_site_plan_compat_test.go @@ -0,0 +1,25 @@ +/* + * 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 "golang.org/x/tools/go/ssa" + +// CoroIntrinsicCallSiteSemantics keeps focused package tests readable while +// production has only the complete CoroCallSitePlan projection. +func (u *EmissionUniverse) CoroIntrinsicCallSiteSemantics(call ssa.CallInstruction) (CoroIntrinsicCallSemantics, bool, error) { + return coroIntrinsicCallSiteSemantics(u, call) +} diff --git a/cl/coro_callable_contract.go b/cl/coro_callable_contract.go new file mode 100644 index 0000000000..80a060c420 --- /dev/null +++ b/cl/coro_callable_contract.go @@ -0,0 +1,363 @@ +/* + * 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 ( + "fmt" + "go/ast" + "strings" + "unicode" + "unicode/utf8" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +const coroCallableContractIDForeignV1 = "foreign.v1" + +type coroCallableContractScope string + +const ( + coroCallableContractScopeWrapper coroCallableContractScope = "wrapper" + coroCallableContractScopeDeclaration coroCallableContractScope = "declaration" +) + +// coroCallableContractCertificate is a frozen, target-neutral description of +// one exact source declaration. Scope is deliberately frontend metadata: the +// shared coroutine model describes callable behavior, while the frontend must +// still prove whether that behavior belongs to a Go wrapper body or to a +// bodyless external declaration. +type coroCallableContractCertificate struct { + Contract coro.CallableContract + TrustedInlineContract coro.CallableContract + HasTrustedInlineContract bool + Scope coroCallableContractScope + ABI string + Canonical string +} + +// coroCallableContractCertificateFor reads only the exact ast.FuncDecl owned +// by fn. Synthetic wrappers, instantiated helper functions without their own +// declaration, and late comment-map guesses cannot acquire a certificate. +func coroCallableContractCertificateFor(fn *ssa.Function) (coroCallableContractCertificate, bool, error) { + if fn == nil { + return coroCallableContractCertificate{}, false, nil + } + decl, _ := fn.Syntax().(*ast.FuncDecl) + if decl == nil { + return coroCallableContractCertificate{}, false, nil + } + return parseCoroCallableContractDecl(decl) +} + +func parseCoroCallableContractDecl(decl *ast.FuncDecl) (coroCallableContractCertificate, bool, error) { + if decl == nil || decl.Doc == nil { + return coroCallableContractCertificate{}, false, nil + } + + var directive []string + var otherCoroDirectives []string + for _, comment := range decl.Doc.List { + if comment == nil { + continue + } + line := strings.TrimSpace(comment.Text) + if !strings.HasPrefix(line, "//") { + continue + } + payload := strings.TrimSpace(strings.TrimPrefix(line, "//")) + fields := strings.Fields(payload) + if len(fields) == 0 || fields[0] != "llgo:coro" { + continue + } + if len(fields) >= 2 && fields[1] == "workerresult" { + // Worker result projection is an orthogonal, compiler-owned wrapper + // contract. It neither changes callable behavior nor conflicts with a + // target-neutral callable contract on the same body. + continue + } + if len(fields) < 2 || fields[1] != "contract" { + otherCoroDirectives = append(otherCoroDirectives, payload) + continue + } + if directive != nil { + return coroCallableContractCertificate{}, false, fmt.Errorf("duplicate //llgo:coro contract directive") + } + directive = fields + } + if directive == nil { + return coroCallableContractCertificate{}, false, nil + } + if len(otherCoroDirectives) != 0 { + return coroCallableContractCertificate{}, false, fmt.Errorf( + "//llgo:coro contract conflicts with legacy directive %q", + otherCoroDirectives[0], + ) + } + if len(directive) < 3 { + return coroCallableContractCertificate{}, false, fmt.Errorf("//llgo:coro contract requires an ID") + } + if directive[2] != coroCallableContractIDForeignV1 { + if coroCallableContractBackendVocabulary(directive[2]) { + return coroCallableContractCertificate{}, false, fmt.Errorf("callable contract ID %q contains backend vocabulary", directive[2]) + } + return coroCallableContractCertificate{}, false, fmt.Errorf("unsupported callable contract ID %q", directive[2]) + } + + inferredScope := coroCallableContractScopeDeclaration + if decl.Body != nil { + inferredScope = coroCallableContractScopeWrapper + } + scope := inferredScope + values := make(map[string]string, 10) + for _, field := range directive[3:] { + key, value, ok := strings.Cut(field, "=") + if !ok || key == "" || value == "" { + if coroCallableContractBackendVocabulary(field) { + return coroCallableContractCertificate{}, false, fmt.Errorf("callable contract token %q contains backend vocabulary", field) + } + return coroCallableContractCertificate{}, false, fmt.Errorf("callable contract token %q must be key=value", field) + } + if _, duplicate := values[key]; duplicate { + return coroCallableContractCertificate{}, false, fmt.Errorf("duplicate callable contract key %q", key) + } + switch key { + case "scope", "progress", "affinity", "reentry", "memory", "abi", + "inline-progress", "inline-affinity", "inline-reentry", "inline-memory": + default: + if coroCallableContractBackendVocabulary(key) || coroCallableContractBackendVocabulary(value) { + return coroCallableContractCertificate{}, false, fmt.Errorf("callable contract field %q contains backend vocabulary", field) + } + return coroCallableContractCertificate{}, false, fmt.Errorf("unknown callable contract key %q", key) + } + values[key] = value + } + + if value, explicit := values["scope"]; explicit { + switch value { + case string(coroCallableContractScopeWrapper): + scope = coroCallableContractScopeWrapper + case string(coroCallableContractScopeDeclaration): + scope = coroCallableContractScopeDeclaration + default: + if coroCallableContractBackendVocabulary(value) { + return coroCallableContractCertificate{}, false, fmt.Errorf("callable contract scope %q contains backend vocabulary", value) + } + return coroCallableContractCertificate{}, false, fmt.Errorf("unknown callable contract scope %q", value) + } + if scope != inferredScope { + return coroCallableContractCertificate{}, false, fmt.Errorf( + "callable contract scope %q conflicts with exact %s FuncDecl", + scope, inferredScope, + ) + } + } + + for _, key := range []string{"progress", "affinity", "reentry", "memory"} { + if _, present := values[key]; !present { + return coroCallableContractCertificate{}, false, fmt.Errorf("callable contract requires explicit %s", key) + } + } + contract := coro.CallableContract{ID: coroCallableContractIDForeignV1} + if err := setCoroCallableProgress(&contract, values["progress"]); err != nil { + return coroCallableContractCertificate{}, false, err + } + if err := setCoroCallableAffinity(&contract, values["affinity"]); err != nil { + return coroCallableContractCertificate{}, false, err + } + if err := setCoroCallableReentry(&contract, values["reentry"]); err != nil { + return coroCallableContractCertificate{}, false, err + } + if err := setCoroCallableMemory(&contract, values["memory"]); err != nil { + return coroCallableContractCertificate{}, false, err + } + if err := contract.Validate(); err != nil { + return coroCallableContractCertificate{}, false, fmt.Errorf("invalid callable contract: %w", err) + } + inlineKeys := []string{"inline-progress", "inline-affinity", "inline-reentry", "inline-memory"} + inlineCount := 0 + for _, key := range inlineKeys { + if _, present := values[key]; present { + inlineCount++ + } + } + if inlineCount != 0 && inlineCount != len(inlineKeys) { + return coroCallableContractCertificate{}, false, fmt.Errorf( + "trusted-inline callable contract requires all of inline-progress, inline-affinity, inline-reentry, and inline-memory", + ) + } + trustedInline := coro.CallableContract{} + hasTrustedInline := inlineCount != 0 + if hasTrustedInline { + trustedInline.ID = coroCallableContractIDForeignV1 + if err := setCoroCallableProgress(&trustedInline, values["inline-progress"]); err != nil { + return coroCallableContractCertificate{}, false, fmt.Errorf("inline-progress: %w", err) + } + if err := setCoroCallableAffinity(&trustedInline, values["inline-affinity"]); err != nil { + return coroCallableContractCertificate{}, false, fmt.Errorf("inline-affinity: %w", err) + } + if err := setCoroCallableReentry(&trustedInline, values["inline-reentry"]); err != nil { + return coroCallableContractCertificate{}, false, fmt.Errorf("inline-reentry: %w", err) + } + if err := setCoroCallableMemory(&trustedInline, values["inline-memory"]); err != nil { + return coroCallableContractCertificate{}, false, fmt.Errorf("inline-memory: %w", err) + } + if err := coro.ValidateTrustedInlineCallableContractRefinement(trustedInline, contract); err != nil { + return coroCallableContractCertificate{}, false, err + } + } + abi := values["abi"] + if abi != "" { + if err := validateCoroCallableABI(abi); err != nil { + return coroCallableContractCertificate{}, false, err + } + } + + canonicalFields := []string{ + "llgo:coro", "contract", coroCallableContractIDForeignV1, + "scope=" + string(scope), + "progress=" + values["progress"], + "affinity=" + values["affinity"], + "reentry=" + values["reentry"], + "memory=" + values["memory"], + } + if hasTrustedInline { + canonicalFields = append(canonicalFields, + "inline-progress="+values["inline-progress"], + "inline-affinity="+values["inline-affinity"], + "inline-reentry="+values["inline-reentry"], + "inline-memory="+values["inline-memory"], + ) + } + if abi != "" { + canonicalFields = append(canonicalFields, "abi="+abi) + } + canonical := strings.Join(canonicalFields, " ") + return coroCallableContractCertificate{ + Contract: contract, TrustedInlineContract: trustedInline, HasTrustedInlineContract: hasTrustedInline, + Scope: scope, ABI: abi, Canonical: canonical, + }, true, nil +} + +func setCoroCallableProgress(contract *coro.CallableContract, value string) error { + switch value { + case "unknown": + contract.Progress = coro.ProgressUnknown + case "executor-safe": + contract.Progress = coro.ProgressExecutorSafe + case "may-block": + contract.Progress = coro.ProgressMayBlock + case "async-completion": + contract.Progress = coro.ProgressAsyncCompletion + case "no-return": + contract.Progress = coro.ProgressNoReturn + default: + return invalidCoroCallableContractValue("progress", value) + } + return nil +} + +func setCoroCallableAffinity(contract *coro.CallableContract, value string) error { + switch value { + case "unknown": + contract.Affinity = coro.AffinityUnknown + case "any-thread": + contract.Affinity = coro.AffinityAnyThread + case "caller-thread": + contract.Affinity = coro.AffinityCallerThread + case "owner-thread": + contract.Affinity = coro.AffinityOwnerThread + case "host-main": + // host-main is an abstract affinity class, not a host backend + // selection. Backend nouns remain rejected everywhere else below. + contract.Affinity = coro.AffinityHostMain + default: + return invalidCoroCallableContractValue("affinity", value) + } + return nil +} + +func setCoroCallableReentry(contract *coro.CallableContract, value string) error { + switch value { + case "unknown": + contract.Reentry = coro.ReentryUnknown + case "none": + contract.Reentry = coro.ReentryNone + case "managed-callback": + contract.Reentry = coro.ReentryManagedCallback + default: + return invalidCoroCallableContractValue("reentry", value) + } + return nil +} + +func setCoroCallableMemory(contract *coro.CallableContract, value string) error { + switch value { + case "unknown": + contract.Memory = coro.MemoryUnknown + case "by-value": + contract.Memory = coro.MemoryByValue + case "borrow-until-return": + contract.Memory = coro.MemoryBorrowUntilReturn + case "borrow-until-complete": + contract.Memory = coro.MemoryBorrowUntilComplete + case "retained": + contract.Memory = coro.MemoryRetained + default: + return invalidCoroCallableContractValue("memory", value) + } + return nil +} + +func invalidCoroCallableContractValue(key, value string) error { + if coroCallableContractBackendVocabulary(value) { + return fmt.Errorf("callable contract %s %q contains backend vocabulary", key, value) + } + return fmt.Errorf("unknown callable contract %s %q", key, value) +} + +func validateCoroCallableABI(value string) error { + if value == "" { + return fmt.Errorf("callable contract ABI must not be empty") + } + if !utf8.ValidString(value) { + return fmt.Errorf("callable contract ABI is not valid UTF-8") + } + if coroCallableContractBackendVocabulary(value) { + return fmt.Errorf("callable contract ABI %q contains backend vocabulary", value) + } + for _, char := range value { + if unicode.IsSpace(char) || unicode.IsControl(char) { + return fmt.Errorf("callable contract ABI %q is not a stable token", value) + } + } + return nil +} + +func coroCallableContractBackendVocabulary(value string) bool { + for _, word := range strings.FieldsFunc(strings.ToLower(value), func(r rune) bool { + return r != '-' && r != '_' && r != '.' && (r < '0' || r > '9') && (r < 'a' || r > 'z') + }) { + for _, part := range strings.FieldsFunc(word, func(r rune) bool { return r == '-' || r == '_' || r == '.' }) { + switch part { + case "worker", "poll", "host", "backend": + return true + } + } + } + return false +} diff --git a/cl/coro_callable_contract_freeze.go b/cl/coro_callable_contract_freeze.go new file mode 100644 index 0000000000..82ed19586e --- /dev/null +++ b/cl/coro_callable_contract_freeze.go @@ -0,0 +1,326 @@ +/* + * 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 ( + "fmt" + "sort" + "strconv" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +type CoroCallableContractScope = coro.CallableContractScope + +const ( + CoroCallableContractScopeDeclaration = coro.CallableContractScopeDeclaration + CoroCallableContractScopeWrapper = coro.CallableContractScopeWrapper +) + +const ( + coroCallableContractCertificateDomain = "llgo-coro-callable-certificate-v1" + coroCallableTypedABIDomain = "llgo-coro-callable-typed-abi-v1" +) + +// CoroCallableContractCertificate is the immutable, production-side binding +// between one exact canonical SSA function and its target-neutral callable +// contract. CallableABI is deliberately independent from PhysicalABISignature: +// an annotation may name an abstract transport ABI, while an ordinary typed +// declaration or wrapper gets a stable ABI derived from TypedABISignature. +// +// CanonicalFunctionIdentity and LinkIdentity are diagnostic/audit fields. The +// certificate ID binds them, the contract digest, scope, callable ABI and (for +// declarations) exact physical C symbol/ABI. Consumers must compare ID rather +// than recreating a certificate from these display fields. +type CoroCallableContractCertificate = coro.CallableContractCertificate + +type coroCallableFrozenShape struct { + kind int + physicalSymbol string + typedABISignature string +} + +// CoroCallableContractCertificate returns the construction-time certificate +// for fn. Alias lookup is exact and resolves only through the frozen alias map; +// package/name or physical-address guesses are never accepted. The returned +// value is a copy and cannot mutate the universe. +func (u *EmissionUniverse) CoroCallableContractCertificate(fn *ssa.Function) (certificate CoroCallableContractCertificate, certified bool, err error) { + if u == nil { + return CoroCallableContractCertificate{}, false, fmt.Errorf("coroutine callable contract certificate: nil emission universe") + } + if fn == nil { + return CoroCallableContractCertificate{}, false, fmt.Errorf("coroutine callable contract certificate: nil function") + } + canonical := u.canonicalAlias(fn) + if canonical == nil { + return CoroCallableContractCertificate{}, false, fmt.Errorf("coroutine callable contract certificate: function has cyclic canonical aliases") + } + if _, required := u.required[canonical]; !required { + return CoroCallableContractCertificate{}, false, fmt.Errorf("coroutine callable contract certificate: function %q is absent from the frozen emission universe", canonical.Name()) + } + certificate, certified = u.callableContracts[canonical] + return certificate, certified, nil +} + +// freezeCoroCallableContractCertificates converts exact source annotations +// into production certificates only after aliases, final managed symbols and +// link identities have all been frozen. Nothing downstream is permitted to +// reread comments or infer metadata from a code address. +func (u *EmissionUniverse) freezeCoroCallableContractCertificates() error { + if u == nil { + return fmt.Errorf("prepare emission universe: cannot freeze callable contracts in a nil universe") + } + + if u.callableContracts == nil { + u.callableContracts = make(map[*ssa.Function]CoroCallableContractCertificate) + } + shapes := make(map[*ssa.Function]coroCallableFrozenShape, len(u.functions)) + shapeErrors := make(map[*ssa.Function]error) + for _, function := range u.functions { + canonical := u.canonicalAlias(function) + if canonical == nil { + return fmt.Errorf("prepare emission universe: callable contract inventory contains cyclic aliases") + } + if canonical != function { + continue + } + shape, err := u.freezeCoroCallableShape(canonical) + if err != nil { + // A contract-free function must not make the new metadata layer + // observable. Preserve the error and reject it only if an exact + // annotation later claims this callable. + shapeErrors[canonical] = err + continue + } + shapes[canonical] = shape + } + + declarations := append([]*ssa.Function(nil), u.functions...) + for alias := range u.aliases { + declarations = append(declarations, alias) + } + declarations = stableUniqueFunctions(declarations) + sort.SliceStable(declarations, func(i, j int) bool { + return u.functionSortKey(declarations[i]) < u.functionSortKey(declarations[j]) + }) + + type exactAnnotation struct { + declaration *ssa.Function + canonical *ssa.Function + parsed coroCallableContractCertificate + } + annotations := make([]exactAnnotation, 0) + annotatedCanonical := make(map[*ssa.Function]*ssa.Function) + for _, declaration := range declarations { + parsed, present, err := coroCallableContractCertificateFor(declaration) + if err != nil { + return fmt.Errorf("prepare emission universe: callable contract on %q: %w", declaration.Name(), err) + } + if !present { + continue + } + canonical := u.canonicalAlias(declaration) + if canonical == nil { + return fmt.Errorf("prepare emission universe: callable contract on %q has cyclic canonical aliases", declaration.Name()) + } + if _, required := u.required[canonical]; !required { + return fmt.Errorf("prepare emission universe: callable contract on %q resolves outside the frozen emission universe", declaration.Name()) + } + if previous := annotatedCanonical[canonical]; previous != nil && previous != declaration { + return fmt.Errorf( + "prepare emission universe: callable contract aliases %q and %q resolve to the same exact canonical function", + previous.Name(), declaration.Name(), + ) + } + annotatedCanonical[canonical] = declaration + annotations = append(annotations, exactAnnotation{ + declaration: declaration, + canonical: canonical, + parsed: parsed, + }) + } + + for _, annotation := range annotations { + declaration, canonical, parsed := annotation.declaration, annotation.canonical, annotation.parsed + if err := shapeErrors[canonical]; err != nil { + return fmt.Errorf("prepare emission universe: callable contract on %q: %w", declaration.Name(), err) + } + shape, ok := shapes[canonical] + if !ok { + return fmt.Errorf("prepare emission universe: callable contract on %q has no frozen typed callable ABI", declaration.Name()) + } + scope := CoroCallableContractScope(parsed.Scope) + identity := CoroCallableIdentityCertificate{} + switch scope { + case CoroCallableContractScopeDeclaration: + if shape.kind != cFunc || shape.physicalSymbol == "" || shape.typedABISignature == "" { + return fmt.Errorf("prepare emission universe: callable declaration contract on %q requires an exact frozen C declaration and physical ABI", declaration.Name()) + } + var identityOK bool + identity, identityOK = u.callableIdentities[canonical] + if !identityOK { + return fmt.Errorf("prepare emission universe: callable declaration contract on %q has no total callable identity", declaration.Name()) + } + if err := identity.Validate(); err != nil { + return fmt.Errorf("prepare emission universe: callable declaration contract on %q has an invalid callable identity: %w", declaration.Name(), err) + } + case CoroCallableContractScopeWrapper: + if shape.kind != goFunc || len(canonical.Blocks) == 0 || shape.typedABISignature == "" { + return fmt.Errorf("prepare emission universe: callable wrapper contract on %q requires an exact bodyful Go wrapper and typed ABI", declaration.Name()) + } + default: + return fmt.Errorf("prepare emission universe: callable contract on %q has invalid frozen scope %q", declaration.Name(), scope) + } + + functionIdentity, linkIdentity := u.finalIdentity(canonical), u.linkIdentities[canonical] + callableABI, explicit := parsed.ABI, parsed.ABI != "" + if scope == CoroCallableContractScopeDeclaration { + functionIdentity, linkIdentity = identity.CanonicalFunctionIdentity, identity.LinkIdentity + callableABI, explicit = identity.CallableABI, identity.CallableABIExplicit + if parsed.ABI != "" && parsed.ABI != callableABI || parsed.ABI == "" && explicit { + return fmt.Errorf("prepare emission universe: callable contract on %q disagrees with its total callable identity ABI", declaration.Name()) + } + } else { + if linkIdentity == "" { + return fmt.Errorf("prepare emission universe: callable contract on %q has no frozen link identity", declaration.Name()) + } + if functionIdentity == "" || functionIdentity == "" || functionIdentity == "" { + return fmt.Errorf("prepare emission universe: callable contract on %q has no exact canonical function identity", declaration.Name()) + } + if !explicit { + if shape.typedABISignature == "" { + return fmt.Errorf("prepare emission universe: callable contract on %q requires an explicit ABI because its typed ABI is unavailable", declaration.Name()) + } + callableABI = derivedCoroCallableTypedABI(shape.typedABISignature) + } + } + contractDigest, err := coro.CallableContractBehaviorDigest(parsed.Contract.ID, parsed.Contract) + if err != nil { + return fmt.Errorf("prepare emission universe: callable contract on %q has no canonical behavior digest: %w", declaration.Name(), err) + } + // foreign.v1 is the source/schema version, not the identity of one + // behavior. Two declarations may legitimately use the same schema with + // different progress, affinity, reentry, or lifetime promises. Give the + // frozen behavior its own content-addressed ContractID before it can enter + // a compilation-wide CallableContractFacts catalog; otherwise the catalog + // would either reject the second contract as a duplicate or, worse, let a + // consumer confuse two different behaviors under the shared schema name. + frozenContract := parsed.Contract + frozenContract.ID = coro.ContractID(string(parsed.Contract.ID) + "/" + contractDigest) + frozenTrustedInline := coro.CallableContract{} + trustedInlineDigest := "" + if parsed.HasTrustedInlineContract { + if err := coro.ValidateTrustedInlineCallableContractRefinement(parsed.TrustedInlineContract, parsed.Contract); err != nil { + return fmt.Errorf("prepare emission universe: callable contract on %q has an invalid trusted-inline refinement: %w", declaration.Name(), err) + } + trustedInlineDigest, err = coro.CallableContractBehaviorDigest( + parsed.TrustedInlineContract.ID, parsed.TrustedInlineContract, + ) + if err != nil { + return fmt.Errorf("prepare emission universe: callable contract on %q has no canonical trusted-inline behavior digest: %w", declaration.Name(), err) + } + frozenTrustedInline = parsed.TrustedInlineContract + frozenTrustedInline.ID = coro.ContractID(string(parsed.TrustedInlineContract.ID) + "/" + trustedInlineDigest) + } + physicalSymbol, physicalABI := "", "" + if scope == CoroCallableContractScopeDeclaration { + physicalSymbol, physicalABI = shape.physicalSymbol, shape.typedABISignature + } + id := emissionDigest(framedEmissionKey( + coroCallableContractCertificateDomain, + functionIdentity, + linkIdentity, + string(scope), + callableABI, + strconv.FormatBool(explicit), + shape.typedABISignature, + physicalSymbol, + physicalABI, + contractDigest, + strconv.FormatBool(parsed.HasTrustedInlineContract), + trustedInlineDigest, + )) + if previous, exists := u.callableContracts[canonical]; exists { + return fmt.Errorf("prepare emission universe: duplicate frozen callable contract for %q (existing %q, replacement %q)", declaration.Name(), previous.ID, id) + } + frozen := CoroCallableContractCertificate{ + ID: id, + CanonicalFunctionIdentity: functionIdentity, + LinkIdentity: linkIdentity, + Contract: frozenContract, + ContractDigest: contractDigest, + TrustedInlineContract: frozenTrustedInline, + TrustedInlineContractDigest: trustedInlineDigest, + HasTrustedInlineContract: parsed.HasTrustedInlineContract, + Scope: scope, + CallableABI: callableABI, + CallableABIExplicit: explicit, + TypedABISignature: shape.typedABISignature, + PhysicalSymbol: physicalSymbol, + PhysicalABISignature: physicalABI, + } + if err := frozen.Validate(); err != nil { + return fmt.Errorf("prepare emission universe: callable contract on %q produced an invalid frozen certificate: %w", declaration.Name(), err) + } + if scope == CoroCallableContractScopeDeclaration { + if err := coro.ValidateCallableContractIdentity(identity, frozen); err != nil { + return fmt.Errorf("prepare emission universe: callable contract on %q: %w", declaration.Name(), err) + } + } + u.callableContracts[canonical] = frozen + } + return nil +} + +func (u *EmissionUniverse) freezeCoroCallableShape(fn *ssa.Function) (coroCallableFrozenShape, error) { + if u == nil || fn == nil || u.canonicalAlias(fn) != fn { + return coroCallableFrozenShape{}, fmt.Errorf("prepare emission universe: callable shape requires an exact canonical function") + } + owners := u.sortedUseOwners(fn) + if len(owners) == 0 { + return coroCallableFrozenShape{}, fmt.Errorf("prepare emission universe: callable shape for %q has no frozen owner", fn.Name()) + } + shape := coroCallableFrozenShape{kind: ignoredFunc} + have := false + firstOwner := "" + firstKey := "" + for _, owner := range owners { + key := u.finalKeys[emissionFunctionOwnerKey{function: fn, owner: owner}] + kind, symbol, signature, ok := splitManagedSymbolKey(key) + if !ok || kind == ignoredFunc || signature == "" { + continue + } + if !have { + shape.kind = kind + shape.physicalSymbol = symbol + shape.typedABISignature = signature + firstOwner = owner.identity + firstKey = key + have = true + } else if shape.kind != kind || shape.physicalSymbol != symbol || shape.typedABISignature != signature { + return coroCallableFrozenShape{}, fmt.Errorf( + "prepare emission universe: function %q has owner-dependent typed callable ABI: owner %q key %q conflicts with owner %q key %q", + fn.Name(), firstOwner, firstKey, owner.identity, key, + ) + } + } + if !have { + return coroCallableFrozenShape{}, nil + } + return shape, nil +} diff --git a/cl/coro_callable_contract_freeze_test.go b/cl/coro_callable_contract_freeze_test.go new file mode 100644 index 0000000000..aa53f75123 --- /dev/null +++ b/cl/coro_callable_contract_freeze_test.go @@ -0,0 +1,306 @@ +//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/ast" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +func TestEmissionUniverseFreezesCallableDeclarationAndWrapperContracts(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/callablecontracts", `package callablecontracts + +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none inline-memory=borrow-until-return +//go:linkname Foreign C.callable_contract_foreign +func Foreign(int) int + +//llgo:coro contract foreign.v1 scope=wrapper progress=async-completion affinity=host-main reentry=managed-callback memory=retained abi=word-call.v1/1 +func Wrapper(value int) int { return value + 1 } + +func Plain() {} +func root(value int) int { return Foreign(value) + Wrapper(value) } +`) + testProg.ssa.Build() + prog := llssa.NewProgram(nil) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{ + SSA: pkg.ssa, Files: []*ast.File{pkg.file}, Identity: "callable-contract-owner", + }}) + if err != nil { + t.Fatal(err) + } + + foreign, ok, err := universe.CoroCallableContractCertificate(pkg.ssa.Func("Foreign")) + if err != nil || !ok { + t.Fatalf("Foreign callable contract = %+v, %t, %v", foreign, ok, err) + } + if len(foreign.ID) != 64 || len(foreign.ContractDigest) != 64 || len(foreign.TrustedInlineContractDigest) != 64 || + foreign.CanonicalFunctionIdentity == "" || foreign.LinkIdentity == "" || + foreign.Scope != CoroCallableContractScopeDeclaration || + foreign.CallableABIExplicit || !strings.HasPrefix(foreign.CallableABI, "typed.v1/") || + foreign.TypedABISignature == "" || foreign.PhysicalSymbol != "callable_contract_foreign" || + foreign.PhysicalABISignature != foreign.TypedABISignature || + !strings.HasPrefix(string(foreign.Contract.ID), coroCallableContractIDForeignV1+"/") || + foreign.Contract.Progress != coro.ProgressMayBlock || !foreign.HasTrustedInlineContract || + !strings.HasPrefix(string(foreign.TrustedInlineContract.ID), coroCallableContractIDForeignV1+"/") || + foreign.TrustedInlineContract.Progress != coro.ProgressExecutorSafe || + foreign.TrustedInlineContract.Memory != coro.MemoryBorrowUntilReturn || + foreign.TrustedInlineContract.ID == foreign.Contract.ID { + t.Fatalf("Foreign frozen callable contract = %+v", foreign) + } + + wrapper, ok, err := universe.CoroCallableContractCertificate(pkg.ssa.Func("Wrapper")) + if err != nil || !ok { + t.Fatalf("Wrapper callable contract = %+v, %t, %v", wrapper, ok, err) + } + if len(wrapper.ID) != 64 || len(wrapper.ContractDigest) != 64 || wrapper.ID == foreign.ID || + wrapper.Scope != CoroCallableContractScopeWrapper || + !wrapper.CallableABIExplicit || wrapper.CallableABI != "word-call.v1/1" || + wrapper.TypedABISignature == "" || wrapper.PhysicalSymbol != "" || wrapper.PhysicalABISignature != "" || + wrapper.Contract.Progress != coro.ProgressAsyncCompletion || wrapper.HasTrustedInlineContract || + wrapper.TrustedInlineContract != (coro.CallableContract{}) || wrapper.TrustedInlineContractDigest != "" { + t.Fatalf("Wrapper frozen callable contract = %+v", wrapper) + } + if wrapper.Contract.ID == foreign.Contract.ID { + t.Fatalf("different callable behaviors share frozen contract ID %q", wrapper.Contract.ID) + } + if _, ok, err := universe.CoroCallableContractCertificate(pkg.ssa.Func("Plain")); err != nil || ok { + t.Fatalf("Plain callable contract = %t, %v; want absent", ok, err) + } + + // The accessor must remain a construction-time snapshot after source AST + // mutation; downstream code may not reopen comments. + declaration := pkg.ssa.Func("Foreign").Syntax().(*ast.FuncDecl) + declaration.Doc.List[0].Text = "//llgo:coro contract foreign.v1 progress=executor-safe affinity=caller-thread reentry=none memory=by-value" + again, ok, err := universe.CoroCallableContractCertificate(pkg.ssa.Func("Foreign")) + if err != nil || !ok || again != foreign { + t.Fatalf("mutated source changed frozen callable contract: %+v, %t, %v; want %+v", again, ok, err, foreign) + } +} + +func TestEmissionUniverseCallableTrustedInlineRefinementBindsCertificateIdentity(t *testing.T) { + build := func(inline string) CoroCallableContractCertificate { + t.Helper() + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/callableinlineidentity", `package callableinlineidentity +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete`+inline+` +//go:linkname Foreign C.callable_inline_identity +func Foreign(int) int +func root(value int) int { return Foreign(value) } +`) + testProg.ssa.Build() + prog := llssa.NewProgram(nil) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{ + SSA: pkg.ssa, Files: []*ast.File{pkg.file}, Identity: "callable-inline-owner", + }}) + if err != nil { + t.Fatal(err) + } + certificate, ok, err := universe.CoroCallableContractCertificate(pkg.ssa.Func("Foreign")) + if err != nil || !ok { + t.Fatalf("callable certificate = %+v, %t, %v", certificate, ok, err) + } + return certificate + } + without := build("") + with := build(" inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none inline-memory=borrow-until-return") + if without.Contract != with.Contract || without.ContractDigest != with.ContractDigest || + without.CallableABI != with.CallableABI || without.TypedABISignature != with.TypedABISignature { + t.Fatalf("trusted-inline refinement changed default behavior/ABI: without=%+v with=%+v", without, with) + } + if without.HasTrustedInlineContract || without.TrustedInlineContract != (coro.CallableContract{}) || + without.TrustedInlineContractDigest != "" { + t.Fatalf("absent trusted-inline refinement retained data: %+v", without) + } + if !with.HasTrustedInlineContract || len(with.TrustedInlineContractDigest) != 64 || + with.TrustedInlineContract.Progress != coro.ProgressExecutorSafe || with.ID == without.ID { + t.Fatalf("trusted-inline refinement did not bind certificate identity: without=%+v with=%+v", without, with) + } +} + +func TestEmissionUniverseCallableContractAccessorCanonicalizesExactGoAlias(t *testing.T) { + testProg := newEmissionTestProgram() + declaration := testProg.addPackage(t, "example.com/emission/callablealias", `package callablealias +//go:linkname Hook +func Hook(int) int +func Root(value int) int { return Hook(value) } +`) + definition := testProg.addPackage(t, "example.com/emission/callablealiasimpl", `package callablealiasimpl +//llgo:coro contract foreign.v1 scope=wrapper progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +//go:linkname implementation example.com/emission/callablealias.Hook +func implementation(value int) int { return value + 1 } +`) + testProg.ssa.Build() + prog := llssa.NewProgram(nil) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{ + {SSA: declaration.ssa, Files: []*ast.File{declaration.file}}, + {SSA: definition.ssa, Files: []*ast.File{definition.file}}, + }) + if err != nil { + t.Fatal(err) + } + alias := declaration.ssa.Func("Hook") + canonical := definition.ssa.Func("implementation") + resolved, body := universe.Resolve(alias) + if !body || resolved != canonical { + t.Fatalf("Resolve(alias) = %v, %t; want exact definition %v", resolved, body, canonical) + } + fromAlias, aliasOK, aliasErr := universe.CoroCallableContractCertificate(alias) + fromCanonical, canonicalOK, canonicalErr := universe.CoroCallableContractCertificate(canonical) + if aliasErr != nil || canonicalErr != nil || !aliasOK || !canonicalOK || fromAlias != fromCanonical { + t.Fatalf("alias/canonical contracts = (%+v,%t,%v) and (%+v,%t,%v)", fromAlias, aliasOK, aliasErr, fromCanonical, canonicalOK, canonicalErr) + } + if fromAlias.Scope != CoroCallableContractScopeWrapper || fromAlias.PhysicalSymbol != "" { + t.Fatalf("alias contract = %+v; want exact Go wrapper", fromAlias) + } +} + +func TestEmissionUniverseCallableContractsFailClosedOnInvalidScope(t *testing.T) { + for _, test := range []struct { + name string + source string + wantErr string + }{ + { + name: "bodyless Go declaration has no physical C ABI", + source: `package badcallable +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete +func Missing(int) int +func root() { _ = Missing(1) } +`, + wantErr: "requires an exact frozen C declaration and physical ABI", + }, + } { + t.Run(test.name, func(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/badcallable", test.source) + testProg.ssa.Build() + prog := llssa.NewProgram(nil) + defer prog.Dispose() + _, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{ + SSA: pkg.ssa, Files: []*ast.File{pkg.file}, Identity: "bad-callable-owner", + }}) + if err == nil || !strings.Contains(err.Error(), test.wantErr) { + t.Fatalf("PrepareEmissionUniverse error = %v; want %q", err, test.wantErr) + } + }) + } +} + +func TestEmissionUniverseCallableIdentityAllowsRepeatedPhysicalTargets(t *testing.T) { + testProg := newEmissionTestProgram() + firstPkg := testProg.addPackage(t, "example.com/emission/callableidentityrepeat/first", `package first +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete +//go:linkname First C.callable_identity_repeat +func First(int) int +func root() { _ = First(1) } +`) + secondPkg := testProg.addPackage(t, "example.com/emission/callableidentityrepeat/second", `package second +//go:linkname Second C.callable_identity_repeat +func Second(int) int +func root() { _ = Second(2) } +`) + differentPkg := testProg.addPackage(t, "example.com/emission/callableidentityrepeat/different", `package different +//go:linkname DifferentABI C.callable_identity_repeat +func DifferentABI(string) string +func root() { _ = DifferentABI("") } +`) + testProg.ssa.Build() + prog := llssa.NewProgram(nil) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{ + {SSA: firstPkg.ssa, Files: []*ast.File{firstPkg.file}, Identity: "callable-identity-repeat-first"}, + {SSA: secondPkg.ssa, Files: []*ast.File{secondPkg.file}, Identity: "callable-identity-repeat-second"}, + {SSA: differentPkg.ssa, Files: []*ast.File{differentPkg.file}, Identity: "callable-identity-repeat-different"}, + }) + if err != nil { + t.Fatal(err) + } + + identities := make(map[string]CoroCallableIdentityCertificate) + functions := map[string]*ssa.Function{ + "First": firstPkg.ssa.Func("First"), "Second": secondPkg.ssa.Func("Second"), + "DifferentABI": differentPkg.ssa.Func("DifferentABI"), + } + for _, name := range []string{"First", "Second", "DifferentABI"} { + identity, ok, err := universe.CoroCallableIdentityCertificate(functions[name]) + if err != nil || !ok { + t.Fatalf("%s identity = %+v, %t, %v", name, identity, ok, err) + } + if err := identity.Validate(); err != nil || identity.PhysicalSymbol != "callable_identity_repeat" { + t.Fatalf("%s identity = %+v: %v", name, identity, err) + } + if previous, duplicate := identities[identity.ID]; duplicate { + t.Fatalf("%s and another exact declaration share identity %+v", name, previous) + } + identities[identity.ID] = identity + } + first, _, _ := universe.CoroCallableIdentityCertificate(functions["First"]) + second, _, _ := universe.CoroCallableIdentityCertificate(functions["Second"]) + different, _, _ := universe.CoroCallableIdentityCertificate(functions["DifferentABI"]) + if first.PhysicalABISignature != second.PhysicalABISignature || + first.PhysicalABISignature == different.PhysicalABISignature { + t.Fatalf("repeated physical ABI inventory = first:%q second:%q different:%q", first.PhysicalABISignature, second.PhysicalABISignature, different.PhysicalABISignature) + } + contract, ok, err := universe.CoroCallableContractCertificate(functions["First"]) + if err != nil || !ok { + t.Fatalf("First contract = %+v, %t, %v", contract, ok, err) + } + if err := coro.ValidateCallableContractIdentity(first, contract); err != nil { + t.Fatal(err) + } + for _, name := range []string{"Second", "DifferentABI"} { + if _, ok, err := universe.CoroCallableContractCertificate(functions[name]); err != nil || ok { + t.Fatalf("%s behavior contract = %t, %v; want identity-only", name, ok, err) + } + } +} + +func TestEmissionUniverseCallableContractsRejectDuplicateExactAlias(t *testing.T) { + testProg := newEmissionTestProgram() + declaration := testProg.addPackage(t, "example.com/emission/callabledupalias", `package callabledupalias +//llgo:coro contract foreign.v1 scope=declaration progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +//go:linkname Hook +func Hook(int) int +func Root(value int) int { return Hook(value) } +`) + definition := testProg.addPackage(t, "example.com/emission/callabledupaliasimpl", `package callabledupaliasimpl +//llgo:coro contract foreign.v1 scope=wrapper progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +//go:linkname implementation example.com/emission/callabledupalias.Hook +func implementation(value int) int { return value + 1 } +`) + testProg.ssa.Build() + prog := llssa.NewProgram(nil) + defer prog.Dispose() + _, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{ + {SSA: declaration.ssa, Files: []*ast.File{declaration.file}}, + {SSA: definition.ssa, Files: []*ast.File{definition.file}}, + }) + if err == nil || !strings.Contains(err.Error(), "same exact canonical function") { + t.Fatalf("PrepareEmissionUniverse error = %v; want duplicate exact alias rejection", err) + } +} diff --git a/cl/coro_callable_contract_test.go b/cl/coro_callable_contract_test.go new file mode 100644 index 0000000000..8d99c56fa3 --- /dev/null +++ b/cl/coro_callable_contract_test.go @@ -0,0 +1,190 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" +) + +func TestCoroCallableContractParsesExactDeclarationAndWrapperScopes(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package callable + +//llgo:coro contract foreign.v1 memory=borrow-until-complete progress=may-block reentry=none affinity=any-thread inline-memory=borrow-until-return inline-reentry=none inline-affinity=any-thread inline-progress=executor-safe +func Foreign(int) int + +//llgo:coro contract foreign.v1 reentry=managed-callback abi=word-call.v1/1 scope=wrapper affinity=host-main memory=retained progress=async-completion +func Wrapper(v int) int { return v } + +//llgo:coro contract foreign.v1 scope=declaration progress=unknown affinity=unknown reentry=unknown memory=unknown +func Unknown() + +func Plain() {} +`) + + foreign, ok, err := coroCallableContractCertificateFor(ssaPkg.Func("Foreign")) + if err != nil || !ok { + t.Fatalf("Foreign contract = %+v, %t, %v", foreign, ok, err) + } + if foreign.Scope != coroCallableContractScopeDeclaration || foreign.ABI != "" || + foreign.Contract.ID != coroCallableContractIDForeignV1 || + foreign.Contract.Progress != coro.ProgressMayBlock || + foreign.Contract.Affinity != coro.AffinityAnyThread || + foreign.Contract.Reentry != coro.ReentryNone || + foreign.Contract.Memory != coro.MemoryBorrowUntilComplete || + !foreign.HasTrustedInlineContract || + foreign.TrustedInlineContract.Progress != coro.ProgressExecutorSafe || + foreign.TrustedInlineContract.Affinity != coro.AffinityAnyThread || + foreign.TrustedInlineContract.Reentry != coro.ReentryNone || + foreign.TrustedInlineContract.Memory != coro.MemoryBorrowUntilReturn { + t.Fatalf("Foreign contract = %+v", foreign) + } + if want := "llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none inline-memory=borrow-until-return"; foreign.Canonical != want { + t.Fatalf("Foreign canonical = %q, want %q", foreign.Canonical, want) + } + + wrapper, ok, err := coroCallableContractCertificateFor(ssaPkg.Func("Wrapper")) + if err != nil || !ok { + t.Fatalf("Wrapper contract = %+v, %t, %v", wrapper, ok, err) + } + if wrapper.Scope != coroCallableContractScopeWrapper || wrapper.ABI != "word-call.v1/1" || + wrapper.Contract.Progress != coro.ProgressAsyncCompletion || + wrapper.Contract.Affinity != coro.AffinityHostMain || + wrapper.Contract.Reentry != coro.ReentryManagedCallback || + wrapper.Contract.Memory != coro.MemoryRetained { + t.Fatalf("Wrapper contract = %+v", wrapper) + } + if want := "llgo:coro contract foreign.v1 scope=wrapper progress=async-completion affinity=host-main reentry=managed-callback memory=retained abi=word-call.v1/1"; wrapper.Canonical != want { + t.Fatalf("Wrapper canonical = %q, want %q", wrapper.Canonical, want) + } + + unknown, ok, err := coroCallableContractCertificateFor(ssaPkg.Func("Unknown")) + if err != nil || !ok { + t.Fatalf("Unknown contract = %+v, %t, %v", unknown, ok, err) + } + if unknown.Contract.Progress != coro.ProgressUnknown || + unknown.Contract.Affinity != coro.AffinityUnknown || + unknown.Contract.Reentry != coro.ReentryUnknown || + unknown.Contract.Memory != coro.MemoryUnknown { + t.Fatalf("explicit unknown contract = %+v", unknown) + } + if plain, ok, err := coroCallableContractCertificateFor(ssaPkg.Func("Plain")); err != nil || ok || plain != (coroCallableContractCertificate{}) { + t.Fatalf("Plain contract = %+v, %t, %v; want absent", plain, ok, err) + } + if nilContract, ok, err := coroCallableContractCertificateFor(nil); err != nil || ok || nilContract != (coroCallableContractCertificate{}) { + t.Fatalf("nil contract = %+v, %t, %v; want absent", nilContract, ok, err) + } +} + +func TestCoroCallableContractRejectsMalformedAndBackendSpecificClaims(t *testing.T) { + valid := "progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete" + for _, test := range []struct { + name string + directive string + body string + want string + }{ + {name: "missing ID", directive: "//llgo:coro contract", want: "requires an ID"}, + {name: "unknown ID", directive: "//llgo:coro contract native.v1 " + valid, want: "unsupported callable contract ID"}, + {name: "backend ID", directive: "//llgo:coro contract worker.v1 " + valid, want: "backend vocabulary"}, + {name: "missing progress", directive: "//llgo:coro contract foreign.v1 affinity=any-thread reentry=none memory=by-value", want: "requires explicit progress"}, + {name: "missing affinity", directive: "//llgo:coro contract foreign.v1 progress=may-block reentry=none memory=by-value", want: "requires explicit affinity"}, + {name: "missing reentry", directive: "//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread memory=by-value", want: "requires explicit reentry"}, + {name: "missing memory", directive: "//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none", want: "requires explicit memory"}, + {name: "inline missing progress", directive: "//llgo:coro contract foreign.v1 " + valid + " inline-affinity=any-thread inline-reentry=none inline-memory=by-value", want: "requires all of inline-progress"}, + {name: "inline missing affinity", directive: "//llgo:coro contract foreign.v1 " + valid + " inline-progress=executor-safe inline-reentry=none inline-memory=by-value", want: "requires all of inline-progress"}, + {name: "inline missing reentry", directive: "//llgo:coro contract foreign.v1 " + valid + " inline-progress=executor-safe inline-affinity=any-thread inline-memory=by-value", want: "requires all of inline-progress"}, + {name: "inline missing memory", directive: "//llgo:coro contract foreign.v1 " + valid + " inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none", want: "requires all of inline-progress"}, + {name: "inline progress may block", directive: "//llgo:coro contract foreign.v1 " + valid + " inline-progress=may-block inline-affinity=any-thread inline-reentry=none inline-memory=by-value", want: "not executor-safe"}, + {name: "inline widens reentry", directive: "//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete inline-progress=executor-safe inline-affinity=any-thread inline-reentry=managed-callback inline-memory=borrow-until-return", want: "not a safe refinement"}, + {name: "inline widens memory", directive: "//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=by-value inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none inline-memory=borrow-until-return", want: "not a safe refinement"}, + {name: "duplicate key", directive: "//llgo:coro contract foreign.v1 " + valid + " progress=may-block", want: `duplicate callable contract key "progress"`}, + {name: "unknown key", directive: "//llgo:coro contract foreign.v1 " + valid + " latency=unbounded", want: `unknown callable contract key "latency"`}, + {name: "empty ABI", directive: "//llgo:coro contract foreign.v1 " + valid + " abi=", want: "must be key=value"}, + {name: "duplicate ABI", directive: "//llgo:coro contract foreign.v1 " + valid + " abi=word-call.v1/1 abi=word-call.v1/1", want: `duplicate callable contract key "abi"`}, + {name: "worker ABI", directive: "//llgo:coro contract foreign.v1 " + valid + " abi=worker-call.v1/1", want: "backend vocabulary"}, + {name: "poll ABI", directive: "//llgo:coro contract foreign.v1 " + valid + " abi=poll.v1", want: "backend vocabulary"}, + {name: "worker backend", directive: "//llgo:coro contract foreign.v1 " + valid + " backend=worker", want: "backend vocabulary"}, + {name: "poll backend value", directive: "//llgo:coro contract foreign.v1 progress=poll affinity=any-thread reentry=none memory=by-value", want: "backend vocabulary"}, + {name: "host token", directive: "//llgo:coro contract foreign.v1 " + valid + " host", want: "backend vocabulary"}, + {name: "unknown progress", directive: "//llgo:coro contract foreign.v1 progress=sometimes affinity=any-thread reentry=none memory=by-value", want: "unknown callable contract progress"}, + {name: "unknown affinity", directive: "//llgo:coro contract foreign.v1 progress=may-block affinity=wherever reentry=none memory=by-value", want: "unknown callable contract affinity"}, + {name: "unknown reentry", directive: "//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=recursive memory=by-value", want: "unknown callable contract reentry"}, + {name: "unknown memory", directive: "//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=shared", want: "unknown callable contract memory"}, + {name: "wrapper scope on declaration", directive: "//llgo:coro contract foreign.v1 scope=wrapper " + valid, want: "conflicts with exact declaration FuncDecl"}, + {name: "declaration scope on wrapper", directive: "//llgo:coro contract foreign.v1 scope=declaration " + valid, body: " {}", want: "conflicts with exact wrapper FuncDecl"}, + {name: "unknown scope", directive: "//llgo:coro contract foreign.v1 scope=callsite " + valid, want: "unknown callable contract scope"}, + {name: "malformed assignment", directive: "//llgo:coro contract foreign.v1 progress =may-block affinity=any-thread reentry=none memory=by-value", want: "must be key=value"}, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, "package malformed\n\n"+test.directive+"\nfunc Target()"+test.body+"\n") + certificate, ok, err := coroCallableContractCertificateFor(ssaPkg.Func("Target")) + if err == nil || ok || certificate != (coroCallableContractCertificate{}) || !strings.Contains(err.Error(), test.want) { + t.Fatalf("contract = %+v, %t, %v; want error containing %q", certificate, ok, err, test.want) + } + }) + } +} + +func TestCoroCallableContractRejectsDuplicateAndLegacyDirectiveConflicts(t *testing.T) { + for _, test := range []struct { + name string + comment string + want string + }{ + { + name: "duplicate contract", + comment: `//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=by-value +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=by-value`, + want: "duplicate //llgo:coro contract directive", + }, + { + name: "legacy worker conflict", + comment: `//llgo:coro worker +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=by-value`, + want: "conflicts with legacy directive", + }, + { + name: "legacy noblock conflict", + comment: `//llgo:coro contract foreign.v1 progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +//llgo:coro noblock`, + want: "conflicts with legacy directive", + }, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, "package conflict\n\n"+test.comment+"\nfunc Target()\n") + _, ok, err := coroCallableContractCertificateFor(ssaPkg.Func("Target")) + if err == nil || ok || !strings.Contains(err.Error(), test.want) { + t.Fatalf("contract = %t, %v; want error containing %q", ok, err, test.want) + } + }) + } + + // An old directive by itself remains the old parser's responsibility. The + // new layer neither accepts it as a callable contract nor rejects it early. + ssaPkg, _, _ := buildGoSSAPkg(t, `package legacy +//llgo:coro worker +func Worker() +`) + if certificate, ok, err := coroCallableContractCertificateFor(ssaPkg.Func("Worker")); err != nil || ok || certificate != (coroCallableContractCertificate{}) { + t.Fatalf("legacy-only contract = %+v, %t, %v; want absent", certificate, ok, err) + } +} diff --git a/cl/coro_callable_identity.go b/cl/coro_callable_identity.go new file mode 100644 index 0000000000..9b4bafff62 --- /dev/null +++ b/cl/coro_callable_identity.go @@ -0,0 +1,171 @@ +/* + * 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 ( + "fmt" + "sort" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +type CoroCallableIdentityCertificate = coro.CallableIdentityCertificate + +// CoroCallableIdentityCertificate returns the immutable identity of one exact +// managed C declaration. It grants no execution policy and is never recovered +// from a physical address or symbol-name lookup. +func (u *EmissionUniverse) CoroCallableIdentityCertificate(fn *ssa.Function) (certificate CoroCallableIdentityCertificate, certified bool, err error) { + if u == nil { + return CoroCallableIdentityCertificate{}, false, fmt.Errorf("coroutine callable identity certificate: nil emission universe") + } + if fn == nil { + return CoroCallableIdentityCertificate{}, false, fmt.Errorf("coroutine callable identity certificate: nil function") + } + canonical := u.canonicalAlias(fn) + if canonical == nil { + return CoroCallableIdentityCertificate{}, false, fmt.Errorf("coroutine callable identity certificate: function has cyclic canonical aliases") + } + if _, required := u.required[canonical]; !required { + return CoroCallableIdentityCertificate{}, false, fmt.Errorf( + "coroutine callable identity certificate: function %q is absent from the frozen managed emission universe", canonical.Name(), + ) + } + certificate, certified = u.callableIdentities[canonical] + return certificate, certified, nil +} + +// freezeCoroCallableIdentityCertificates inventories every exact C +// declaration already retained by the managed emission universe. Repeated +// physical (symbol, ABI) pairs remain distinct DeclarationRefs because the +// certificate digest also binds canonical and link identities. This scan does +// not globally reject ABI conflicts between different declarations. +func (u *EmissionUniverse) freezeCoroCallableIdentityCertificates() error { + if u == nil { + return fmt.Errorf("prepare emission universe: cannot freeze callable identities in a nil universe") + } + if u.callableIdentities == nil { + u.callableIdentities = make(map[*ssa.Function]CoroCallableIdentityCertificate) + } + + declarations := append([]*ssa.Function(nil), u.functions...) + for alias := range u.aliases { + declarations = append(declarations, alias) + } + declarations = stableUniqueFunctions(declarations) + sort.SliceStable(declarations, func(i, j int) bool { + return u.functionSortKey(declarations[i]) < u.functionSortKey(declarations[j]) + }) + annotations := make(map[*ssa.Function]coroCallableContractCertificate) + annotationOwners := make(map[*ssa.Function]*ssa.Function) + for _, declaration := range declarations { + parsed, present, err := coroCallableContractCertificateFor(declaration) + if err != nil { + return fmt.Errorf("prepare emission universe: callable identity annotation on %q: %w", declaration.Name(), err) + } + if !present || parsed.Scope != coroCallableContractScopeDeclaration { + continue + } + canonical := u.canonicalAlias(declaration) + if canonical == nil { + return fmt.Errorf("prepare emission universe: callable identity annotation on %q has cyclic canonical aliases", declaration.Name()) + } + if previous := annotationOwners[canonical]; previous != nil && previous != declaration { + return fmt.Errorf( + "prepare emission universe: callable contract aliases %q and %q resolve to the same exact canonical function", + previous.Name(), declaration.Name(), + ) + } + annotationOwners[canonical] = declaration + annotations[canonical] = parsed + } + + for _, function := range u.functions { + canonical := u.canonicalAlias(function) + if canonical == nil { + return fmt.Errorf("prepare emission universe: callable identity inventory contains cyclic aliases") + } + if canonical != function { + continue + } + shape, err := u.freezeCoroCallableShape(canonical) + if err != nil { + // Total callable identity is frozen only for managed C declarations. + // A Pkg-nil Go wrapper may intentionally have one owner-scoped symbol + // per consuming module; that is not a C declaration ambiguity and must + // remain invisible unless the wrapper carries an explicit callable + // contract (the contract freezer retains and diagnoses its shape error). + managedC := false + for _, owner := range u.sortedUseOwners(canonical) { + kind, _, _, ok := splitManagedSymbolKey(u.finalKeys[emissionFunctionOwnerKey{function: canonical, owner: owner}]) + managedC = managedC || ok && kind == cFunc + } + if managedC { + return fmt.Errorf("prepare emission universe: callable identity on %q: %w", canonical.Name(), err) + } + continue + } + if shape.kind != cFunc { + continue + } + if shape.physicalSymbol == "" || shape.typedABISignature == "" { + return fmt.Errorf("prepare emission universe: managed C declaration %q has no frozen physical symbol or ABI", canonical.Name()) + } + baseFunctionIdentity := u.finalIdentity(canonical) + if baseFunctionIdentity == "" || baseFunctionIdentity == "" || baseFunctionIdentity == "" { + return fmt.Errorf("prepare emission universe: managed C declaration %q has no exact canonical function identity", canonical.Name()) + } + // finalIdentity intentionally models the managed physical key and may be + // shared by two Go declarations naming the same C symbol+ABI. Bind the + // stable exact SSA declaration key as well so each one gets a distinct + // DeclarationRef without changing or disambiguating the physical symbol. + functionIdentity := framedEmissionKey("cl-callable-exact-declaration-v1", u.functionSortKey(canonical)) + linkIdentity := u.linkIdentities[canonical] + if linkIdentity == "" { + return fmt.Errorf("prepare emission universe: managed C declaration %q has no frozen link identity", canonical.Name()) + } + + callableABI := "" + explicit := false + if annotation, ok := annotations[canonical]; ok && annotation.ABI != "" { + callableABI, explicit = annotation.ABI, true + } + if callableABI == "" { + callableABI = derivedCoroCallableTypedABI(shape.typedABISignature) + } + certificate, err := coro.FreezeCallableIdentityCertificate(coro.CallableIdentityCertificate{ + CanonicalFunctionIdentity: functionIdentity, + LinkIdentity: linkIdentity, + CallableABI: callableABI, + CallableABIExplicit: explicit, + TypedABISignature: shape.typedABISignature, + PhysicalSymbol: shape.physicalSymbol, + PhysicalABISignature: shape.typedABISignature, + Origin: coro.CallableIdentityOriginManagedCDeclaration, + Evidence: coro.CallableIdentityEvidenceManagedFinalShape, + }) + if err != nil { + return fmt.Errorf("prepare emission universe: freeze callable identity on %q: %w", canonical.Name(), err) + } + u.callableIdentities[canonical] = certificate + } + return nil +} + +func derivedCoroCallableTypedABI(signature string) string { + return "typed.v1/" + emissionDigest(framedEmissionKey(coroCallableTypedABIDomain, signature)) +} diff --git a/cl/coro_callable_shadow.go b/cl/coro_callable_shadow.go new file mode 100644 index 0000000000..031fe97664 --- /dev/null +++ b/cl/coro_callable_shadow.go @@ -0,0 +1,988 @@ +/* + * 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 ( + "fmt" + "go/ast" + "go/types" + "sort" + "strconv" + "strings" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// CoroCallableShadowABI is producer metadata, not a property recovered from a +// uintptr. A FuncPCABI0 producer publishes the exact foreign-call family and +// word arity that its target declaration permits. Consumers may compare this +// value with their own call ABI, but may never manufacture it from the emitted +// address. +type CoroCallableShadowABI struct { + Family string + WordArgs int +} + +const coroCallableShadowWorkerSyscallFamily = "word-call.v1" + +// CoroCallableShadow is the compiler-only fact paired with one exact +// FuncPCABI0 SSA result. Producer is deliberately part of the identity: two +// syntactically independent publications of the same text address remain two +// facts even though Target and PhysicalSymbol match. +type CoroCallableShadow struct { + Producer *ssa.Call + SourceTarget *ssa.Function + Target *ssa.Function + PhysicalSymbol string + ABI CoroCallableShadowABI + // ForeignPointerResultMask marks worker result words that the exact C + // declaration promises are pointers to non-Go storage. The fact is injected + // at FuncPCABI0 formation and never reconstructed from the returned uintptr. + ForeignPointerResultMask uint8 + ContractCertificateID string + LegacyWorkerAddressCompat bool +} + +func coroWorkerWordCallableABI(arity int) string { + return coroCallableShadowWorkerSyscallFamily + "/" + strconv.Itoa(arity) +} + +type coroWorkerWordCallableABIShape struct { + wordArgs int + foreignPointerResultMask uint8 +} + +const coroWorkerForeignPointerResultR1 = "+foreign-pointer-result=r1" + +func parseCoroWorkerWordCallableABI(value string) (coroWorkerWordCallableABIShape, bool) { + var shape coroWorkerWordCallableABIShape + prefix := coroCallableShadowWorkerSyscallFamily + "/" + if !strings.HasPrefix(value, prefix) { + return shape, false + } + text := strings.TrimPrefix(value, prefix) + if strings.HasSuffix(text, coroWorkerForeignPointerResultR1) { + shape.foreignPointerResultMask = 1 + text = strings.TrimSuffix(text, coroWorkerForeignPointerResultR1) + } + arity, err := strconv.Atoi(text) + if err != nil || arity < 0 || arity > coroWorkerMaxArgsV1 || text != strconv.Itoa(arity) { + return coroWorkerWordCallableABIShape{}, false + } + shape.wordArgs = arity + return shape, true +} + +func coroWorkerCallableContractCompatible(contract coro.CallableContract) bool { + return contract.Progress == coro.ProgressMayBlock && + contract.Affinity == coro.AffinityAnyThread && + contract.Reentry == coro.ReentryNone && + contract.Memory != coro.MemoryUnknown && contract.Memory != coro.MemoryRetained +} + +// coroWorkerCallableDeclarationContractArity is used only while the emission +// universe is still discovering address-only FuncPCABI0 operands. It parses an +// exact declaration; it does not issue a capability. The production shadow is +// injected later from CoroCallableContractCertificate after aliases and ABI +// identities have frozen. +func coroWorkerCallableDeclarationContractArity(fn *ssa.Function) (int, bool, error) { + parsed, present, err := coroCallableContractCertificateFor(fn) + if err != nil || !present { + return 0, false, err + } + if parsed.Scope != coroCallableContractScopeDeclaration || + !coroWorkerCallableContractCompatible(parsed.Contract) || parsed.ABI == "" { + return 0, false, nil + } + shape, ok := parseCoroWorkerWordCallableABI(parsed.ABI) + return shape.wordArgs, ok, nil +} + +// coroWorkerAddressOnlyDeclaration reports whether fn is one exact declaration +// whose Go signature exists only so FuncPCABI0 can publish a physical C text +// address. Its callable ABI is the explicit word-call ABI carried beside that +// address, not the otherwise-unused Go declaration signature. In particular, +// a catalog declaration such as func libc_write_trampoline() must not make the +// ordinary typed C ABI inventory believe that C.write has a second zero-argument +// calling convention. +// +// Keep this classification deliberately narrower than "has a callable +// contract": only an exact bodyless trampoline plus either a valid explicit +// word-call ABI or the legacy workeraddr spelling is address-only. Ordinary +// typed declarations, malformed aliases, and contracts without a word-call ABI +// remain in the physical ABI collision inventory. +func (u *EmissionUniverse) coroWorkerAddressOnlyDeclaration(fn *ssa.Function) (bool, error) { + if u == nil || fn == nil { + return false, nil + } + canonical := u.canonicalAlias(fn) + if canonical == nil { + return false, fmt.Errorf("worker address-only declaration has cyclic canonical aliases") + } + if !coroWorkerAddressAliasDeclaration(canonical) { + return false, nil + } + physical := extractTrampolineCName(canonical.Name()) + if physical == "" { + return false, nil + } + physical = remapTrampolineCNameForTarget(u.prog.Target(), physical) + + if certificate, certified := u.callableContracts[canonical]; certified { + _, wordABI := parseCoroWorkerWordCallableABI(certificate.CallableABI) + if certificate.Scope != CoroCallableContractScopeDeclaration || + !certificate.CallableABIExplicit || !wordABI { + return false, nil + } + if certificate.PhysicalSymbol != physical { + return false, fmt.Errorf( + "worker address-only declaration %q contract physical symbol %q differs from trampoline symbol %q", + canonical.Name(), certificate.PhysicalSymbol, physical, + ) + } + return true, nil + } + + directive, err := coroForeignCallDirectiveFor(canonical) + if err != nil { + return false, err + } + if directive != coroForeignCallWorkerAddress { + return false, nil + } + if _, err := coroWorkerAddressDirectiveArity(canonical); err != nil { + return false, err + } + return true, nil +} + +// coroWorkerCallableTarget freezes the only two accepted producer sources: +// the target-neutral declaration contract, and the temporary workeraddr +// migration spelling. It consumes no uintptr and performs no address lookup. +func coroWorkerCallableTarget( + universe *EmissionUniverse, + sourceTarget, target *ssa.Function, +) (coroWorkerAddressTarget, string, error) { + if universe == nil || sourceTarget == nil || target == nil { + return coroWorkerAddressTarget{}, "invalid-funcpcabi0-target", nil + } + decl, _ := target.Syntax().(*ast.FuncDecl) + if decl == nil || decl.Body != nil || decl.Recv != nil || target.Signature == nil || + target.Signature.Recv() != nil || target.Signature.Variadic() || len(target.Blocks) != 0 { + return coroWorkerAddressTarget{}, "", fmt.Errorf( + "worker callable target %q must be an exact bodyless non-method declaration", target.Name(), + ) + } + physical := extractTrampolineCName(target.Name()) + if physical == "" { + return coroWorkerAddressTarget{}, "", fmt.Errorf( + "worker callable target %q has no FuncPCABI0 C trampoline lowering", target.Name(), + ) + } + physical = remapTrampolineCNameForTarget(universe.prog.Target(), physical) + + // Address-only trampoline declarations are deliberately absent from the + // managed required set. The contract freezer nevertheless owns the exact + // canonical-keyed map; this internal consumer must not route through the + // public accessor, whose required-function check is correct for ordinary + // managed callers. + certificate, certified := universe.callableContracts[target] + if certified { + if certificate.Scope != CoroCallableContractScopeDeclaration { + return coroWorkerAddressTarget{}, "callable-contract-is-not-a-declaration", nil + } + if !coroWorkerCallableContractCompatible(certificate.Contract) { + return coroWorkerAddressTarget{}, "callable-contract-is-not-worker-compatible", nil + } + if !certificate.CallableABIExplicit { + return coroWorkerAddressTarget{}, "callable-contract-requires-explicit-word-abi", nil + } + shape, ok := parseCoroWorkerWordCallableABI(certificate.CallableABI) + if !ok { + return coroWorkerAddressTarget{}, "callable-contract-has-incompatible-word-abi", nil + } + if certificate.PhysicalSymbol != physical { + return coroWorkerAddressTarget{}, "", fmt.Errorf( + "worker callable target %q contract physical symbol %q differs from FuncPCABI0 symbol %q", + target.Name(), certificate.PhysicalSymbol, physical, + ) + } + return coroWorkerAddressTarget{ + target: target, + physicalSymbol: physical, + workerArity: shape.wordArgs, + foreignPointerResultMask: shape.foreignPointerResultMask, + contractCertificateID: certificate.ID, + legacyWorkerAddressOnly: false, + }, "", nil + } + // Address-only declarations are intentionally removed from the managed + // function inventory before the general callable-contract freezer runs. + // Freeze their exact source contract into the producer shadow here, while + // the SSA target and its typed trampoline ABI are still available. This is + // still producer-side metadata; no emitted uintptr participates. + parsed, present, err := coroCallableContractCertificateFor(target) + if err != nil { + return coroWorkerAddressTarget{}, "", err + } + if present { + if parsed.Scope != coroCallableContractScopeDeclaration { + return coroWorkerAddressTarget{}, "callable-contract-is-not-a-declaration", nil + } + if !coroWorkerCallableContractCompatible(parsed.Contract) { + return coroWorkerAddressTarget{}, "callable-contract-is-not-worker-compatible", nil + } + shape, ok := parseCoroWorkerWordCallableABI(parsed.ABI) + if !ok { + return coroWorkerAddressTarget{}, "callable-contract-has-incompatible-word-abi", nil + } + behaviorDigest, err := coro.CallableContractBehaviorDigest(parsed.Contract.ID, parsed.Contract) + if err != nil { + return coroWorkerAddressTarget{}, "", err + } + certificateID := emissionDigest(framedEmissionKey( + "llgo-coro-address-only-callable-contract-v1", + coroWorkerAddressFunctionIdentity(universe, sourceTarget), + coroWorkerAddressFunctionIdentity(universe, target), + physical, + structuralGoLinknameABITypeKey(target.Signature), + parsed.Canonical, + behaviorDigest, + )) + return coroWorkerAddressTarget{ + target: target, + physicalSymbol: physical, + workerArity: shape.wordArgs, + foreignPointerResultMask: shape.foreignPointerResultMask, + contractCertificateID: certificateID, + legacyWorkerAddressOnly: false, + }, "", nil + } + + directive, err := coroForeignCallDirectiveFor(target) + if err != nil { + return coroWorkerAddressTarget{}, "", fmt.Errorf("worker-address target %q: %w", target.Name(), err) + } + if directive != coroForeignCallWorkerAddress { + return coroWorkerAddressTarget{}, "target-lacks-workeraddr", nil + } + arity, err := coroWorkerAddressDirectiveArity(target) + if err != nil { + return coroWorkerAddressTarget{}, "", err + } + return coroWorkerAddressTarget{ + target: target, + physicalSymbol: physical, + workerArity: arity, + contractCertificateID: "legacy-workeraddr.v0", + legacyWorkerAddressOnly: true, + }, "", nil +} + +// CoroCallableShadowIncomingEdge records one exact static call that supplies a +// private parameter carrier. An uncertified edge remains in the inventory so a +// later SSA-plan join can prove that it is inactive in managed execution. This +// is what permits a shared syscall wrapper to have both a safe and an +// incompatible caller without silently trusting the latter. +type CoroCallableShadowIncomingEdge struct { + Call *ssa.Call + Carrier *ssa.Function + Parameter int + Candidates []CoroCallableShadow + Certified bool + Reason string +} + +// CoroCallableShadowSink is the result at one exact llgo.syscall call. For a +// direct producer, Certified means that the producer ABI exactly matches the +// sink. For a private parameter carrier, it means that at least one exact +// incoming edge is certified; all other edges are retained in Incoming and +// must be narrowed by the eventual whole-plan verifier. +type CoroCallableShadowSink struct { + Call *ssa.Call + ABI CoroCallableShadowABI + Candidates []CoroCallableShadow + Incoming []CoroCallableShadowIncomingEdge + Certified bool + Reason string +} + +// CoroCallableShadowAnalysis is an immutable, reportable producer-forward +// analysis. It intentionally has no address-keyed lookup API. +type CoroCallableShadowAnalysis struct { + producers map[*ssa.Call]CoroCallableShadow + rejected map[*ssa.Call]string + sinks map[*ssa.Call]CoroCallableShadowSink +} + +// Producer returns the shadow injected at an exact FuncPCABI0 producer. +func (a *CoroCallableShadowAnalysis) Producer(call *ssa.Call) (CoroCallableShadow, bool) { + if a == nil || call == nil { + return CoroCallableShadow{}, false + } + shadow, ok := a.producers[call] + return shadow, ok +} + +// ProducerRejection returns the fail-closed reason for a FuncPCABI0 call that +// could not publish a callable shadow. +func (a *CoroCallableShadowAnalysis) ProducerRejection(call *ssa.Call) (string, bool) { + if a == nil || call == nil { + return "", false + } + reason, ok := a.rejected[call] + return reason, ok +} + +// Sink returns a copy of the producer-forward result for an exact +// llgo.syscall call. +func (a *CoroCallableShadowAnalysis) Sink(call ssa.CallInstruction) (CoroCallableShadowSink, bool) { + if a == nil || call == nil { + return CoroCallableShadowSink{}, false + } + direct, ok := call.(*ssa.Call) + if !ok { + return CoroCallableShadowSink{}, false + } + sink, ok := a.sinks[direct] + if !ok { + return CoroCallableShadowSink{}, false + } + sink.Candidates = cloneCoroCallableShadows(sink.Candidates) + sink.Incoming = cloneCoroCallableShadowIncoming(sink.Incoming) + return sink, true +} + +type coroCallableShadowFactKey struct { + value ssa.Value + producer *ssa.Call +} + +type coroCallableShadowBuilder struct { + universe *EmissionUniverse + result *CoroCallableShadowAnalysis + + incoming map[*ssa.Function][]*ssa.Call + escaped map[*ssa.Function]bool + closed map[*ssa.Function]string + + facts map[ssa.Value]map[*ssa.Call]CoroCallableShadow + failures map[ssa.Value]string + queue []coroCallableShadowFactKey + sinkABI map[*ssa.Call]CoroCallableShadowABI +} + +// AnalyzeCoroCallableShadows builds the compiler-side shadow flow from exact +// FuncPCABI0 producers to exact llgo.syscall consumers. The accepted transport +// is intentionally small: an SSA value may flow directly to the consumer or +// through uintptr parameters of closed, private, statically called Go +// functions. No integer operation, store, return, indirect call, exported +// entry, or escaped carrier preserves the shadow. +func AnalyzeCoroCallableShadows(universe *EmissionUniverse) (*CoroCallableShadowAnalysis, error) { + if universe == nil { + return nil, fmt.Errorf("callable shadow analysis requires a prepared emission universe") + } + b := &coroCallableShadowBuilder{ + universe: universe, + result: &CoroCallableShadowAnalysis{ + producers: make(map[*ssa.Call]CoroCallableShadow), + rejected: make(map[*ssa.Call]string), + sinks: make(map[*ssa.Call]CoroCallableShadowSink), + }, + incoming: make(map[*ssa.Function][]*ssa.Call), + escaped: make(map[*ssa.Function]bool), + closed: make(map[*ssa.Function]string), + facts: make(map[ssa.Value]map[*ssa.Call]CoroCallableShadow), + failures: make(map[ssa.Value]string), + sinkABI: make(map[*ssa.Call]CoroCallableShadowABI), + } + b.indexCallsAndEscapes() + if err := b.seedProducersAndSinks(); err != nil { + return nil, err + } + if err := b.propagate(); err != nil { + return nil, err + } + if err := b.finishSinks(); err != nil { + return nil, err + } + return b.result, nil +} + +func (b *coroCallableShadowBuilder) indexCallsAndEscapes() { + for _, fn := range b.universe.functions { + if fn == nil || len(fn.Blocks) == 0 || b.universe.canonicalAlias(fn) != fn { + continue + } + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + if call, ok := instruction.(*ssa.Call); ok && call.Common() != nil && !call.Common().IsInvoke() { + if target, resolved := b.universe.Resolve(call.Common().StaticCallee()); resolved && target != nil { + b.incoming[target] = append(b.incoming[target], call) + } + } + for _, operand := range instruction.Operands(nil) { + if operand == nil { + continue + } + reference, ok := (*operand).(*ssa.Function) + if !ok { + continue + } + target, resolved := b.universe.Resolve(reference) + if !resolved || target == nil { + continue + } + call, direct := instruction.(*ssa.Call) + if direct && call.Common() != nil && !call.Common().IsInvoke() { + callee, calleeResolved := b.universe.Resolve(call.Common().StaticCallee()) + if calleeResolved && callee == target { + continue + } + } + b.escaped[target] = true + } + } + } + } +} + +func (b *coroCallableShadowBuilder) seedProducersAndSinks() error { + physicalTargets := make(map[string]CoroCallableShadow) + for _, fn := range b.universe.functions { + if fn == nil || len(fn.Blocks) == 0 || b.universe.canonicalAlias(fn) != fn { + continue + } + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Common() == nil || call.Common().IsInvoke() || call.Common().StaticCallee() == nil { + continue + } + opcode, intrinsic, err := b.universe.coroIntrinsicOpcode(call.Common().StaticCallee()) + if err != nil { + continue + } + if !intrinsic { + continue + } + switch { + case opcode == llgoFuncPCABI0: + shadow, reason, err := b.injectProducer(call) + if err != nil { + return nilErrorWithCallableShadowContext(call, err) + } + if reason != "" { + b.result.rejected[call] = reason + b.failures[call] = reason + continue + } + if previous, exists := physicalTargets[shadow.PhysicalSymbol]; exists && + (previous.Target != shadow.Target || previous.ABI != shadow.ABI || + previous.ForeignPointerResultMask != shadow.ForeignPointerResultMask || + previous.ContractCertificateID != shadow.ContractCertificateID || + previous.LegacyWorkerAddressCompat != shadow.LegacyWorkerAddressCompat) { + return fmt.Errorf( + "callable shadow analysis: physical target %q has conflicting producer targets or ABIs", + shadow.PhysicalSymbol, + ) + } + physicalTargets[shadow.PhysicalSymbol] = shadow + b.result.producers[call] = shadow + b.addFact(call, shadow) + case isLLGoSyscallIntrinsic(opcode): + arity := len(call.Common().Args) - 1 + abi := CoroCallableShadowABI{Family: coroCallableShadowWorkerSyscallFamily, WordArgs: arity} + b.sinkABI[call] = abi + if err := validateCoroWorkerSyscallIntrinsicCallSite(call); err != nil { + b.result.sinks[call] = CoroCallableShadowSink{Call: call, ABI: abi, Reason: "invalid-syscall-call-shape"} + } + } + } + } + } + return nil +} + +func nilErrorWithCallableShadowContext(call *ssa.Call, err error) error { + if err == nil { + return nil + } + return fmt.Errorf("callable shadow producer %q: %w", call.String(), err) +} + +func (b *coroCallableShadowBuilder) injectProducer(call *ssa.Call) (CoroCallableShadow, string, error) { + if err := b.universe.validateCoroFuncPCABI0CallSite(call); err != nil { + return CoroCallableShadow{}, "invalid-funcpcabi0-operand", nil + } + args := call.Common().Args + if len(args) != 1 { + return CoroCallableShadow{}, "invalid-funcpcabi0-arity", nil + } + boxed, ok := args[0].(*ssa.MakeInterface) + if !ok { + return CoroCallableShadow{}, "dynamic-funcpcabi0-operand", nil + } + source, ok := boxed.X.(*ssa.Function) + if !ok || source == nil || source.Parent() != nil || len(source.FreeVars) != 0 { + return CoroCallableShadow{}, "dynamic-funcpcabi0-target", nil + } + target := b.universe.canonicalAlias(source) + if target == nil || target.Parent() != nil || len(target.FreeVars) != 0 { + return CoroCallableShadow{}, "uncanonical-funcpcabi0-target", nil + } + if b.universe.Contains(target) { + background, classified, err := b.universe.FunctionBackground(target) + if err != nil { + return CoroCallableShadow{}, "", err + } + if classified && background == llssa.InGo { + // FuncPCABI0 and FuncPCABIInternal are also Go runtime primitives for + // publishing managed entry PCs (for example, map algorithm-table and + // race-instrumentation callbacks). Such a producer is useful code-address + // metadata, but it is not a foreign worker-call capability. Keep an exact + // rejection on the producer so an unrelated publication cannot abort the + // global shadow inventory while any path into llgo.syscall still fails + // closed without a worker certificate. + return CoroCallableShadow{}, "managed-go-code-address-is-not-worker-callable", nil + } + } + capability, reason, err := coroWorkerCallableTarget(b.universe, source, target) + if err != nil { + return CoroCallableShadow{}, "", err + } + if reason != "" { + return CoroCallableShadow{}, reason, nil + } + return CoroCallableShadow{ + Producer: call, + SourceTarget: source, + Target: target, + PhysicalSymbol: capability.physicalSymbol, + ForeignPointerResultMask: capability.foreignPointerResultMask, + ContractCertificateID: capability.contractCertificateID, + LegacyWorkerAddressCompat: capability.legacyWorkerAddressOnly, + ABI: CoroCallableShadowABI{ + Family: coroCallableShadowWorkerSyscallFamily, + WordArgs: capability.workerArity, + }, + }, "", nil +} + +func (b *coroCallableShadowBuilder) addFact(value ssa.Value, shadow CoroCallableShadow) { + if value == nil || shadow.Producer == nil { + return + } + byProducer := b.facts[value] + if byProducer == nil { + byProducer = make(map[*ssa.Call]CoroCallableShadow) + b.facts[value] = byProducer + } + if _, exists := byProducer[shadow.Producer]; exists { + return + } + byProducer[shadow.Producer] = shadow + b.queue = append(b.queue, coroCallableShadowFactKey{value: value, producer: shadow.Producer}) +} + +func (b *coroCallableShadowBuilder) propagate() error { + for len(b.queue) != 0 { + item := b.queue[0] + b.queue = b.queue[1:] + shadow, exists := b.facts[item.value][item.producer] + if !exists { + continue + } + refs := item.value.Referrers() + if refs == nil { + continue + } + for _, ref := range *refs { + if _, debug := ref.(*ssa.DebugRef); debug { + continue + } + call, isCall := ref.(*ssa.Call) + if !isCall || call.Common() == nil || call.Common().IsInvoke() || call.Common().StaticCallee() == nil { + reason := "callable-shadow-escape-or-unsupported-operation" + if _, arithmetic := ref.(*ssa.BinOp); arithmetic { + reason = "arithmetic-destroys-callable-shadow" + } + b.rejectDerivedValue(ref, reason) + continue + } + indices := coroCallableShadowArgumentIndices(call, item.value) + if len(indices) == 0 { + continue + } + opcode, intrinsic, err := b.universe.coroIntrinsicOpcode(call.Common().StaticCallee()) + if err != nil { + b.rejectDerivedValue(call, "callable-shadow-passed-outside-universe") + continue + } + if intrinsic && isLLGoSyscallIntrinsic(opcode) { + // The fact is consumed only from argument zero. Any other use is + // deliberately not a callable transport. + for _, index := range indices { + if index != 0 { + b.rejectDerivedValue(call, "callable-shadow-used-as-syscall-data") + } + } + continue + } + if intrinsic { + b.rejectDerivedValue(call, "callable-shadow-passed-to-intrinsic") + continue + } + carrier, resolved := b.universe.Resolve(call.Common().StaticCallee()) + if !resolved || carrier == nil { + b.rejectDerivedValue(call, "callable-shadow-passed-outside-universe") + continue + } + closed, _, err := b.closedCarrier(carrier) + if err != nil { + return err + } + if !closed { + continue + } + for _, index := range indices { + if index < 0 || index >= len(carrier.Params) || !coroWorkerUintptrType(carrier.Params[index].Type()) { + continue + } + b.addFact(carrier.Params[index], shadow) + } + } + } + return nil +} + +func (b *coroCallableShadowBuilder) rejectDerivedValue(instruction ssa.Instruction, reason string) { + value, ok := instruction.(ssa.Value) + if !ok || value == nil { + return + } + if _, exists := b.failures[value]; !exists { + b.failures[value] = reason + } +} + +func coroCallableShadowArgumentIndices(call *ssa.Call, value ssa.Value) []int { + if call == nil || call.Common() == nil || value == nil { + return nil + } + var indices []int + for index, argument := range call.Common().Args { + if argument == value { + indices = append(indices, index) + } + } + return indices +} + +func (b *coroCallableShadowBuilder) closedCarrier(fn *ssa.Function) (bool, string, error) { + if reason, cached := b.closed[fn]; cached { + return reason == "", reason, nil + } + reason := "" + switch { + case fn == nil || fn.Parent() != nil || len(fn.Blocks) == 0 || len(fn.FreeVars) != 0: + reason = "open-or-escaped-parameter-carrier" + case fn.Signature == nil || fn.Signature.Recv() != nil || fn.Signature.Variadic() || + fn.TypeParams() != nil || len(fn.TypeArgs()) != 0: + reason = "open-or-escaped-parameter-carrier" + case b.escaped[fn]: + reason = "open-or-escaped-parameter-carrier" + default: + object, _ := fn.Object().(*types.Func) + decl, _ := fn.Syntax().(*ast.FuncDecl) + if object == nil || object.Exported() || decl == nil || decl.Body == nil { + reason = "open-or-escaped-parameter-carrier" + } + } + if reason == "" { + background, classified, err := b.universe.FunctionBackground(fn) + if err != nil { + return false, "", err + } + if !classified || background != llssa.InGo { + reason = "open-or-escaped-parameter-carrier" + } + } + if reason == "" { + directive, err := coroRawABIDirective(fn, b.universe) + if err != nil { + return false, "", err + } + if directive != "" { + reason = "open-or-escaped-parameter-carrier" + } + } + b.closed[fn] = reason + return reason == "", reason, nil +} + +func (b *coroCallableShadowBuilder) finishSinks() error { + for call, abi := range b.sinkABI { + if existing, invalid := b.result.sinks[call]; invalid && existing.Reason != "" { + continue + } + sink := CoroCallableShadowSink{Call: call, ABI: abi} + if call.Common() == nil || len(call.Common().Args) == 0 { + sink.Reason = "invalid-syscall-call-shape" + b.result.sinks[call] = sink + continue + } + source := call.Common().Args[0] + sink.Candidates = b.sortedFacts(source) + if parameter, ok := source.(*ssa.Parameter); ok { + incoming, certified, reason, err := b.parameterInventory(parameter, abi, make(map[*ssa.Parameter]bool)) + if err != nil { + return err + } + sink.Incoming = incoming + sink.Certified = certified + sink.Reason = reason + } else { + sink.Certified = coroCallableShadowAllCompatible(sink.Candidates, abi) + if !sink.Certified { + sink.Reason = b.failureReason(source, abi) + } + } + sortCoroCallableShadowIncoming(sink.Incoming) + b.result.sinks[call] = sink + } + return nil +} + +func (b *coroCallableShadowBuilder) parameterInventory( + parameter *ssa.Parameter, + abi CoroCallableShadowABI, + visiting map[*ssa.Parameter]bool, +) ([]CoroCallableShadowIncomingEdge, bool, string, error) { + if parameter == nil || parameter.Parent() == nil { + return nil, false, "open-or-escaped-parameter-carrier", nil + } + if visiting[parameter] { + return nil, false, "cyclic-parameter-carrier", nil + } + visiting[parameter] = true + defer delete(visiting, parameter) + + owner := parameter.Parent() + closed, closedReason, err := b.closedCarrier(owner) + if err != nil { + return nil, false, "", err + } + index := -1 + for candidateIndex, candidate := range owner.Params { + if candidate == parameter { + index = candidateIndex + break + } + } + if index < 0 || !closed { + return b.openCarrierInventory(owner, index, abi, closedReason), false, closedReason, nil + } + calls := b.incoming[owner] + if len(calls) == 0 { + return nil, false, "parameter-carrier-has-no-static-incoming", nil + } + var inventory []CoroCallableShadowIncomingEdge + anyCertified := false + for _, call := range calls { + edge := CoroCallableShadowIncomingEdge{Call: call, Carrier: owner, Parameter: index} + if call == nil || call.Common() == nil || index >= len(call.Common().Args) { + edge.Reason = "invalid-static-incoming-edge" + inventory = append(inventory, edge) + continue + } + source := call.Common().Args[index] + edge.Candidates = b.sortedFacts(source) + if upstream, ok := source.(*ssa.Parameter); ok { + nested, nestedCertified, nestedReason, err := b.parameterInventory(upstream, abi, visiting) + if err != nil { + return nil, false, "", err + } + inventory = append(inventory, nested...) + edge.Certified = nestedCertified && coroCallableShadowAnyCompatible(edge.Candidates, abi) + if !edge.Certified { + edge.Reason = nestedReason + } + } else { + edge.Certified = coroCallableShadowAllCompatible(edge.Candidates, abi) + if !edge.Certified { + edge.Reason = b.failureReason(source, abi) + } + } + if edge.Certified { + anyCertified = true + } + inventory = append(inventory, edge) + } + if anyCertified { + return inventory, true, "", nil + } + reason := "parameter-carrier-has-no-certified-incoming" + if len(inventory) == 1 && inventory[0].Reason != "" { + reason = inventory[0].Reason + } + return inventory, false, reason, nil +} + +func (b *coroCallableShadowBuilder) openCarrierInventory( + owner *ssa.Function, + parameter int, + abi CoroCallableShadowABI, + reason string, +) []CoroCallableShadowIncomingEdge { + if owner == nil || parameter < 0 { + return nil + } + var inventory []CoroCallableShadowIncomingEdge + for _, call := range b.incoming[owner] { + edge := CoroCallableShadowIncomingEdge{ + Call: call, + Carrier: owner, + Parameter: parameter, + Reason: reason, + } + if call != nil && call.Common() != nil && parameter < len(call.Common().Args) { + edge.Candidates = b.sortedFacts(call.Common().Args[parameter]) + if reason == "" && !coroCallableShadowAllCompatible(edge.Candidates, abi) { + edge.Reason = b.failureReason(call.Common().Args[parameter], abi) + } + } + inventory = append(inventory, edge) + } + return inventory +} + +func (b *coroCallableShadowBuilder) sortedFacts(value ssa.Value) []CoroCallableShadow { + byProducer := b.facts[value] + shadows := make([]CoroCallableShadow, 0, len(byProducer)) + for _, shadow := range byProducer { + shadows = append(shadows, shadow) + } + sort.SliceStable(shadows, func(i, j int) bool { + return coroCallableShadowSortKey(shadows[i]) < coroCallableShadowSortKey(shadows[j]) + }) + return shadows +} + +func (b *coroCallableShadowBuilder) failureReason(value ssa.Value, abi CoroCallableShadowABI) string { + if reason := b.failures[value]; reason != "" { + return reason + } + candidates := b.sortedFacts(value) + if len(candidates) != 0 && !coroCallableShadowAllCompatible(candidates, abi) { + return "callable-shadow-abi-mismatch" + } + if _, arithmetic := value.(*ssa.BinOp); arithmetic { + return "arithmetic-destroys-callable-shadow" + } + if _, parameter := value.(*ssa.Parameter); parameter { + return "parameter-carrier-has-no-certified-incoming" + } + return "missing-exact-callable-shadow" +} + +func coroCallableShadowAnyCompatible(candidates []CoroCallableShadow, abi CoroCallableShadowABI) bool { + for _, candidate := range candidates { + if candidate.ABI == abi { + return true + } + } + return false +} + +func coroCallableShadowAllCompatible(candidates []CoroCallableShadow, abi CoroCallableShadowABI) bool { + if len(candidates) == 0 { + return false + } + for _, candidate := range candidates { + if candidate.ABI != abi { + return false + } + } + return true +} + +func coroCallableShadowSortKey(shadow CoroCallableShadow) string { + parent := "" + block, instruction := -1, -1 + if shadow.Producer != nil { + if shadow.Producer.Parent() != nil { + parent = shadow.Producer.Parent().String() + } + block, instruction = coroWorkerSyscallInstructionSite(shadow.Producer) + } + target := "" + if shadow.Target != nil { + target = shadow.Target.String() + } + return fmt.Sprintf("%s/%08d/%08d/%s/%s/%08d", parent, block, instruction, target, shadow.PhysicalSymbol, shadow.ABI.WordArgs) +} + +func sortCoroCallableShadowIncoming(edges []CoroCallableShadowIncomingEdge) { + sort.SliceStable(edges, func(i, j int) bool { + left, right := edges[i], edges[j] + leftCarrier, rightCarrier := "", "" + if left.Carrier != nil { + leftCarrier = left.Carrier.String() + } + if right.Carrier != nil { + rightCarrier = right.Carrier.String() + } + if leftCarrier != rightCarrier { + return leftCarrier < rightCarrier + } + leftParent, rightParent := "", "" + if left.Call != nil && left.Call.Parent() != nil { + leftParent = left.Call.Parent().String() + } + if right.Call != nil && right.Call.Parent() != nil { + rightParent = right.Call.Parent().String() + } + if leftParent != rightParent { + return leftParent < rightParent + } + leftBlock, leftInstruction := coroWorkerSyscallInstructionSite(left.Call) + rightBlock, rightInstruction := coroWorkerSyscallInstructionSite(right.Call) + if leftBlock != rightBlock { + return leftBlock < rightBlock + } + if leftInstruction != rightInstruction { + return leftInstruction < rightInstruction + } + return left.Parameter < right.Parameter + }) +} + +func cloneCoroCallableShadows(src []CoroCallableShadow) []CoroCallableShadow { + return append([]CoroCallableShadow(nil), src...) +} + +func cloneCoroCallableShadowIncoming(src []CoroCallableShadowIncomingEdge) []CoroCallableShadowIncomingEdge { + dst := make([]CoroCallableShadowIncomingEdge, len(src)) + for index, edge := range src { + edge.Candidates = cloneCoroCallableShadows(edge.Candidates) + dst[index] = edge + } + return dst +} diff --git a/cl/coro_callable_shadow_test.go b/cl/coro_callable_shadow_test.go new file mode 100644 index 0000000000..d4256d612b --- /dev/null +++ b/cl/coro_callable_shadow_test.go @@ -0,0 +1,379 @@ +//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/ast" + "strings" + "testing" +) + +const coroCallableContractWorkerFixture = `package contractworker + +//llgo:link funcPCABI0 llgo.funcPCABI0 +func funcPCABI0(fn any) uintptr + +//llgo:link raw llgo.syscall +func raw(fn, a0 uintptr) (uintptr, uintptr, uintptr) + +//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete abi=word-call.v1/1 +func libc_contract_worker_v1_trampoline() + +func Fixed(a0 uintptr) uintptr { + r1, _, _ := raw(funcPCABI0(libc_contract_worker_v1_trampoline), a0) + return r1 +} +` + +const coroCallableShadowManagedCodeAddressFixture = `package managedcodeaddr + +//llgo:link funcPCABI0 llgo.funcPCABI0 +func funcPCABI0(fn any) uintptr + +//llgo:link funcPCABIInternal llgo.funcPCABIInternal +func funcPCABIInternal(fn any) uintptr + +//llgo:link raw llgo.syscall +func raw(fn, a0 uintptr) (uintptr, uintptr, uintptr) + +func managedTarget() {} + +func ObserveABI0() uintptr { + return funcPCABI0(managedTarget) +} + +func ObserveABIInternal() uintptr { + return funcPCABIInternal(managedTarget) +} + +func MisuseAsWorker(a0 uintptr) uintptr { + r1, _, _ := raw(funcPCABIInternal(managedTarget), a0) + return r1 +} +` + +func TestCoroCallableShadowClassifiesManagedFuncPCAsCodeAddressOnly(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/managedcodeaddr", coroCallableShadowManagedCodeAddressFixture) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + t.Fatal(err) + } + analysis, err := AnalyzeCoroCallableShadows(universe) + if err != nil { + t.Fatal(err) + } + + const wantReason = "managed-go-code-address-is-not-worker-callable" + for _, name := range []string{"ObserveABI0", "ObserveABIInternal", "MisuseAsWorker"} { + producer := exactIntrinsicOpcodeCall(t, universe, pkg.ssa.Func(name), llgoFuncPCABI0) + if shadow, ok := analysis.Producer(producer); ok { + t.Fatalf("%s managed FuncPC producer unexpectedly received worker shadow %+v", name, shadow) + } + if reason, ok := analysis.ProducerRejection(producer); !ok || reason != wantReason { + t.Fatalf("%s managed FuncPC rejection = %q, %t; want %q", name, reason, ok, wantReason) + } + } + + call := exactWorkerSyscallCall(t, universe, pkg.ssa.Func("MisuseAsWorker")) + sink, ok := analysis.Sink(call) + if !ok || sink.Certified || sink.Reason != wantReason || len(sink.Candidates) != 0 { + t.Fatalf("managed FuncPC worker sink = %+v, %t; want exact fail-closed rejection", sink, ok) + } + if certificate, certified, err := universe.CoroWorkerSyscallCertificate(call); err != nil || certified || certificate.ID != "" { + t.Fatalf("managed FuncPC worker certificate = %+v, %t, %v; want absent, false, nil", certificate, certified, err) + } +} + +func TestCoroCallableShadowFlowsForwardFromFuncPCABI0(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/callableshadow", coroWorkerSyscallCapabilityFixture) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + t.Fatal(err) + } + analysis, err := AnalyzeCoroCallableShadows(universe) + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + name string + certified bool + arity int + reason string + }{ + {name: "Fixed", certified: true, arity: 1}, + {name: "FixedSix", certified: true, arity: 6}, + {name: "privateCarrier", certified: true, arity: 1}, + {name: "privateMixedCarrier", certified: true, arity: 1}, + {name: "Arbitrary", certified: false, arity: 1, reason: "open-or-escaped-parameter-carrier"}, + {name: "ExportedCarrier", certified: false, arity: 1, reason: "open-or-escaped-parameter-carrier"}, + {name: "privateEscapedCarrier", certified: false, arity: 1, reason: "open-or-escaped-parameter-carrier"}, + {name: "Arithmetic", certified: false, arity: 1, reason: "arithmetic"}, + {name: "Incompatible", certified: false, arity: 1, reason: "abi-mismatch"}, + } { + call := exactWorkerSyscallCall(t, universe, pkg.ssa.Func(test.name)) + sink, ok := analysis.Sink(call) + if !ok { + t.Fatalf("%s has no callable-shadow sink result", test.name) + } + if sink.ABI.Family != coroCallableShadowWorkerSyscallFamily || sink.ABI.WordArgs != test.arity { + t.Fatalf("%s sink ABI = %+v; want family %q arity %d", test.name, sink.ABI, coroCallableShadowWorkerSyscallFamily, test.arity) + } + if sink.Certified != test.certified { + t.Fatalf("%s certified = %t, reason=%q, candidates=%+v, incoming=%+v; want %t", test.name, sink.Certified, sink.Reason, sink.Candidates, sink.Incoming, test.certified) + } + if test.reason != "" && !strings.Contains(sink.Reason, test.reason) { + t.Fatalf("%s reason = %q; want substring %q", test.name, sink.Reason, test.reason) + } + } +} + +func TestCoroCallableShadowBindsABIAtProducerAndKeepsConditionalEdges(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/callableshadowconditional", coroWorkerSyscallCapabilityFixture) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + t.Fatal(err) + } + analysis, err := AnalyzeCoroCallableShadows(universe) + if err != nil { + t.Fatal(err) + } + + okProducer := exactIntrinsicOpcodeCall(t, universe, pkg.ssa.Func("ThroughMixedCarrierOK"), llgoFuncPCABI0) + okShadow, ok := analysis.Producer(okProducer) + if !ok { + t.Fatal("safe FuncPCABI0 producer did not receive a compiler shadow") + } + if okShadow.Target == nil || okShadow.Target.Name() != "libc_fixed_worker_v1_trampoline" || + okShadow.ABI != (CoroCallableShadowABI{Family: coroCallableShadowWorkerSyscallFamily, WordArgs: 1}) { + t.Fatalf("safe producer shadow = %+v", okShadow) + } + + wrongProducer := exactIntrinsicOpcodeCall(t, universe, pkg.ssa.Func("ThroughMixedCarrierWrong"), llgoFuncPCABI0) + wrongShadow, ok := analysis.Producer(wrongProducer) + if !ok { + t.Fatal("incompatible FuncPCABI0 producer did not receive its independent compiler shadow") + } + if wrongShadow.ABI.WordArgs != 0 { + t.Fatalf("wrong producer ABI = %+v; want producer-declared arity 0", wrongShadow.ABI) + } + + call := exactWorkerSyscallCall(t, universe, pkg.ssa.Func("privateMixedCarrier")) + sink, ok := analysis.Sink(call) + if !ok || !sink.Certified { + t.Fatalf("conditional sink = %+v, %t; want conditionally certified", sink, ok) + } + if len(sink.Incoming) != 2 { + t.Fatalf("conditional incoming edge count = %d; want 2 (%+v)", len(sink.Incoming), sink.Incoming) + } + certified, rejected := 0, 0 + for _, edge := range sink.Incoming { + if edge.Certified { + certified++ + } else { + rejected++ + if !strings.Contains(edge.Reason, "abi-mismatch") { + t.Fatalf("rejected conditional edge reason = %q; want ABI mismatch", edge.Reason) + } + } + } + if certified != 1 || rejected != 1 { + t.Fatalf("conditional edge inventory certified=%d rejected=%d; want 1/1", certified, rejected) + } +} + +func TestCoroCallableShadowRejectsUnannotatedProducerWithoutAddressRecovery(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/callableshadowunknown", coroWorkerSyscallCapabilityFixture) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + t.Fatal(err) + } + analysis, err := AnalyzeCoroCallableShadows(universe) + if err != nil { + t.Fatal(err) + } + producer := exactIntrinsicOpcodeCall(t, universe, pkg.ssa.Func("Uncertified"), llgoFuncPCABI0) + if shadow, ok := analysis.Producer(producer); ok { + t.Fatalf("unannotated producer unexpectedly received shadow %+v", shadow) + } + if reason, ok := analysis.ProducerRejection(producer); !ok || reason != "target-lacks-workeraddr" { + t.Fatalf("unannotated producer rejection = %q, %t; want target-lacks-workeraddr", reason, ok) + } + sink, ok := analysis.Sink(exactWorkerSyscallCall(t, universe, pkg.ssa.Func("Uncertified"))) + if !ok || sink.Certified || sink.Reason != "target-lacks-workeraddr" { + t.Fatalf("unannotated sink = %+v, %t; want producer-side fail-closed result", sink, ok) + } +} + +func TestCoroCallableShadowRejectsDynamicTrapDispatcherWithoutOperationProof(t *testing.T) { + testProg := newEmissionTestProgram() + const packagePath = "example.com/emission/dynamictrap" + pkg := testProg.addPackage(t, packagePath, `package dynamictrap +//llgo:link funcPCABI0 llgo.funcPCABI0 +func funcPCABI0(fn any) uintptr +//llgo:link raw llgo.syscall +func raw(fn, trap, a0, a1, a2 uintptr) (uintptr, uintptr, uintptr) +func libc_arbitrary_trap_dispatcher_trampoline() +func RawSyscall(trap, a0, a1, a2 uintptr) uintptr { + r1, _, _ := raw(funcPCABI0(libc_arbitrary_trap_dispatcher_trampoline), trap, a0, a1, a2) + return r1 +} +`) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + prog.SetLinkname(packagePath+".libc_arbitrary_trap_dispatcher_trampoline", "C.__arbitrary_trap_dispatcher") + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + t.Fatal(err) + } + analysis, err := AnalyzeCoroCallableShadows(universe) + if err != nil { + t.Fatal(err) + } + producer := exactIntrinsicOpcodeCall(t, universe, pkg.ssa.Func("RawSyscall"), llgoFuncPCABI0) + if shadow, ok := analysis.Producer(producer); ok { + t.Fatalf("arbitrary trap dispatcher unexpectedly received worker shadow %+v", shadow) + } + if reason, ok := analysis.ProducerRejection(producer); !ok || reason != "target-lacks-workeraddr" { + t.Fatalf("arbitrary trap producer rejection = %q, %t; want target-lacks-workeraddr", reason, ok) + } + // StaticCodeAddress is only the occurrence proof that FuncPCABI0 consumes + // the exact target without materializing a managed interface. It is not a + // worker-call capability: the independent callable shadow and operation + // certificate checks above and below must still reject this dispatcher. + if observed, err := universe.CoroStaticCodeAddressCallArgument(producer, 0); err != nil || !observed { + t.Fatalf("arbitrary trap dispatcher static code-address occurrence = %t, %v; want true, nil", observed, err) + } + call := exactWorkerSyscallCall(t, universe, pkg.ssa.Func("RawSyscall")) + if sink, ok := analysis.Sink(call); !ok || sink.Certified || sink.Reason != "target-lacks-workeraddr" { + t.Fatalf("arbitrary trap worker sink = %+v, %t; want exact fail-closed rejection", sink, ok) + } + if certificate, certified, err := universe.CoroWorkerSyscallCertificate(call); err != nil || certified || certificate.ID != "" { + t.Fatalf("arbitrary trap worker certificate = %+v, %t, %v; want absent, false, nil", certificate, certified, err) + } +} + +func TestCoroCallableShadowGenericContractAuthorizesProductionWorkerCertificate(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/callableshadowcontract", coroCallableContractWorkerFixture) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + t.Fatal(err) + } + producer := exactIntrinsicOpcodeCall(t, universe, pkg.ssa.Func("Fixed"), llgoFuncPCABI0) + analysis, err := AnalyzeCoroCallableShadows(universe) + if err != nil { + t.Fatal(err) + } + shadow, ok := analysis.Producer(producer) + if !ok || shadow.ContractCertificateID == "" || shadow.LegacyWorkerAddressCompat || + shadow.ABI != (CoroCallableShadowABI{Family: coroCallableShadowWorkerSyscallFamily, WordArgs: 1}) { + reason, rejected := analysis.ProducerRejection(producer) + t.Fatalf("generic contract producer shadow = %+v, %t; rejection=%q,%t", shadow, ok, reason, rejected) + } + call := exactWorkerSyscallCall(t, universe, pkg.ssa.Func("Fixed")) + certificate, certified, err := universe.CoroWorkerSyscallCertificate(call) + if err != nil || !certified || certificate.ID == "" || certificate.CallableShadowSetID == "" || + certificate.StaticTargetCount != 1 { + t.Fatalf("generic contract worker certificate = %+v, %t, %v", certificate, certified, err) + } + + sink, ok := analysis.Sink(call) + if !ok || len(sink.Candidates) != 1 { + t.Fatalf("generic contract sink = %+v, %t", sink, ok) + } + sink.Candidates[0].PhysicalSymbol += "_forged" + opcode, intrinsic, opcodeErr := universe.coroIntrinsicOpcode(call.Common().StaticCallee()) + if opcodeErr != nil || !intrinsic { + t.Fatalf("worker opcode = %d, %t, %v", opcode, intrinsic, opcodeErr) + } + if _, _, _, err := freezeCoroWorkerSyscallShadowCertificate(universe, call, opcode, sink); err == nil || + !strings.Contains(err.Error(), "differs from its exact producer contract") { + t.Fatalf("forged forward shadow target was not rejected: %v", err) + } +} + +func TestCoroWorkerCallableGenericContractEligibilityIsExact(t *testing.T) { + for _, test := range []struct { + name string + properties string + want bool + wantArity int + }{ + {"valid", "progress=may-block affinity=any-thread reentry=none memory=by-value abi=word-call.v1/3", true, 3}, + {"progress", "progress=executor-safe affinity=any-thread reentry=none memory=by-value abi=word-call.v1/3", false, 0}, + {"affinity", "progress=may-block affinity=caller-thread reentry=none memory=by-value abi=word-call.v1/3", false, 0}, + {"reentry", "progress=may-block affinity=any-thread reentry=managed-callback memory=by-value abi=word-call.v1/3", false, 0}, + {"unknown memory", "progress=may-block affinity=any-thread reentry=none memory=unknown abi=word-call.v1/3", false, 0}, + {"retained memory", "progress=may-block affinity=any-thread reentry=none memory=retained abi=word-call.v1/3", false, 0}, + {"implicit ABI", "progress=may-block affinity=any-thread reentry=none memory=by-value", false, 0}, + {"other ABI", "progress=may-block affinity=any-thread reentry=none memory=by-value abi=typed.v1/3", false, 0}, + } { + t.Run(test.name, func(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/callableeligibility", `package callableeligibility +//llgo:coro contract foreign.v1 scope=declaration `+test.properties+` +func libc_eligibility_v1_trampoline() +`) + testProg.ssa.Build() + arity, ok, err := coroWorkerCallableDeclarationContractArity(pkg.ssa.Func("libc_eligibility_v1_trampoline")) + if err != nil || ok != test.want || arity != test.wantArity { + t.Fatalf("eligibility = %d, %t, %v; want %d, %t, nil", arity, ok, err, test.wantArity, test.want) + } + }) + } +} diff --git a/cl/coro_callable_transport_test.go b/cl/coro_callable_transport_test.go new file mode 100644 index 0000000000..05133b5630 --- /dev/null +++ b/cl/coro_callable_transport_test.go @@ -0,0 +1,236 @@ +//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" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const coroCallableTransportFixtureSource = `package foo + +//llgo:type C +type CFunc func(int) int + +type Mixed struct { + Raw CFunc + Managed func(int) int +} + +func BoxRaw(value CFunc) any { return value } +func AssertRaw(value any) (CFunc, bool) { + result, ok := value.(CFunc) + return result, ok +} + +func BoxMixed(value Mixed) any { return value } +func AssertMixed(value any) (Mixed, bool) { + result, ok := value.(Mixed) + return result, ok +} + +func BoxManaged(value func(int) int) any { return value } +func AssertManaged(value any) (func(int) int, bool) { + result, ok := value.(func(int) int) + return result, ok +} +` + +type coroCallableTransportFixture struct { + prog llssa.Program + pkg *ssa.Package + universe *EmissionUniverse + plan *coro.SSAPlan +} + +func prepareCoroCallableTransportFixture(t *testing.T) coroCallableTransportFixture { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroCallableTransportFixtureSource) + prog := newLLSSAProg(t) + ParsePkgSyntax(prog, ssaPkg.Pkg, files) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + roots := make(coro.Roots, 0, 6) + for _, name := range []string{"BoxRaw", "AssertRaw", "BoxMixed", "AssertMixed", "BoxManaged", "AssertManaged"} { + roots = append(roots, coro.Root{Function: ssaPkg.Func(name), Demand: coro.AsyncDemand}) + } + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyRawCFunctionType: func(typ types.Type) (bool, error) { + _, signature := types.Unalias(typ).Underlying().(*types.Signature) + return signature && prog.TypeBackground(typ) == llssa.InC, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return coroCallableTransportFixture{prog: prog, pkg: ssaPkg, universe: universe, plan: plan} +} + +func TestCoroCallableTransportPreservesRawCAndManagedInterfaceLeaves(t *testing.T) { + fixture := prepareCoroCallableTransportFixture(t) + defer fixture.prog.Dispose() + + for _, name := range []string{"BoxRaw", "BoxMixed", "BoxManaged"} { + fn := fixture.pkg.Func(name) + box := coroCallableMakeInterface(t, fn) + if err := validateCoroCallableTransportValue(fixture.plan, fn, box.X, fixture.universe); err != nil { + t.Fatalf("%s callable transport: %v", name, err) + } + } + for _, name := range []string{"AssertRaw", "AssertMixed", "AssertManaged"} { + fn := fixture.pkg.Func(name) + assertion := coroCallableTypeAssert(t, fn) + if err := validateCoroCallableTransportValue(fixture.plan, fn, assertion, fixture.universe); err != nil { + t.Fatalf("%s callable transport: %v", name, err) + } + } + + rawBox := coroCallableMakeInterface(t, fixture.pkg.Func("BoxRaw")) + rawPlan, found := fixture.plan.ValuePlan(rawBox.X) + if !found || len(rawPlan.Funcs) != 1 || rawPlan.Funcs[0].Transport != coro.RawCCodePointer || rawPlan.Funcs[0].Rep != coro.DirectPlain { + t.Fatalf("raw box operand plan = %+v, present=%t; want one raw direct pointer", rawPlan, found) + } + rawBoxAudit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.pkg.Func("BoxRaw"), "") + if err != nil { + t.Fatal(err) + } + if reason := rawBoxAudit.validateMakeInterface(rawBox); reason != "" { + t.Fatalf("raw C interface box physical validation: %s", reason) + } + mixedBox := coroCallableMakeInterface(t, fixture.pkg.Func("BoxMixed")) + mixedPlan, found := fixture.plan.ValuePlan(mixedBox.X) + if !found || len(mixedPlan.Funcs) != 2 || + mixedPlan.Funcs[0].Transport != coro.RawCCodePointer || mixedPlan.Funcs[0].Rep != coro.DirectPlain || + mixedPlan.Funcs[1].Transport != coro.ManagedTransport || mixedPlan.Funcs[1].Rep != coro.Dispatch { + t.Fatalf("mixed box operand plan = %+v, present=%t; want raw direct plus managed descriptor", mixedPlan, found) + } +} + +func TestCoroCallableTransportTypeAssertUsesPhysicalHelperContract(t *testing.T) { + fixture := prepareCoroCallableTransportFixture(t) + defer fixture.prog.Dispose() + + rawFn := fixture.pkg.Func("AssertRaw") + raw := coroCallableTypeAssert(t, rawFn) + rawAudit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, rawFn, "") + if err != nil { + t.Fatal(err) + } + if coroTypeAssertUsesManagedClosure(rawAudit.ctx, raw) { + t.Fatal("raw C type assertion was classified as a managed closure") + } + if reason := rawAudit.validateTypeAssert(raw); reason != "" { + t.Fatalf("raw C type assertion physical validation: %s", reason) + } + + mixedFn := fixture.pkg.Func("AssertMixed") + mixed := coroCallableTypeAssert(t, mixedFn) + mixedAudit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, mixedFn, "") + if err != nil { + t.Fatal(err) + } + if reason := mixedAudit.validateTypeAssert(mixed); reason != "" { + t.Fatalf("mixed aggregate type assertion physical validation: %s", reason) + } + + managedFn := fixture.pkg.Func("AssertManaged") + managed := coroCallableTypeAssert(t, managedFn) + managedAudit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, managedFn, "") + if err != nil { + t.Fatal(err) + } + if !coroTypeAssertUsesManagedClosure(managedAudit.ctx, managed) { + t.Fatal("managed type assertion did not retain the closure descriptor contract") + } + if reason := managedAudit.validateTypeAssert(managed); !strings.Contains(reason, "MatchesClosure") { + t.Fatalf("managed type assertion validation = %q; want MatchesClosure to remain in the frozen helper contract", reason) + } +} + +func TestCoroCallableTransportRejectsForgedDescriptorPlans(t *testing.T) { + for _, test := range []struct { + name string + leaf coro.FuncRepLeaf + want coro.FuncTransport + }{ + { + name: "raw C disguised as descriptor", + leaf: coro.FuncRepLeaf{Rep: coro.Dispatch, Transport: coro.RawCCodePointer}, + want: coro.RawCCodePointer, + }, + { + name: "managed closure disguised as direct pointer", + leaf: coro.FuncRepLeaf{Rep: coro.DirectPlain, Transport: coro.ManagedTransport}, + want: coro.ManagedTransport, + }, + } { + t.Run(test.name, func(t *testing.T) { + if err := validateCoroInterfaceCallableLeaf(test.leaf, test.want); err == nil { + t.Fatalf("forged leaf %+v unexpectedly passed", test.leaf) + } + }) + } +} + +func coroCallableMakeInterface(t *testing.T, fn *ssa.Function) *ssa.MakeInterface { + t.Helper() + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + if box, ok := instruction.(*ssa.MakeInterface); ok { + return box + } + } + } + t.Fatalf("function %s has no MakeInterface", fn) + return nil +} + +func coroCallableTypeAssert(t *testing.T, fn *ssa.Function) *ssa.TypeAssert { + t.Helper() + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + if assertion, ok := instruction.(*ssa.TypeAssert); ok { + return assertion + } + } + } + t.Fatalf("function %s has no TypeAssert", fn) + return nil +} diff --git a/cl/coro_channel.go b/cl/coro_channel.go new file mode 100644 index 0000000000..ef41a36f03 --- /dev/null +++ b/cl/coro_channel.go @@ -0,0 +1,271 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroChanSendParkHookV1 = "__llgo_coro_chan_send_park_v1" + coroChanRecvParkHookV1 = "__llgo_coro_chan_recv_park_v1" + coroChanResumeHookV1 = "__llgo_coro_chan_resume_v1" +) + +const ( + coroChanResumeSendOK uint64 = iota + 1 + coroChanResumeRecvOK + coroChanResumeRecvClosed + coroChanResumeSendClosed + coroChanResumeTaskAbort + coroChanResumeShutdown +) + +const ( + coroChanCloseOK uint64 = iota + coroChanCloseNil + coroChanCloseClosed +) + +func isCoroCloseBuiltinCall(call *ssa.Call) bool { + if call == nil || call.Common() == nil { + return false + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + return ok && builtin.Name() == "close" +} + +func coroChanParkSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + types.NewParam(token.NoPos, nil, "header", pointer), + types.NewParam(token.NoPos, nil, "channel", pointer), + types.NewParam(token.NoPos, nil, "elem", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + types.NewParam(token.NoPos, nil, "size", types.Typ[types.Uintptr]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroChanResumeSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + ) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "status", types.Typ[types.Uint32])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func (p *context) requireCoroChannelBody(b llssa.Builder) *coroBodyContext { + body := p.coroBody() + if body == nil || b.Func != p.fn { + panic("coroutine channel lowering requires an active planned physical coroutine body") + } + if body.abi.version < coroPhysicalABIVersionV1 || body.completion == nil || + body.finalSuspend == nil || body.unsupportedRunDecision == nil { + panic("coroutine channel lowering requires the complete PhysicalABIV1 scheduler ABI") + } + return body +} + +func (p *context) newCoroChannelStorage(b llssa.Builder, elemType llssa.Type) (elem, state llssa.Expr) { + // These addresses may be published to hchan immediately before suspend. + // Allocate them in the physical ramp entry so no waiter can retain a + // resume-local M stack address. Reset at the logical operation point because + // the same static channel instruction may execute repeatedly in a loop. + elem = p.coroFrameAlloca(elemType) + stateType := p.prog.RuntimeType("CoroChanParkV1") + state = p.coroFrameAlloca(stateType) + b.Store(elem, b.Prog.Zero(elemType)) + b.Store(state, b.Prog.Zero(stateType)) + return +} + +func (p *context) compileCoroChanSend(b llssa.Builder, channel, value llssa.Expr) { + body := p.requireCoroChannelBody(b) + elem, state := p.newCoroChannelStorage(b, value.Type) + b.Store(elem, value) + ready := b.CoroChanTrySend(channel, elem) + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.UnOp(token.NOT, ready), + park: func(suspend llssa.Builder) { + park := p.pkg.NewFunc(coroChanSendParkHookV1, coroChanParkSignature(), llssa.InC) + suspend.Call( + park.Expr, + body.task, + body.coro.Handle(), + suspend.Convert(suspend.Prog.VoidPtr(), body.header), + suspend.Convert(suspend.Prog.VoidPtr(), channel), + suspend.Convert(suspend.Prog.VoidPtr(), elem), + suspend.Convert(suspend.Prog.VoidPtr(), state), + p.prog.IntVal(p.prog.SizeOf(value.Type), p.prog.Uintptr()), + ) + }, + resume: func(resume llssa.Builder) llssa.Expr { + statusHook := p.pkg.NewFunc(coroChanResumeHookV1, coroChanResumeSignature(), llssa.InC) + return resume.Call(statusHook.Expr, body.task, resume.Convert(resume.Prog.VoidPtr(), state)) + }, + normal: []uint64{coroChanResumeSendOK}, + faults: []coroParkFaultRoute{ + {status: coroChanResumeSendClosed, kind: coroFaultChannelSendClosedV1}, + }, + abort: coroChanResumeTaskAbort, + shutdown: coroChanResumeShutdown, + }) +} + +func (p *context) compileCoroChanRecv(b llssa.Builder, instruction *ssa.UnOp, channel llssa.Expr) llssa.Expr { + if instruction == nil || instruction.Op != token.ARROW { + panic(fmt.Errorf("coroutine channel receive requires one SSA receive instruction")) + } + body := p.requireCoroChannelBody(b) + elemType := p.prog.Elem(channel.Type) + elem, state := p.newCoroChannelStorage(b, elemType) + result := b.CoroChanTryRecv(channel, elem) + recvOK := b.Extract(result, 0) + tryOK := b.Extract(result, 1) + recvOKSlot := p.coroFrameAlloca(p.prog.Bool()) + b.Store(recvOKSlot, recvOK) + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.UnOp(token.NOT, tryOK), + park: func(suspend llssa.Builder) { + park := p.pkg.NewFunc(coroChanRecvParkHookV1, coroChanParkSignature(), llssa.InC) + suspend.Call( + park.Expr, + body.task, + body.coro.Handle(), + suspend.Convert(suspend.Prog.VoidPtr(), body.header), + suspend.Convert(suspend.Prog.VoidPtr(), channel), + suspend.Convert(suspend.Prog.VoidPtr(), elem), + suspend.Convert(suspend.Prog.VoidPtr(), state), + p.prog.IntVal(p.prog.SizeOf(elemType), p.prog.Uintptr()), + ) + }, + resume: func(resume llssa.Builder) llssa.Expr { + statusHook := p.pkg.NewFunc(coroChanResumeHookV1, coroChanResumeSignature(), llssa.InC) + status := resume.Call(statusHook.Expr, body.task, resume.Convert(resume.Prog.VoidPtr(), state)) + resume.Store( + recvOKSlot, + resume.BinOp( + token.EQL, + status, + resume.Prog.IntVal(coroChanResumeRecvOK, resume.Prog.Uint32()), + ), + ) + return status + }, + normal: []uint64{ + coroChanResumeRecvOK, + coroChanResumeRecvClosed, + }, + abort: coroChanResumeTaskAbort, + shutdown: coroChanResumeShutdown, + }) + // elem is compiler-owned coroutine-frame storage allocated above. Its + // address is valid even when the channel element has size zero, so loading + // it must not synthesize a user nil-dereference helper that was never part + // of the frozen call graph. + value := b.LoadKnownNonNil(elem) + if !instruction.CommaOk { + return value + } + return b.Aggregate(p.type_(instruction.Type(), llssa.InGo), value, b.Load(recvOKSlot)) +} + +func (p *context) compileCoroChanClose(b llssa.Builder, channel llssa.Expr) { + body := p.requireCoroChannelBody(b) + status := b.CoroChanTryClose(channel) + nilChannel := b.Func.MakeBlock() + alreadyClosed := b.Func.MakeBlock() + normal := b.Func.MakeBlock() + dispatch := b.Switch(status, body.unsupportedRunDecision) + dispatch.Case(b.Prog.IntVal(coroChanCloseOK, b.Prog.Uint32()), normal) + dispatch.Case(b.Prog.IntVal(coroChanCloseNil, b.Prog.Uint32()), nilChannel) + dispatch.Case(b.Prog.IntVal(coroChanCloseClosed, b.Prog.Uint32()), alreadyClosed) + dispatch.End(b) + + b.SetBlockEx(nilChannel, llssa.AtEnd, false) + p.compileCoroTerminalFault(b, coroFaultChannelCloseNilV1) + b.SetBlockEx(alreadyClosed, llssa.AtEnd, false) + p.compileCoroTerminalFault(b, coroFaultChannelCloseClosedV1) + b.SetBlockContinuation(normal) + body.activate(b) +} + +func (p *context) compileCoroChanSelect(b llssa.Builder, states []*llssa.SelectState) llssa.Expr { + body := p.requireCoroChannelBody(b) + frame := p.fn.NewBuilder() + defer frame.Dispose() + frame.SetBlockEx(p.fn.Block(0), llssa.AtStart, true) + plan := b.NewCoroSelectInFrame(frame, states) + attempt := b.CoroChanSelectTry(plan) + chosenSlot := p.coroFrameAlloca(b.Prog.Int()) + recvOKSlot := p.coroFrameAlloca(b.Prog.Bool()) + b.Store(chosenSlot, b.Extract(attempt, 0)) + b.Store(recvOKSlot, b.Extract(attempt, 1)) + tryOK := b.Extract(attempt, 2) + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.UnOp(token.NOT, tryOK), + park: func(suspend llssa.Builder) { + suspend.CoroChanSelectPark( + plan, + body.task, + body.coro.Handle(), + suspend.Convert(suspend.Prog.VoidPtr(), body.header), + ) + }, + resume: func(resume llssa.Builder) llssa.Expr { + result := resume.CoroChanSelectResume(plan, body.task) + resume.Store(chosenSlot, resume.Extract(result, 0)) + resume.Store(recvOKSlot, resume.Extract(result, 1)) + return resume.Extract(result, 2) + }, + normal: []uint64{ + coroChanResumeSendOK, + coroChanResumeRecvOK, + coroChanResumeRecvClosed, + }, + faults: []coroParkFaultRoute{ + {status: coroChanResumeSendClosed, kind: coroFaultChannelSendClosedV1}, + }, + abort: coroChanResumeTaskAbort, + shutdown: coroChanResumeShutdown, + }) + return b.CoroChanSelectResult(plan, b.Load(chosenSlot), b.Load(recvOKSlot)) +} + +func (p *context) compileCoroChanTrySelect(b llssa.Builder, states []*llssa.SelectState) llssa.Expr { + body := p.requireCoroChannelBody(b) + plan := b.NewCoroSelect(states) + attempt := b.CoroChanSelectTry(plan) + closed := b.Func.MakeBlock() + normal := b.Func.MakeBlock() + b.If(b.Extract(attempt, 3), closed, normal) + b.SetBlockEx(closed, llssa.AtEnd, false) + p.compileCoroTerminalFault(b, coroFaultChannelSendClosedV1) + b.SetBlockContinuation(normal) + body.activate(b) + return b.CoroChanSelectResult(plan, b.Extract(attempt, 0), b.Extract(attempt, 1)) +} diff --git a/cl/coro_channel_test.go b/cl/coro_channel_test.go new file mode 100644 index 0000000000..41ecedbde2 --- /dev/null +++ b/cl/coro_channel_test.go @@ -0,0 +1,473 @@ +//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 ( + "bytes" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroChannelTestSource = `package foo + +var Sink uint32 + +func Cleanup() { Sink++ } + +func Send(ch chan uint32, value uint32) { + ch <- value +} + +func Recv(ch chan uint32) uint32 { + return <-ch +} + +func RecvOK(ch chan uint32) (uint32, bool) { + value, ok := <-ch + return value, ok +} + +func Select(first, second chan uint32, value uint32) (int, uint32, bool) { + select { + case first <- value: + return 0, 0, true + case received, ok := <-second: + return 1, received, ok + } +} + +func TrySelectThenRecv(first, second chan uint32, value uint32) (int, uint32, bool) { + selected := -1 + var received uint32 + var ok bool + select { + case first <- value: + selected = 0 + case received, ok = <-second: + selected = 1 + default: + } + received += <-second + return selected, received, ok +} + +func EmptySelect() { + select {} +} + +func SendWithCleanup(ch chan uint32, value uint32) { + defer Cleanup() + ch <- value +} + +func SelectWithCleanup(first, second chan uint32, value uint32) { + defer Cleanup() + select { + case first <- value: + case <-second: + } +} +` + +func TestCoroChannelNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroChannelFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + for _, fn := range functions { + functionPlan, ok := plan.FunctionPlan(fn) + if !ok || functionPlan.Emission != coro.EmitCoroutine || functionPlan.FuncRep != coro.DirectCoro || + functionPlan.Demand != coro.AsyncDemand || !functionPlan.Effect.Contains(coro.MayPark) { + t.Fatalf("%s plan = %+v, present=%t; want async direct may-park coroutine", fn.Name(), functionPlan, ok) + } + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify channel coroutine before CoroSplit: %v\n%s", err, module.String()) + } + + sendPhysical := requireCoroPhysicalFunction(t, module, "foo.Send") + send := sendPhysical.String() + assertCoroCancellationTerminalStatusPublication(t, sendPhysical) + assertCoroChannelBody(t, "Send", send, coroChanSendParkHookV1, []uint64{ + coroChanResumeSendOK, + coroChanResumeSendClosed, + coroChanResumeTaskAbort, + coroChanResumeShutdown, + }) + for _, symbol := range []string{"github.com/goplus/llgo/runtime/internal/runtime.CoroChanTrySend", coroFaultPrepareHookV1} { + if !strings.Contains(send, symbol) { + t.Fatalf("Send coroutine lacks %q:\n%s", symbol, send) + } + } + if hook := strings.Index(send, "call void @"+coroFaultPrepareHookV1); hook < 0 || + !strings.Contains(send[hook:], "i32 3") { + t.Fatalf("Send coroutine did not select the send-closed fault kind:\n%s", send) + } + for _, name := range []string{"Recv", "RecvOK"} { + recv := requireCoroPhysicalFunction(t, module, "foo."+name).String() + assertCoroChannelBody(t, name, recv, coroChanRecvParkHookV1, []uint64{ + coroChanResumeRecvOK, + coroChanResumeRecvClosed, + coroChanResumeTaskAbort, + coroChanResumeShutdown, + }) + if !strings.Contains(recv, "@\"github.com/goplus/llgo/runtime/internal/runtime.CoroChanTryRecv\"") { + t.Fatalf("%s coroutine lacks nonblocking receive helper:\n%s", name, recv) + } + } + selectBody := requireCoroPhysicalFunction(t, module, "foo.Select").String() + assertCoroSelectBody(t, selectBody) + emptySelectBody := requireCoroPhysicalFunction(t, module, "foo.EmptySelect").String() + assertCoroSelectBody(t, emptySelectBody) + trySelectBody := requireCoroPhysicalFunction(t, module, "foo.TrySelectThenRecv").String() + assertCoroTrySelectBody(t, trySelectBody) + for _, name := range []string{"SendWithCleanup", "SelectWithCleanup"} { + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if !strings.Contains(body, "foo.Cleanup") || !strings.Contains(body, "switch i32") { + t.Fatalf("%s did not route terminal channel outcomes through the static cleanup drainer:\n%s", name, body) + } + } + for _, name := range []string{"SendWithCleanup", "SelectWithCleanup"} { + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if strings.Count(body, "call void @"+coroFaultPayloadHookV1+"(i32 3") != 1 || + !strings.Contains(body, "call void @"+coroPanicPrepareHookV1) || + strings.Contains(body, "call void @"+coroFaultPrepareHookV1) { + t.Fatalf("%s did not materialize send-closed into the recoverable cleanup overlay:\n%s", name, body) + } + } + for _, forbidden := range []string{"runtime.ChanSend\"", "runtime.ChanRecv\"", "runtime.Select\"", "Future", "Promise", "Task"} { + if strings.Contains(module.String(), forbidden) { + t.Fatalf("channel lowering retained forbidden abstraction %q:\n%s", forbidden, module.String()) + } + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{"foo.Send$coro", "foo.Recv$coro", "foo.RecvOK$coro"} { + resume := module.NamedFunction(name + ".resume") + if resume.IsNil() || !strings.Contains(resume.String(), "call i32 @"+coroChanResumeHookV1) { + t.Fatalf("CoroSplit lost channel resume dispatch in %s:\n%s", name, module.String()) + } + if name == "foo.Send$coro" { + assertCoroCancellationTerminalStatusPublication(t, resume) + } + } + selectResume := module.NamedFunction("foo.Select$coro.resume") + if selectResume.IsNil() || !strings.Contains( + selectResume.String(), + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectResume", + ) { + t.Fatalf("CoroSplit lost channel select resume dispatch:\n%s", module.String()) + } + // Both the send and receive payload addresses must be direct frame + // fields at the resumed keepalive point. Before this regression was + // fixed, the send slot and select descriptor array were physical M-stack + // allocas whose addresses escaped through the hchan queue. + if !regexp.MustCompile( + `@llvm\.fake\.use\(ptr %[^,\n]*reload\.addr[^,\n]*, ptr %[^)\n]*reload\.addr`, + ).MatchString(selectResume.String()) { + t.Fatalf("CoroSplit did not retain every select payload in its frame:\n%s", selectResume.String()) + } + emptySelectResume := module.NamedFunction("foo.EmptySelect$coro.resume") + if emptySelectResume.IsNil() || !strings.Contains( + emptySelectResume.String(), + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectResume", + ) { + t.Fatalf("CoroSplit lost empty channel select cancellation dispatch:\n%s", module.String()) + } + trySelectResume := module.NamedFunction("foo.TrySelectThenRecv$coro.resume") + if trySelectResume.IsNil() || strings.Count( + trySelectResume.String(), + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectTry", + ) != 1 || strings.Contains( + trySelectResume.String(), + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectPark", + ) { + t.Fatalf("CoroSplit changed nonblocking channel select into a physical park:\n%s", module.String()) + } + for _, intrinsic := range []string{"llvm.coro.id", "llvm.coro.begin", "llvm.coro.suspend", "llvm.coro.end"} { + if hasLLVMCall(module.String(), intrinsic) { + t.Fatalf("post-split channel module still calls %s:\n%s", intrinsic, module.String()) + } + } + if removed := llssa.RemoveKeepAliveCallsAfterCoroSplit(module); removed == 0 || + strings.Contains(selectResume.String(), "@llvm.fake.use") { + t.Fatalf("post-split channel payload keepalive cleanup = %d:\n%s", removed, selectResume.String()) + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit channel object: %v\n%s", err, module.String()) + } + defer object.Dispose() + for _, symbol := range []string{ + coroChanSendParkHookV1, + coroChanRecvParkHookV1, + coroChanResumeHookV1, + coroFaultPrepareHookV1, + coroFaultPayloadHookV1, + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectTry", + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectPark", + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectResume", + } { + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(symbol)) { + t.Fatalf("post-CoroSplit channel object lost ABI symbol %q", symbol) + } + } + }) + } +} + +func assertCoroSelectBody(t *testing.T, body string) { + t.Helper() + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("Select coro.suspend calls = %d, want initial + select + final:\n%s", got, body) + } + for _, symbol := range []string{ + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectTry", + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectPark", + "github.com/goplus/llgo/runtime/internal/runtime.CoroChanSelectResume", + } { + if got := strings.Count(body, symbol); got != 1 { + t.Fatalf("Select references to %q = %d, want 1:\n%s", symbol, got, body) + } + } + for _, status := range []uint64{ + coroChanResumeSendOK, + coroChanResumeRecvOK, + coroChanResumeRecvClosed, + coroChanResumeSendClosed, + coroChanResumeTaskAbort, + coroChanResumeShutdown, + } { + if !regexp.MustCompile(`(?m)^\s+i32 ` + strconv.FormatUint(status, 10) + `, label `).MatchString(body) { + t.Fatalf("Select resume dispatch lacks status %d:\n%s", status, body) + } + } + park := strings.Index(body, "runtime.CoroChanSelectPark") + if park < 0 { + t.Fatalf("Select does not publish its physical cases:\n%s", body) + } + suspend := strings.Index(body[park:], "call i8 @llvm.coro.suspend") + resume := strings.Index(body[park:], "runtime.CoroChanSelectResume") + if suspend < 0 || resume < 0 || suspend >= resume { + t.Fatalf("Select does not publish all cases before suspend and clean them after resume:\n%s", body) + } +} + +func assertCoroTrySelectBody(t *testing.T, body string) { + t.Helper() + if got := strings.Count(body, "runtime.CoroChanSelectTry"); got != 1 { + t.Fatalf("TrySelectThenRecv select-try calls = %d, want 1:\n%s", got, body) + } + for _, forbidden := range []string{"runtime.CoroChanSelectPark", "runtime.CoroChanSelectResume"} { + if strings.Contains(body, forbidden) { + t.Fatalf("TrySelectThenRecv nonblocking select uses %q:\n%s", forbidden, body) + } + } + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("TrySelectThenRecv coro.suspend calls = %d, want initial + trailing receive + final:\n%s", got, body) + } +} + +func assertCoroChannelBody(t *testing.T, name, body, parkHook string, statuses []uint64) { + t.Helper() + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("%s coro.suspend calls = %d, want initial + channel + final:\n%s", name, got, body) + } + for _, symbol := range []string{parkHook, coroChanResumeHookV1} { + if got := strings.Count(body, "@"+symbol); got != 1 { + t.Fatalf("%s references to %q = %d, want 1:\n%s", name, symbol, got, body) + } + } + if !strings.Contains(body, "switch i32") { + t.Fatalf("%s has no exact typed resume-status dispatch:\n%s", name, body) + } + statusCall := regexp.MustCompile( + `(?m)^\s+(%[-A-Za-z0-9._]+) = call i32 @` + regexp.QuoteMeta(coroChanResumeHookV1) + `\([^\n]+\)\s*$`, + ).FindStringSubmatchIndex(body) + if len(statusCall) != 4 { + t.Fatalf("%s has no unique channel resume status value:\n%s", name, body) + } + status := body[statusCall[2]:statusCall[3]] + afterCall := body[statusCall[1]:] + dispatchPattern := regexp.MustCompile( + `(?s)switch i32 ` + regexp.QuoteMeta(status) + `,?\s+[^\[]+\[(.*?)\]`, + ) + dispatch := dispatchPattern.FindStringSubmatch(afterCall) + if len(dispatch) != 2 { + t.Fatalf("%s has no channel resume switch for status %s:\n%s", name, status, body) + } + between := afterCall[:strings.Index(afterCall, dispatch[0])] + if regexp.MustCompile(`(?m)^\s*(br|switch|ret|unreachable)\b`).MatchString(between) { + t.Fatalf("%s branches before dispatching channel resume status %s:\n%s", name, status, between) + } + for _, status := range statuses { + if !regexp.MustCompile(`(?m)^\s+i32 ` + strconv.FormatUint(status, 10) + `, label `).MatchString(dispatch[1]) { + t.Fatalf("%s channel resume switch lacks status %d:\n%s", name, status, dispatch[0]) + } + } + hook := strings.Index(body, "call void @"+parkHook) + if hook < 0 { + t.Fatalf("%s does not publish its physical park:\n%s", name, body) + } + suspend := strings.Index(body[hook:], "call i8 @llvm.coro.suspend") + resume := strings.Index(body[hook:], "call i32 @"+coroChanResumeHookV1) + if suspend < 0 || resume < 0 || suspend >= resume { + t.Fatalf("%s does not publish park before suspend and dispatch after resume:\n%s", name, body) + } +} + +func compileCoroChannelFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, []*ssa.Function, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroChannelTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, + nil, + []EmissionPackage{{SSA: ssaPkg, Files: files}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := []*ssa.Function{ + ssaPkg.Func("Send"), ssaPkg.Func("Recv"), ssaPkg.Func("RecvOK"), + ssaPkg.Func("Select"), ssaPkg.Func("TrySelectThenRecv"), ssaPkg.Func("EmptySelect"), + ssaPkg.Func("SendWithCleanup"), ssaPkg.Func("SelectWithCleanup"), + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + roots := make(coro.Roots, 0, len(functions)) + for _, fn := range functions { + roots = append(roots, coro.Root{Function: fn, Demand: coro.AsyncDemand}) + } + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + } + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, functions +} + +func TestCoroChannelCompilationCapabilityFailsClosed(t *testing.T) { + compilation := &Compilation{ + CoroABI: coro.PhysicalABIV1, + SchedulerABI: "invalid-scheduler", + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + } + if err := compilation.validateCoroABIIdentity(false); err == nil || !strings.Contains(err.Error(), "scheduler ABI") { + t.Fatalf("channel scheduler identity error = %v", err) + } +} + +func TestCoroChannelPhysicalABIRejectsNilSelectChannel(t *testing.T) { + llssa.Initialize(llssa.InitAll) + prog, pkg, plan, functions := compileCoroChannelFixture(t, nil) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + var selectFn *ssa.Function + for _, fn := range functions { + if fn.Name() == "Select" { + selectFn = fn + break + } + } + if selectFn == nil { + t.Fatal("Select function not found") + } + var instruction *ssa.Select + for _, block := range selectFn.Blocks { + for _, candidate := range block.Instrs { + if candidate, ok := candidate.(*ssa.Select); ok { + instruction = candidate + break + } + } + } + if instruction == nil || len(instruction.States) == 0 || instruction.States[0] == nil { + t.Fatal("Select instruction has no concrete channel case") + } + instruction.States[0].Chan = nil + functionPlan, ok := plan.FunctionPlan(selectFn) + if !ok { + t.Fatal("Select function plan not found") + } + err := validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + selectFn, functionPlan, plan, nil, true, true, false, false, "", true, false, false, + ) + if err == nil || !strings.Contains(err.Error(), "channel select case 0 channel is nil") { + t.Fatalf("nil select channel validation error = %v", err) + } +} diff --git a/cl/coro_child_keepalive_test.go b/cl/coro_child_keepalive_test.go new file mode 100644 index 0000000000..1bbb4cff0a --- /dev/null +++ b/cl/coro_child_keepalive_test.go @@ -0,0 +1,143 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroChildAwaitKeepsPointerDerivedUintptrOwnerThroughCompletion(t *testing.T) { + const source = `package foo +import "unsafe" +var sink uintptr +func Child(word uintptr) { sink = word } +func Parent(pointer *byte) { + if pointer != nil { + Child(uintptr(unsafe.Pointer(pointer))) + } +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + parent, child := ssaPkg.Func("Parent"), ssaPkg.Func("Child") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: parent, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == child { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + + var childCall *ssa.Call + for _, block := range parent.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Common().StaticCallee() == child { + childCall = call + } + } + } + if childCall == nil { + t.Fatal("fixture has no Parent -> Child SSA call") + } + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, parent, CoroFrameRetentionParkABIV2) + if err != nil { + t.Fatal(err) + } + roots := audit.currentFrameRetentionProof().exactCallKeepaliveRoots(childCall) + if len(roots) != 1 || roots[0] != parent.Params[0] { + t.Fatalf("child await keepalive roots = %v, want exact pointer parameter", rootNames(roots)) + } + + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + CoroFrameRetentionABI: CoroFrameRetentionParkABIV2, + } + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify child keepalive before CoroSplit: %v\n%s", err, module.String()) + } + + parentIR := requireCoroPhysicalFunction(t, module, "foo.Parent").String() + consume := "call i32 @" + coroAwaitConsumeHookV1 + fakeUse := "call void (...) @llvm.fake.use(ptr " + consumeAt, fakeUseAt := allTextIndexes(parentIR, consume), allTextIndexes(parentIR, fakeUse) + if len(consumeAt) != 2 || len(fakeUseAt) != 2 { + t.Fatalf("child completion consume/fake-use sites = %d/%d, want 2/2:\n%s", len(consumeAt), len(fakeUseAt), parentIR) + } + for index := range consumeAt { + if fakeUseAt[index] <= consumeAt[index] || index+1 < len(consumeAt) && fakeUseAt[index] >= consumeAt[index+1] { + t.Fatalf("fake-use %d does not follow its exact completion consume:\n%s", index, parentIR) + } + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Parent$coro.resume") + if resume.IsNil() || strings.Count(resume.String(), fakeUse) != 2 { + t.Fatalf("CoroSplit did not retain both completion-bound pointer owners:\n%s", module.String()) + } +} + +func allTextIndexes(text, marker string) []int { + var indexes []int + for offset := 0; ; { + index := strings.Index(text[offset:], marker) + if index < 0 { + return indexes + } + index += offset + indexes = append(indexes, index) + offset = index + len(marker) + } +} diff --git a/cl/coro_clear_builtin_test.go b/cl/coro_clear_builtin_test.go new file mode 100644 index 0000000000..59acbe70a2 --- /dev/null +++ b/cl/coro_clear_builtin_test.go @@ -0,0 +1,66 @@ +//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 ( + "strings" + "testing" + + "golang.org/x/tools/go/ssa" +) + +func TestCoroClearBuiltinRequiresExactManagedHelper(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func ClearSlice(values []uintptr) { clear(values) } +func ClearMap(values map[uintptr]uintptr) { clear(values) } +`) + for _, test := range []struct { + name string + helper string + }{ + {name: "ClearSlice", helper: "SliceClear"}, + {name: "ClearMap", helper: "MapClear"}, + } { + function := ssaPkg.Func(test.name) + var call *ssa.Call + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + candidate, ok := instruction.(*ssa.Call) + if !ok || candidate.Common() == nil { + continue + } + builtin, ok := candidate.Common().Value.(*ssa.Builtin) + if ok && builtin.Name() == "clear" { + call = candidate + } + } + } + if call == nil { + t.Fatalf("%s has no clear builtin", test.name) + } + audit, err := newCoroPhysicalPureSSAAudit(nil, nil, function, "") + if err != nil { + t.Fatal(err) + } + handled, reason := audit.validate(call) + if !handled || !strings.Contains(reason, "runtime helper capability validation requires a frozen emission universe") { + t.Fatalf("%s audit = handled %t, reason %q; want exact %s helper gate", test.name, handled, reason, test.helper) + } + } +} diff --git a/cl/coro_complex_builtin_test.go b/cl/coro_complex_builtin_test.go new file mode 100644 index 0000000000..9731309292 --- /dev/null +++ b/cl/coro_complex_builtin_test.go @@ -0,0 +1,103 @@ +//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 ( + "strings" + "testing" + + "golang.org/x/tools/go/ssa" +) + +const coroComplexBuiltinFixture = `package foo + +type C64 complex64 +type C128 complex128 + +func Real64(value complex64) float32 { return real(value) } +func Imag64(value C64) float32 { return imag(value) } +func Real128(value C128) float64 { return real(value) } +func Imag128(value complex128) float64 { return imag(value) } +` + +func TestCoroComplexComponentBuiltins(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroComplexBuiltinFixture) + for _, test := range []struct { + function string + builtin string + }{ + {function: "Real64", builtin: "real"}, + {function: "Imag64", builtin: "imag"}, + {function: "Real128", builtin: "real"}, + {function: "Imag128", builtin: "imag"}, + } { + t.Run(test.function, func(t *testing.T) { + fn := ssaPkg.Func(test.function) + call := coroComplexBuiltinCall(t, fn, test.builtin) + audit := &coroPhysicalPureSSAAudit{ + fn: fn, + reachableBlocks: coroPhysicalConstantReachableBlocks(fn), + } + if reason := audit.validateBuiltin(call); reason != "" { + t.Fatalf("%s rejected: %s", test.builtin, reason) + } + }) + } +} + +func TestCoroComplexComponentBuiltinFailsClosed(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroComplexBuiltinFixture) + fn := ssaPkg.Func("Real64") + call := coroComplexBuiltinCall(t, fn, "real") + audit := &coroPhysicalPureSSAAudit{ + fn: fn, + reachableBlocks: coroPhysicalConstantReachableBlocks(fn), + } + args := call.Call.Args + call.Call.Args = nil + defer func() { call.Call.Args = args }() + if reason := audit.validateBuiltin(call); !strings.Contains(reason, "requires one complex argument") { + t.Fatalf("malformed real rejection = %q", reason) + } +} + +func coroComplexBuiltinCall(t *testing.T, fn *ssa.Function, name string) *ssa.Call { + t.Helper() + var found *ssa.Call + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + builtin, ok := call.Call.Value.(*ssa.Builtin) + if !ok || builtin.Name() != name { + continue + } + if found != nil { + t.Fatalf("%s has more than one %s builtin call", fn, name) + } + found = call + } + } + if found == nil { + t.Fatalf("%s has no %s builtin call", fn, name) + } + return found +} diff --git a/cl/coro_copy_managed_test.go b/cl/coro_copy_managed_test.go new file mode 100644 index 0000000000..aaa9721d44 --- /dev/null +++ b/cl/coro_copy_managed_test.go @@ -0,0 +1,309 @@ +//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 ( + "bytes" + "go/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroCopyRuntimeFixture = `package runtime +import "unsafe" + +type Slice struct { + Data unsafe.Pointer + Len int + Cap int +} + +type String struct { + Data unsafe.Pointer + Len int +} + +func SliceCopy(destination Slice, data unsafe.Pointer, count, elementSize int) int { + if count > destination.Len { return destination.Len } + return count +} +` + +const coroCopyFixture = `package foo + +func CopySlice(destination, source []byte, wrong []rune) int { + return copy(destination, source) +} + +func CopyString(destination []byte, source string) int { + return copy(destination, source) +} +` + +type coroCopyTestPlan struct { + prog llssa.Program + runtimePkg emissionTestPackage + fooPkg emissionTestPackage + universe *EmissionUniverse + plan *coro.SSAPlan + functions map[string]*ssa.Function + calls map[string]*ssa.Call +} + +func TestCoroCopyHelperNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, target := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(target.name, func(t *testing.T) { + fixture := prepareCoroCopyTestPlan(t, target.target, true) + defer fixture.prog.Dispose() + + for name, call := range fixture.calls { + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.functions[name], "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + if reason := audit.validateCopyBuiltin(call); reason != "" { + t.Fatalf("%s copy rejected: %s", name, reason) + } + } + + helper := fixture.runtimePkg.ssa.Func("SliceCopy") + helperPlan, ok := fixture.plan.FunctionPlan(helper) + if !ok || helperPlan.External != coro.Defined || helperPlan.Emission != coro.EmitPlain || + helperPlan.Primary != coro.PrimaryPlain || helperPlan.FuncRep != coro.DirectPlain || + helperPlan.Effect != coro.NoSuspend || helperPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("SliceCopy plan = %+v, present=%t; want exact no-unwind direct plain helper", helperPlan, ok) + } + + compilation := &Compilation{CoroPlan: fixture.plan, EmissionUniverse: fixture.universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + runtimeLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.runtimePkg.ssa, []*ast.File{fixture.runtimePkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile SliceCopy helper: %v", err) + } + runtimeModule := runtimeLL.Module() + defer runtimeModule.Dispose() + fooLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.fooPkg.ssa, []*ast.File{fixture.fooPkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile copy owners: %v", err) + } + fooModule := fooLL.Module() + defer fooModule.Dispose() + for name, module := range map[string]llvm.Module{"runtime": runtimeModule, "foo": fooModule} { + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify %s before CoroSplit: %v\n%s", name, err, module.String()) + } + } + for name := range fixture.functions { + body := requireCoroPhysicalFunction(t, fooModule, "foo."+name).String() + if !strings.Contains(body, "runtime.SliceCopy") || strings.Contains(body, "runtime.SliceCopy$coro") { + t.Fatalf("%s did not call the exact plain SliceCopy helper:\n%s", name, body) + } + if strings.Contains(body, coroAwaitPrepareHookV1) || strings.Contains(body, coroAwaitConsumeHookV1) { + t.Fatalf("%s awaited a proven no-suspend SliceCopy helper:\n%s", name, body) + } + } + + for _, module := range []llvm.Module{runtimeModule, fooModule} { + runCoroABITestPipeline(t, fixture.prog, module) + object, err := fixture.prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit copy object: %v\n%s", err, module.String()) + } + if len(object.Bytes()) == 0 { + object.Dispose() + t.Fatal("post-CoroSplit copy object is empty") + } + object.Dispose() + } + if !bytes.Contains([]byte(fooModule.String()), []byte("foo.CopySlice$coro.resume")) { + t.Fatalf("CoroSplit lost the copy owner resume entry:\n%s", fooModule.String()) + } + }) + } +} + +func TestCoroCopyHelperFailClosed(t *testing.T) { + t.Run("lowered fact required", func(t *testing.T) { + fixture := prepareCoroCopyTestPlan(t, nil, false) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.functions["CopySlice"], "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + if reason := audit.validateCopyBuiltin(fixture.calls["CopySlice"]); !strings.Contains(reason, "exact coroutine-safe lowered-call plan") { + t.Fatalf("missing-fact rejection = %q", reason) + } + }) + + t.Run("malformed shape", func(t *testing.T) { + fixture := prepareCoroCopyTestPlan(t, nil, true) + defer fixture.prog.Dispose() + call := fixture.calls["CopySlice"] + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.functions["CopySlice"], "") + if err != nil { + t.Fatal(err) + } + args := call.Call.Args + call.Call.Args = args[:1] + defer func() { call.Call.Args = args }() + if reason := audit.validateCopyBuiltin(call); !strings.Contains(reason, "invalid argument/result shape") { + t.Fatalf("malformed copy rejection = %q", reason) + } + }) + + t.Run("element mismatch", func(t *testing.T) { + fixture := prepareCoroCopyTestPlan(t, nil, true) + defer fixture.prog.Dispose() + function := fixture.functions["CopySlice"] + call := fixture.calls["CopySlice"] + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, function, "") + if err != nil { + t.Fatal(err) + } + source := call.Call.Args[1] + call.Call.Args[1] = function.Params[2] + defer func() { call.Call.Args[1] = source }() + if reason := audit.validateCopyBuiltin(call); !strings.Contains(reason, "element types differ") { + t.Fatalf("mismatched copy rejection = %q", reason) + } + }) +} + +func prepareCoroCopyTestPlan(t *testing.T, target *llssa.Target, loweredCalls bool) coroCopyTestPlan { + t.Helper() + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, coroCopyRuntimeFixture) + fooPkg := testProg.addPackage(t, "foo", coroCopyFixture) + testProg.ssa.Build() + + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + prog.SetRuntime(runtimePkg.types) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: fooPkg.ssa, Files: []*ast.File{fooPkg.file}}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(fooPkg.ssa.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := map[string]*ssa.Function{ + "CopySlice": fooPkg.ssa.Func("CopySlice"), + "CopyString": fooPkg.ssa.Func("CopyString"), + } + calls := make(map[string]*ssa.Call, len(functions)) + var roots coro.Roots + for name, function := range functions { + calls[name] = coroCopyBuiltinCall(t, function) + roots = append(roots, coro.Root{Function: function, Demand: coro.AsyncDemand}) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + config := coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + for _, root := range functions { + if function == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + } + return coro.SSAFunctionPolicy{}, nil + }, + } + if loweredCalls { + config.ClassifyLoweredCalls = universe.CoroLoweredCalls + } + plan, err := coro.AnalyzeSSA(fooPkg.ssa.Prog, roots, config) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return coroCopyTestPlan{ + prog: prog, + runtimePkg: runtimePkg, + fooPkg: fooPkg, + universe: universe, + plan: plan, + functions: functions, + calls: calls, + } +} + +func coroCopyBuiltinCall(t *testing.T, function *ssa.Function) *ssa.Call { + t.Helper() + var found *ssa.Call + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + builtin, ok := call.Call.Value.(*ssa.Builtin) + if !ok || builtin.Name() != "copy" { + continue + } + if found != nil { + t.Fatalf("%s has more than one copy builtin", function) + } + found = call + } + } + if found == nil { + t.Fatalf("%s has no copy builtin", function) + } + return found +} diff --git a/cl/coro_critical.go b/cl/coro_critical.go new file mode 100644 index 0000000000..a80e226c6e --- /dev/null +++ b/cl/coro_critical.go @@ -0,0 +1,387 @@ +/* + * 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 ( + "fmt" + "go/token" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +const coroCriticalDepthLimit = ^uint32(0) >> 2 + +// coroCriticalProof is the authoritative function-local C0 region proof used +// by both preflight and physical emission. Every map is keyed by the exact +// frozen SSA object; no source name or regenerated block identity participates +// in lowering. +type coroCriticalProof struct { + roles map[*ssa.Call]coroCriticalCallRole + entryDepth map[*ssa.BasicBlock]uint32 + beforeDepth map[ssa.Instruction]uint32 + afterDepth map[ssa.Instruction]uint32 +} + +// proveCoroCriticalRegions proves structured preemption masking without +// introducing a second executable IR. C0 is intentionally strict: a masked +// region is bounded, helper-free, path-balanced, and contains no ordinary call +// or stack cut. Wider scheduler-owned transactions must be represented by an +// operation source, not smuggled through this mask. +func proveCoroCriticalRegions( + universe *EmissionUniverse, + plan *coro.SSAPlan, + audit *coroPhysicalPureSSAAudit, +) (*coroCriticalProof, error) { + if audit == nil || audit.fn == nil || len(audit.fn.Blocks) == 0 { + return nil, nil + } + fn := audit.fn + proof := &coroCriticalProof{ + roles: make(map[*ssa.Call]coroCriticalCallRole), + entryDepth: make(map[*ssa.BasicBlock]uint32, len(fn.Blocks)), + beforeDepth: make(map[ssa.Instruction]uint32), + afterDepth: make(map[ssa.Instruction]uint32), + } + if universe == nil { + return nil, nil + } + + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + role, critical, err := universe.coroCriticalCallSite(call) + if err != nil { + return nil, coroCriticalInstructionError(fn, instruction, err.Error()) + } + if !critical { + continue + } + if plan == nil || !plan.ElidesCall(call) { + return nil, coroCriticalInstructionError(fn, instruction, "critical marker is not frozen as an elided compiler intrinsic") + } + if _, retained := plan.CallPlan(call); retained { + return nil, coroCriticalInstructionError(fn, instruction, "critical marker retained an ordinary managed CallPlan") + } + proof.roles[call] = role + } + } + if len(proof.roles) == 0 { + return nil, nil + } + if plan == nil || audit.ctx == nil { + return nil, fmt.Errorf("function %q critical regions require a frozen plan and lowering context", fn.String()) + } + + reachable := coroCriticalReachableBlocks(fn) + for call := range proof.roles { + if !reachable[call.Block()] { + return nil, coroCriticalInstructionError(fn, call, "critical marker is unreachable") + } + } + + outDepth := make(map[*ssa.BasicBlock]uint32, len(fn.Blocks)) + entry := fn.Blocks[0] + proof.entryDepth[entry] = 0 + queued := map[*ssa.BasicBlock]bool{entry: true} + queue := []*ssa.BasicBlock{entry} + for len(queue) != 0 { + block := queue[0] + queue = queue[1:] + depth := proof.entryDepth[block] + for _, instruction := range block.Instrs { + proof.beforeDepth[instruction] = depth + if call, ok := instruction.(*ssa.Call); ok { + switch proof.roles[call] { + case coroCriticalCallEnter: + if depth == coroCriticalDepthLimit { + return nil, coroCriticalInstructionError(fn, instruction, "critical nesting overflows the packed runtime depth") + } + depth++ + case coroCriticalCallExit: + if depth == 0 { + return nil, coroCriticalInstructionError(fn, instruction, "critical exit underflows depth zero") + } + depth-- + } + } + proof.afterDepth[instruction] = depth + if depth != 0 { + switch instruction.(type) { + case *ssa.Return, *ssa.Panic: + return nil, coroCriticalInstructionError(fn, instruction, "function exit or panic is forbidden while preemption is masked") + } + } + } + outDepth[block] = depth + if len(block.Succs) == 0 && depth != 0 { + return nil, fmt.Errorf("function %q block %d terminates with unbalanced critical depth %d", fn.String(), block.Index, depth) + } + for _, successor := range block.Succs { + if successor == nil || !reachable[successor] { + return nil, fmt.Errorf("function %q block %d has an invalid critical CFG successor", fn.String(), block.Index) + } + previous, seen := proof.entryDepth[successor] + if seen && previous != depth { + return nil, fmt.Errorf( + "function %q critical depth join mismatch at block %d: %d versus %d", + fn.String(), successor.Index, previous, depth, + ) + } + if !seen { + proof.entryDepth[successor] = depth + } + if !queued[successor] { + queued[successor] = true + queue = append(queue, successor) + } + } + } + + // LLVM emission may retain structurally unreachable source blocks. They are + // outside every critical region, but still receive total depth maps so + // codegen never guesses a missing proof entry. + for _, block := range fn.Blocks { + if reachable[block] { + continue + } + proof.entryDepth[block] = 0 + outDepth[block] = 0 + for _, instruction := range block.Instrs { + proof.beforeDepth[instruction] = 0 + proof.afterDepth[instruction] = 0 + } + } + + if err := validateCoroCriticalMaskedDAG(fn, proof, outDepth, reachable); err != nil { + return nil, err + } + for _, block := range fn.Blocks { + if !reachable[block] { + continue + } + for _, instruction := range block.Instrs { + before, after := proof.beforeDepth[instruction], proof.afterDepth[instruction] + if before == 0 && after == 0 { + continue + } + if err := validateCoroCriticalInstruction(universe, plan, audit, proof, instruction); err != nil { + return nil, coroCriticalInstructionError(fn, instruction, err.Error()) + } + } + } + return proof, nil +} + +func coroCriticalReachableBlocks(fn *ssa.Function) map[*ssa.BasicBlock]bool { + reachable := make(map[*ssa.BasicBlock]bool) + if fn == nil || len(fn.Blocks) == 0 { + return reachable + } + queue := []*ssa.BasicBlock{fn.Blocks[0]} + reachable[fn.Blocks[0]] = true + for len(queue) != 0 { + block := queue[0] + queue = queue[1:] + for _, successor := range block.Succs { + if successor != nil && !reachable[successor] { + reachable[successor] = true + queue = append(queue, successor) + } + } + } + return reachable +} + +// validateCoroCriticalMaskedDAG rejects cycles whose backedge remains masked +// and computes the longest dynamic masked instruction path over the resulting +// DAG. A surrounding loop is legal only when every iteration returns to depth +// zero before its backedge. +func validateCoroCriticalMaskedDAG( + fn *ssa.Function, + proof *coroCriticalProof, + outDepth map[*ssa.BasicBlock]uint32, + reachable map[*ssa.BasicBlock]bool, +) error { + indegree := make(map[*ssa.BasicBlock]int, len(fn.Blocks)) + reachableCount := 0 + for _, block := range fn.Blocks { + if !reachable[block] { + continue + } + reachableCount++ + if outDepth[block] == 0 { + continue + } + for _, successor := range block.Succs { + indegree[successor]++ + } + } + queue := make([]*ssa.BasicBlock, 0, reachableCount) + for _, block := range fn.Blocks { + if reachable[block] && indegree[block] == 0 { + queue = append(queue, block) + } + } + carry := make(map[*ssa.BasicBlock]int, reachableCount) + processed := 0 + for len(queue) != 0 { + block := queue[0] + queue = queue[1:] + processed++ + length := 0 + if proof.entryDepth[block] != 0 { + length = carry[block] + } + for _, instruction := range block.Instrs { + before, after := proof.beforeDepth[instruction], proof.afterDepth[instruction] + active := before != 0 || after != 0 + if !active { + length = 0 + continue + } + if before == 0 { + length = 0 + } + if _, debug := instruction.(*ssa.DebugRef); !debug { + length++ + if length > coroPreemptInstructionBudget { + return coroCriticalInstructionError(fn, instruction, fmt.Sprintf( + "critical path exceeds the %d-instruction preemption budget", coroPreemptInstructionBudget, + )) + } + } + if after == 0 { + length = 0 + } + } + if outDepth[block] != 0 { + for _, successor := range block.Succs { + if length > carry[successor] { + carry[successor] = length + } + } + } + for _, successor := range block.Succs { + if outDepth[block] == 0 { + continue + } + indegree[successor]-- + if indegree[successor] == 0 { + queue = append(queue, successor) + } + } + } + if processed != reachableCount { + return fmt.Errorf("function %q has a cyclic CFG path while preemption is masked", fn.String()) + } + return nil +} + +func validateCoroCriticalInstruction( + universe *EmissionUniverse, + plan *coro.SSAPlan, + audit *coroPhysicalPureSSAAudit, + proof *coroCriticalProof, + instruction ssa.Instruction, +) error { + if _, debug := instruction.(*ssa.DebugRef); debug { + return nil + } + if call, ok := instruction.(*ssa.Call); ok { + if role := proof.roles[call]; role == coroCriticalCallEnter || role == coroCriticalCallExit { + return nil + } + frozen, found, err := universe.coroProgramIR.callSitePlan(call) + if err != nil || !found { + if err == nil { + err = fmt.Errorf("call is absent from the frozen ProgramIR") + } + return err + } + if frozen.failure != "" { + return fmt.Errorf("invalid frozen intrinsic: %s", frozen.failure) + } + if !frozen.plan.Intrinsic || !isCoroAtomicIntrinsic(frozen.opcode) || + !frozen.plan.ElidesCall() || !plan.ElidesCall(call) { + return fmt.Errorf("ordinary or non-atomic call is forbidden while preemption is masked") + } + if frozen.plan.IntrinsicSemantics != CoroIntrinsicCallInlineNoSuspend { + return fmt.Errorf("critical atomic intrinsic lacks exact inline no-suspend semantics") + } + return nil + } + + switch current := instruction.(type) { + case *ssa.Phi, *ssa.FieldAddr, *ssa.IndexAddr, *ssa.Field, *ssa.Extract, + *ssa.ChangeType, *ssa.Convert, *ssa.BinOp, *ssa.Store: + handled, reason := audit.validate(instruction) + if !handled { + return fmt.Errorf("scalar/address instruction has no physical lowering proof") + } + if reason != "" { + return fmt.Errorf("scalar/address instruction is not critical-safe: %s", reason) + } + case *ssa.UnOp: + if current.Op != token.MUL && current.Op != token.SUB && current.Op != token.XOR && current.Op != token.NOT { + return fmt.Errorf("unsupported unary operation while preemption is masked") + } + handled, reason := audit.validate(instruction) + if !handled || reason != "" { + if reason == "" { + reason = "no physical lowering proof" + } + return fmt.Errorf("unary instruction is not critical-safe: %s", reason) + } + case *ssa.If: + if !coroLeafScalar(current.Cond.Type()) { + return fmt.Errorf("non-scalar branch condition while preemption is masked") + } + case *ssa.Jump: + case *ssa.Return: + return fmt.Errorf("return is forbidden while preemption is masked") + case *ssa.Panic: + return fmt.Errorf("panic is forbidden while preemption is masked") + default: + return fmt.Errorf("%T is outside the bounded critical-region allowlist", instruction) + } + if reason := audit.requireNoRuntimeHelpers(instruction); reason != "" { + return fmt.Errorf("instruction has hidden runtime lowering: %s", reason) + } + return nil +} + +func coroCriticalInstructionError(fn *ssa.Function, instruction ssa.Instruction, reason string) error { + block, ordinal := -1, -1 + if instruction != nil && instruction.Block() != nil { + block = instruction.Block().Index + for index, candidate := range instruction.Block().Instrs { + if candidate == instruction { + ordinal = index + break + } + } + } + name := "" + if fn != nil { + name = fn.String() + } + return fmt.Errorf("function %q critical instruction block=%d index=%d: %s", name, block, ordinal, reason) +} diff --git a/cl/coro_critical_ir_test.go b/cl/coro_critical_ir_test.go new file mode 100644 index 0000000000..19402318f5 --- /dev/null +++ b/cl/coro_critical_ir_test.go @@ -0,0 +1,221 @@ +//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 ( + "bytes" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroCriticalIRTestSource = `package foo + +import _ "unsafe" + +//go:linkname criticalEnter llgo.coroCriticalEnter +func criticalEnter() + +//go:linkname criticalExit llgo.coroCriticalExit +func criticalExit() + +var cell uint32 + +func Root(value uint32) uint32 { + criticalEnter() + cell = value + result := cell + criticalExit() + return result +} +` + +func TestCoroCriticalRegionLoweringNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root, enter, exit := compileCoroCriticalIRFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if !plan.ElidesCall(enter) || !plan.ElidesCall(exit) { + t.Fatal("critical marker declarations were not both frontend-elided") + } + if _, retained := plan.CallPlan(enter); retained { + t.Fatal("critical enter retained an ordinary managed CallPlan") + } + if _, retained := plan.CallPlan(exit); retained { + t.Fatal("critical exit retained an ordinary managed CallPlan") + } + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || !rootPlan.LocalEffect.Contains(coro.YieldOnly) { + t.Fatalf("critical Root plan = %+v, present=%t; want one yield-capable coroutine body", rootPlan, ok) + } + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify critical coroutine before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Root").String() + for _, marker := range []string{"@foo.criticalEnter", "@foo.criticalExit", "@llgo.coroCriticalEnter", "@llgo.coroCriticalExit"} { + if strings.Contains(body, marker) { + t.Fatalf("source critical marker %q leaked into physical IR:\n%s", marker, body) + } + } + begin := strings.Index(body, "call void @"+coroCriticalEnterHookV1) + endRelative := -1 + if begin >= 0 { + endRelative = strings.Index(body[begin:], "call i1 @"+coroCriticalExitHookV1) + } + if begin < 0 || endRelative < 0 { + t.Fatalf("physical body lacks ordered critical hooks:\n%s", body) + } + if got := strings.Count(body[:begin], "call i1 @"+coroPreemptPollHookV1); got != 1 { + t.Fatalf("outer critical enter has %d pre-entry polls, want the one block-entry safepoint:\n%s", got, body) + } + span := body[begin : begin+endRelative] + for _, forbidden := range []string{ + "@" + coroPreemptPollHookV1, + "@" + coroYieldPrepareHookV1, + "@llvm.coro.suspend", + } { + if strings.Contains(span, forbidden) { + t.Fatalf("critical span contains forbidden safepoint %q:\n%s", forbidden, span) + } + } + if exitIndex := begin + endRelative; !strings.Contains(body[exitIndex:], "call void @"+coroYieldPrepareHookV1) || + !strings.Contains(body[exitIndex:], "call i8 @llvm.coro.suspend") { + t.Fatalf("outer critical exit is not connected to conditional runnable handoff:\n%s", body) + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() { + t.Fatalf("post-CoroSplit module lacks Root resume body:\n%s", module.String()) + } + for _, hook := range []string{coroCriticalEnterHookV1, coroCriticalExitHookV1} { + if !strings.Contains(module.String(), "@"+hook) { + t.Fatalf("post-CoroSplit module lost critical ABI hook %q", hook) + } + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit critical object: %v\n%s", err, module.String()) + } + defer object.Dispose() + for _, hook := range []string{coroCriticalEnterHookV1, coroCriticalExitHookV1} { + if !bytes.Contains(object.Bytes(), []byte(hook)) { + t.Fatalf("post-CoroSplit object lost unresolved critical ABI symbol %q", hook) + } + } + }) + } +} + +func compileCoroCriticalIRFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, *ssa.Call, *ssa.Call, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroCriticalIRTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + var enter, exit *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Common().StaticCallee() == nil { + continue + } + switch call.Common().StaticCallee().Name() { + case "criticalEnter": + enter = call + case "criticalExit": + exit = call + } + } + } + if enter == nil || exit == nil { + prog.Dispose() + t.Fatal("critical fixture lacks exact enter/exit calls") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, Exec: coro.NeedsPreempt}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root, enter, exit +} diff --git a/cl/coro_critical_lowering.go b/cl/coro_critical_lowering.go new file mode 100644 index 0000000000..93870fa6f4 --- /dev/null +++ b/cl/coro_critical_lowering.go @@ -0,0 +1,76 @@ +/* + * 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 ( + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +func (c *coroBodyContext) criticalCallDepth(common *ssa.CallCommon) (coroCriticalCallRole, uint32) { + if c == nil || c.critical == nil || common == nil { + panic("coroutine critical lowering requires a frozen CFG proof") + } + for call, role := range c.critical.roles { + if call != nil && call.Common() == common { + depth, ok := c.critical.beforeDepth[call] + if !ok { + panic("coroutine critical marker has no proven input depth") + } + return role, depth + } + } + panic("coroutine critical lowering received an unproved marker") +} + +func (p *context) compileCoroCriticalEnter(b llssa.Builder, common *ssa.CallCommon) { + body := p.coroBody() + if body == nil || b.Func != p.fn || body.criticalEnter.IsNil() { + panic("llgo.coroCriticalEnter requires an active critical-capable coroutine body") + } + role, depth := body.criticalCallDepth(common) + if role != coroCriticalCallEnter { + panic("llgo.coroCriticalEnter disagrees with its frozen marker role") + } + // Entering the outer mask is itself a safepoint. Once the runtime depth is + // nonzero no source poll may be emitted until the matching outer exit. + if depth == 0 && body.needsPreempt && !body.sourceBlockPollFresh { + body.pollAndSuspendForPreempt(b) + } + b.Call(body.criticalEnter, body.task) + if depth == 0 { + body.instructions = 0 + } + body.sourceBlockPollFresh = false +} + +func (p *context) compileCoroCriticalExit(b llssa.Builder, common *ssa.CallCommon) { + body := p.coroBody() + if body == nil || b.Func != p.fn || body.criticalExit.IsNil() { + panic("llgo.coroCriticalExit requires an active critical-capable coroutine body") + } + role, depth := body.criticalCallDepth(common) + if role != coroCriticalCallExit || depth == 0 { + panic("llgo.coroCriticalExit disagrees with its frozen marker role/depth") + } + requested := b.Call(body.criticalExit, body.task) + if depth == 1 { + body.suspendCurrentFrameIfYieldRequested(b, requested) + body.instructions = 0 + body.sourceBlockPollFresh = true + } +} diff --git a/cl/coro_critical_proof_test.go b/cl/coro_critical_proof_test.go new file mode 100644 index 0000000000..116781f11a --- /dev/null +++ b/cl/coro_critical_proof_test.go @@ -0,0 +1,197 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +const coroCriticalProofPreamble = `package foo +import _ "unsafe" +//go:linkname enter llgo.coroCriticalEnter +func enter() +//go:linkname exit llgo.coroCriticalExit +func exit() +var cell uint32 +var sink *uint32 +` + +func TestCoroCriticalRegionProofRejectsInvalidCFGAndOperations(t *testing.T) { + tests := []struct { + name string + body string + want string + }{ + { + name: "underflow", + body: `func Root() { exit() }`, + want: "underflows depth zero", + }, + { + name: "unbalanced return", + body: `func Root() { enter() }`, + want: "function exit or panic is forbidden", + }, + { + name: "depth join mismatch", + body: `func Root(flag bool) { + if flag { enter() } + cell = 1 + if flag { exit() } +}`, + want: "critical depth join mismatch", + }, + { + name: "masked cycle", + body: `func Root(n uint32) { + enter() + for n != 0 { cell = n; n-- } + exit() +}`, + want: "cyclic CFG path", + }, + { + name: "ordinary call", + body: `func helper() { cell = 1 } +func Root() { enter(); helper(); exit() }`, + want: "ordinary or non-atomic call", + }, + { + name: "allocation", + body: `func Root() { enter(); sink = new(uint32); exit() }`, + want: "outside the bounded critical-region allowlist", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := proveCoroCriticalFixture(t, coroCriticalProofPreamble+test.body) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("critical proof error = %v, want substring %q", err, test.want) + } + }) + } +} + +func TestCoroCriticalRegionProofAcceptsBalancedBranchAndDepthZeroLoop(t *testing.T) { + for _, body := range []string{ + `func Root(flag bool) { + enter() + if flag { cell = 1 } else { cell = 2 } + exit() +}`, + `func Root(n uint32) { + for n != 0 { + enter() + cell = n + exit() + n-- + } +}`, + `func Root() { + enter() + enter() + cell = 1 + exit() + exit() +}`, + } { + proof, err := proveCoroCriticalFixture(t, coroCriticalProofPreamble+body) + if err != nil || proof == nil { + t.Fatalf("balanced critical proof = %v, %v", proof, err) + } + } +} + +func TestCoroCriticalRegionProofRejectsOverBudgetPath(t *testing.T) { + var source strings.Builder + source.WriteString(coroCriticalProofPreamble) + source.WriteString("func Root(v uint32) { enter();\n") + for index := 0; index < coroPreemptInstructionBudget+1; index++ { + source.WriteString("cell = v\n") + } + source.WriteString("exit() }") + _, err := proveCoroCriticalFixture(t, source.String()) + if err == nil || !strings.Contains(err.Error(), "exceeds the 64-instruction preemption budget") { + t.Fatalf("over-budget critical proof error = %v", err) + } +} + +func TestCoroCriticalMarkerCannotBeMaterializedAsFunctionValue(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, coroCriticalProofPreamble+` +var marker = enter +func Root() { marker() } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + _, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err == nil || !strings.Contains(err.Error(), "critical marker") || !strings.Contains(err.Error(), "cannot be materialized as a function value") { + t.Fatalf("critical marker materialization error = %v", err) + } +} + +func proveCoroCriticalFixture(t *testing.T, source string) (*coroCriticalProof, error) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, Exec: coro.NeedsPreempt}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + if err != nil { + t.Fatal(err) + } + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "") + if err != nil { + t.Fatal(err) + } + return proveCoroCriticalRegions(universe, plan, audit) +} diff --git a/cl/coro_darwin_environment_shadow_test.go b/cl/coro_darwin_environment_shadow_test.go new file mode 100644 index 0000000000..06f6693d48 --- /dev/null +++ b/cl/coro_darwin_environment_shadow_test.go @@ -0,0 +1,104 @@ +//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/ast" + "testing" +) + +const coroDarwinEnvironmentCallableShadowFixture = `package darwinenv + +//llgo:link funcPCABI0 llgo.funcPCABI0 +func funcPCABI0(fn any) uintptr + +//llgo:link syscall1Int32 llgo.syscall32 +func syscall1Int32(fn, a1 uintptr) (uintptr, uintptr, uintptr) + +//llgo:link syscall3Int32 llgo.syscall32 +func syscall3Int32(fn, a1, a2, a3 uintptr) (uintptr, uintptr, uintptr) + +//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete abi=word-call.v1/3 +func libc_setenv_trampoline() + +//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete abi=word-call.v1/1 +func libc_unsetenv_trampoline() + +func Setenv(name, value uintptr) uintptr { + r1, _, _ := syscall3Int32(funcPCABI0(libc_setenv_trampoline), name, value, 1) + return r1 +} + +func Unsetenv(name uintptr) uintptr { + r1, _, _ := syscall1Int32(funcPCABI0(libc_unsetenv_trampoline), name) + return r1 +} +` + +func TestCoroDarwinEnvironmentWorkerPublishesExactCallableShadows(t *testing.T) { + testProg := newEmissionTestProgram() + const packagePath = "example.com/emission/darwinenv" + pkg := testProg.addPackage(t, packagePath, coroDarwinEnvironmentCallableShadowFixture) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + prog.SetLinkname(packagePath+".libc_setenv_trampoline", "C.setenv") + prog.SetLinkname(packagePath+".libc_unsetenv_trampoline", "C.unsetenv") + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + t.Fatal(err) + } + analysis, err := AnalyzeCoroCallableShadows(universe) + if err != nil { + t.Fatal(err) + } + + for _, test := range []struct { + wrapper string + target string + physical string + arity int + }{ + {wrapper: "Setenv", target: "libc_setenv_trampoline", physical: "setenv", arity: 3}, + {wrapper: "Unsetenv", target: "libc_unsetenv_trampoline", physical: "unsetenv", arity: 1}, + } { + t.Run(test.wrapper, func(t *testing.T) { + wrapper := pkg.ssa.Func(test.wrapper) + producer := exactIntrinsicOpcodeCall(t, universe, wrapper, llgoFuncPCABI0) + shadow, ok := analysis.Producer(producer) + if !ok || shadow.Target == nil || shadow.Target.Name() != test.target || + shadow.PhysicalSymbol != test.physical || shadow.ContractCertificateID == "" || + shadow.LegacyWorkerAddressCompat || + shadow.ABI != (CoroCallableShadowABI{Family: coroCallableShadowWorkerSyscallFamily, WordArgs: test.arity}) { + reason, rejected := analysis.ProducerRejection(producer) + t.Fatalf("callable shadow = %+v, %t; rejection=%q,%t", shadow, ok, reason, rejected) + } + + call := exactWorkerSyscallCall(t, universe, wrapper) + certificate, certified, err := universe.CoroWorkerSyscallCertificate(call) + if err != nil || !certified || certificate.ID == "" || + certificate.StaticTargetCount != 1 || certificate.WorkerABISignature == "" { + t.Fatalf("worker certificate = %+v, %t, %v", certificate, certified, err) + } + }) + } +} diff --git a/cl/coro_defer.go b/cl/coro_defer.go new file mode 100644 index 0000000000..bde43f262c --- /dev/null +++ b/cl/coro_defer.go @@ -0,0 +1,1306 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + "github.com/goplus/llgo/cl/blocks" + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// PhysicalABIV1 cannot use LLGo's legacy setjmp/TLS defer chain: that chain +// describes a native activation, while a stackless coroutine activation lives +// in the LLVM coroutine frame. Acyclic sites use one frame-resident active bit +// and typed argument slots per site. If any site is cyclic, every site in that +// function instead pushes one managed, typed record onto a frame-rooted LIFO +// chain. Using one chain for the whole function preserves registration order +// when acyclic and cyclic sites interleave; exact site tags select statically +// compiled call paths, so no function pointer is recovered from scalar data. +type coroStaticCleanupTargetKind uint8 + +const ( + coroStaticCleanupPlain coroStaticCleanupTargetKind = iota + coroStaticCleanupCoroutine + coroStaticCleanupDispatch +) + +type coroStaticCleanupSitePlan struct { + instruction *ssa.Defer + target *ssa.Function + targetPlan coro.FunctionPlan + kind coroStaticCleanupTargetKind + closure *ssa.MakeClosure + descriptor ssa.Value + signature *types.Signature + callPlan coro.SSACallPlan + tag uint32 +} + +type coroStaticCleanupPlan struct { + sites []*coroStaticCleanupSitePlan + terminalResultAllocations []*ssa.Alloc + dynamic bool + dynamicTrigger *ssa.Defer + dynamicAlloc *ssa.Function + dynamicFree *ssa.Function +} + +// CoroStaticCleanupPlainTarget reports the narrow EmitPlain exception usable +// by explicit-status entry resolution. Every planned call consumer of target +// must be a certified static defer site; roots, compiler-inserted calls, and +// any ordinary/spawn/dynamic call keep the result false. This is intentionally +// a query over the frozen SSA plan rather than a symbol-name annotation. +func (u *EmissionUniverse) CoroStaticCleanupPlainTarget( + whole *coro.SSAPlan, + target *ssa.Function, + frameRetentionABI string, +) (bool, error) { + if u == nil || whole == nil || target == nil { + return false, fmt.Errorf("static cleanup plain-target query requires a universe, plan, and target") + } + targetPlan, ok := whole.FunctionPlan(target) + if !ok { + return false, fmt.Errorf("static cleanup plain-target query: target %q is absent from the plan", target.Name()) + } + if targetPlan.External != coro.Defined || targetPlan.Emission != coro.EmitPlain || + targetPlan.Primary != coro.PrimaryPlain || targetPlan.FuncRep != coro.DirectPlain || + targetPlan.Demand == coro.NoDemand || targetPlan.Effect != coro.NoSuspend { + return false, nil + } + for _, root := range whole.Roots() { + if root.Function == target { + return false, nil + } + } + for _, owner := range whole.Functions() { + for _, lowered := range whole.LoweredCalls(owner.Function) { + if lowered.Target == target { + return false, nil + } + } + } + + certified := false + for _, owner := range whole.Functions() { + function := owner.Function + if function == nil { + continue + } + needsOwnerProof := false + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if !ok { + continue + } + callPlan, planned := whole.CallPlan(call) + if !planned || !coroCleanupCallPlanContains(callPlan, targetPlan.ID) { + continue + } + if _, deferCall := instruction.(*ssa.Defer); !deferCall || callPlan.Kind != coro.CallDefer { + return false, nil + } + needsOwnerProof = true + } + } + if !needsOwnerProof { + continue + } + ownerCleanup, err := prepareCoroStaticCleanupPlan( + function, whole, u, frameRetentionABI, true, + ) + if err != nil { + return false, fmt.Errorf("static cleanup plain-target query: owner %q: %w", owner.Plan.ID, err) + } + for _, site := range ownerCleanup.sites { + if site.target == target && site.kind == coroStaticCleanupPlain { + certified = true + } + } + } + return certified, nil +} + +func coroCleanupCallPlanContains(plan coro.SSACallPlan, target coro.FunctionID) bool { + for _, candidate := range plan.Targets { + if candidate == target { + return true + } + } + return false +} + +func prepareCoroStaticCleanupPlan( + fn *ssa.Function, + whole *coro.SSAPlan, + universe *EmissionUniverse, + frameRetentionABI string, + explicitPanic bool, +) (*coroStaticCleanupPlan, error) { + if fn == nil || whole == nil { + return nil, nil + } + caller, ok := whole.FunctionPlan(fn) + if !ok { + return nil, fmt.Errorf("function %q has no compilation plan", fn.Name()) + } + infos := blocks.Infos(fn.Blocks) + byInstruction := make(map[*ssa.Defer]*coroStaticCleanupSitePlan) + allSites := make([]*coroStaticCleanupSitePlan, 0) + var dynamicTrigger *ssa.Defer + runDefers := 0 + for _, block := range fn.Blocks { + for instructionIndex, raw := range block.Instrs { + switch instruction := raw.(type) { + case *ssa.Defer: + if instruction.DeferStack != nil { + return nil, fmt.Errorf("defer in block %d uses an alternate dynamic defer stack", block.Index) + } + if infos[block.Index].InLoop && dynamicTrigger == nil { + dynamicTrigger = instruction + } + target, targetPlan, kind, err := resolveCoroStaticCleanupTarget(whole, caller, instruction, universe) + if err != nil { + return nil, fmt.Errorf("defer in block %d: %w", block.Index, err) + } + closure, _ := instruction.Call.Value.(*ssa.MakeClosure) + // A plain defer still executes inline on this native activation and + // therefore needs a strict no-unwind proof. A coroutine defer returns + // Panic through the parent-owned CompletionRecord; awaitCoroChild + // re-enters this same drainer after clearing the active site, so older + // records still run with the replacement panic value. + if kind == coroStaticCleanupPlain { + if reason := validateCoroStaticCleanupNoUnwind( + whole, universe, target, targetPlan, frameRetentionABI, + ); reason != "" { + return nil, fmt.Errorf("plain defer target %q has no exact no-unwind proof: %s", targetPlan.ID, reason) + } + } + site := &coroStaticCleanupSitePlan{ + instruction: instruction, + target: target, + targetPlan: targetPlan, + kind: kind, + closure: closure, + } + if kind == coroStaticCleanupDispatch { + site.descriptor = instruction.Call.Value + site.signature = instruction.Call.Signature() + callPlan, planned := whole.CallPlan(instruction) + if !planned { + return nil, fmt.Errorf("defer in block %d: managed descriptor cleanup lost its CallPlan", block.Index) + } + site.callPlan = callPlan + if err := validateCoroManagedCleanupPlainTargets( + whole, universe, callPlan, frameRetentionABI, + ); err != nil { + return nil, fmt.Errorf("defer in block %d: %w", block.Index, err) + } + } + byInstruction[instruction] = site + allSites = append(allSites, site) + case *ssa.RunDefers: + if !coroStaticRunDefersReturns(block, instructionIndex) { + return nil, fmt.Errorf("RunDefers in block %d is not followed only by named-result reloads and the terminal Return", block.Index) + } + runDefers++ + } + } + } + + if len(byInstruction) == 0 { + if caller.Exec.Contains(coro.NeedsCleanupFrame) || runDefers != 0 { + return nil, fmt.Errorf("needs-cleanup-frame body has no supported static defer site") + } + return nil, nil + } + if !caller.Exec.Contains(coro.NeedsCleanupFrame) { + return nil, fmt.Errorf("static defer body lacks needs-cleanup-frame execution classification") + } + if runDefers == 0 { + return nil, fmt.Errorf("static defer body has no RunDefers instruction") + } + if !explicitPanic { + return nil, fmt.Errorf("static coroutine defer cleanup requires the explicit-status panic ABI; legacy panic cannot guarantee cleanup") + } + terminalResultAllocations, err := coroStaticTerminalReconstructionAllocations(fn) + if err != nil { + return nil, err + } + if dynamicTrigger != nil { + if uint64(len(allSites)) > uint64(^uint32(0)) { + return nil, fmt.Errorf("dynamic cleanup site count %d exceeds the stable tag space", len(allSites)) + } + for index, site := range allSites { + // Zero remains an invalid/corrupt record marker. Source block and + // instruction order are immutable in the prepared SSA universe, so this + // one-based tag is deterministic for validation and code generation. + site.tag = uint32(index) + 1 + } + allocator, allocOK := whole.ResolveLoweredCall(fn, "AllocU") + releaser, freeOK := whole.ResolveLoweredCall(fn, "FreeDeferNode") + if !allocOK || allocator == nil || !freeOK || releaser == nil { + return nil, fmt.Errorf("dynamic cleanup requires exact owner-scoped AllocU and FreeDeferNode edges") + } + return &coroStaticCleanupPlan{ + sites: allSites, + terminalResultAllocations: terminalResultAllocations, + dynamic: true, + dynamicTrigger: dynamicTrigger, + dynamicAlloc: allocator, + dynamicFree: releaser, + }, nil + } + + // blocks.Infos' Next chain is a topological order outside SCCs. Defer + // sites in SCCs were rejected above, so reversing this list later is the + // exact registration order for every path on which two sites both ran. + ordered := make([]*coroStaticCleanupSitePlan, 0, len(byInstruction)) + for index := 0; index >= 0; index = infos[index].Next { + for _, raw := range fn.Blocks[index].Instrs { + if instruction, ok := raw.(*ssa.Defer); ok { + ordered = append(ordered, byInstruction[instruction]) + } + } + } + if len(ordered) != len(byInstruction) { + return nil, fmt.Errorf("static defer order covers %d of %d sites", len(ordered), len(byInstruction)) + } + return &coroStaticCleanupPlan{ + sites: ordered, + terminalResultAllocations: terminalResultAllocations, + }, nil +} + +// validateCoroManagedCleanupPlainTargets closes the one unwind hole in +// cleanup-time descriptor dispatch. A coroutine capability reports Panic via +// its child CompletionRecord; a plain capability executes inline in the +// drainer and therefore must have the same exact no-unwind proof as a static +// plain defer. Until the descriptor producer ABI publishes an equivalent +// capability bit, an open set is deliberately rejected: an unknown HasPlain +// target cannot be inferred safe from its function type. +func validateCoroManagedCleanupPlainTargets( + whole *coro.SSAPlan, + universe *EmissionUniverse, + callPlan coro.SSACallPlan, + frameRetentionABI string, +) error { + if whole == nil { + return fmt.Errorf("managed descriptor cleanup requires a compilation plan") + } + if callPlan.Open { + return fmt.Errorf("open managed descriptor cleanup has no plain no-unwind producer invariant") + } + for _, targetID := range callPlan.Targets { + target, found := whole.Function(targetID) + if !found || target == nil { + return fmt.Errorf("managed descriptor cleanup target %q is absent from the plan", targetID) + } + targetPlan, found := whole.FunctionPlan(target) + if !found || targetPlan.ID != targetID { + return fmt.Errorf("managed descriptor cleanup target %q has no canonical function plan", targetID) + } + switch targetPlan.Emission { + case coro.EmitCoroutine: + // The direct child completion transaction owns unwind/recovery. + case coro.EmitPlain: + if reason := validateCoroStaticCleanupNoUnwind( + whole, universe, target, targetPlan, frameRetentionABI, + ); reason != "" { + return fmt.Errorf("plain descriptor cleanup target %q has no exact no-unwind proof: %s", targetID, reason) + } + default: + return fmt.Errorf("managed descriptor cleanup target %q has unsupported emission %s", targetID, targetPlan.Emission) + } + } + return nil +} + +// coroDeferRequiresDynamicCleanup identifies the source occurrence that makes +// the whole owner use the heterogeneous cleanup chain. +func coroDeferRequiresDynamicCleanup(instruction *ssa.Defer) bool { + if instruction == nil || instruction.Parent() == nil || instruction.Block() == nil { + return false + } + infos := blocks.Infos(instruction.Parent().Blocks) + index := instruction.Block().Index + return index >= 0 && index < len(infos) && infos[index].InLoop +} + +// coroFunctionRequiresDynamicCleanup computes the owner-wide lowering choice +// once for ProgramIR construction. If one defer can execute repeatedly, every +// defer in the owner uses the same chain so registration order remains exact; +// consequently every source defer receives its own AllocU/FreeDeferNode +// physical placement in SitePlan. +func coroFunctionRequiresDynamicCleanup(fn *ssa.Function) bool { + if fn == nil || len(fn.Blocks) == 0 { + return false + } + infos := blocks.Infos(fn.Blocks) + for _, block := range fn.Blocks { + if block == nil || block.Index < 0 || block.Index >= len(infos) || !infos[block.Index].InLoop { + continue + } + for _, instruction := range block.Instrs { + if _, deferred := instruction.(*ssa.Defer); deferred { + return true + } + } + } + return false +} + +func validateCoroDynamicCleanupHelpers(plan *coroStaticCleanupPlan, whole *coro.SSAPlan) error { + if plan == nil || !plan.dynamic { + return nil + } + if whole == nil || plan.dynamicTrigger == nil || plan.dynamicAlloc == nil || plan.dynamicFree == nil { + return fmt.Errorf("dynamic cleanup helper proof is incomplete") + } + for _, helper := range []struct { + name string + target *ssa.Function + }{ + {name: "AllocU", target: plan.dynamicAlloc}, + {name: "FreeDeferNode", target: plan.dynamicFree}, + } { + call, frozen := whole.ResolveLoweredCallRecord(plan.dynamicTrigger.Parent(), helper.name) + if !frozen || call.Target != helper.target || call.RawPlain || call.UnwindOnly || call.ExplicitStatusElided { + return fmt.Errorf("dynamic cleanup %s edge is not one exact ordinary lowered call", helper.name) + } + targetPlan, frozen := whole.FunctionPlan(helper.target) + if !frozen || targetPlan.External != coro.Defined || targetPlan.Emission != coro.EmitPlain || + targetPlan.Primary != coro.PrimaryPlain || targetPlan.FuncRep != coro.DirectPlain || + targetPlan.Demand == coro.NoDemand || targetPlan.Effect != coro.NoSuspend || + targetPlan.Exec&(coro.MayUnwind|coro.BlockForeign|coro.NeedsPreempt|coro.OpaqueExec) != 0 { + return fmt.Errorf( + "dynamic cleanup %s target is not a demanded non-suspending, non-unwinding direct plain body (emission=%s primary=%s representation=%s demand=%s effect=%s exec=%s)", + helper.name, targetPlan.Emission, targetPlan.Primary, targetPlan.FuncRep, + targetPlan.Demand, targetPlan.Effect, targetPlan.Exec, + ) + } + } + allocSignature := plan.dynamicAlloc.Signature + if allocSignature == nil || allocSignature.Recv() != nil || allocSignature.Variadic() || + allocSignature.Params().Len() != 1 || allocSignature.Results().Len() != 1 || + !coroFrameRetentionUintptrLike(allocSignature.Params().At(0).Type()) || + !coroFrameRetentionUnsafePointer(allocSignature.Results().At(0).Type()) { + return fmt.Errorf("dynamic cleanup AllocU target has an invalid func(uintptr) unsafe.Pointer ABI") + } + freeSignature := plan.dynamicFree.Signature + if freeSignature == nil || freeSignature.Recv() != nil || freeSignature.Variadic() || + freeSignature.Params().Len() != 1 || freeSignature.Results().Len() != 0 || + !coroFrameRetentionUnsafePointer(freeSignature.Params().At(0).Type()) { + return fmt.Errorf("dynamic cleanup FreeDeferNode target has an invalid func(unsafe.Pointer) ABI") + } + return nil +} + +func coroStaticRunDefersReturns(block *ssa.BasicBlock, instructionIndex int) bool { + _, ok := coroStaticRunDefersReconstructionAllocations(block, instructionIndex) + return ok +} + +// coroStaticRunDefersReconstructionAllocations recognizes the exact x/tools +// terminal shape used for named results: RunDefers, zero or more direct loads +// from owner-local result cells, then Return. It returns the cells rather than +// merely a boolean so cleanup planning, frame proof, and code generation share +// one structural fact. +func coroStaticRunDefersReconstructionAllocations( + block *ssa.BasicBlock, + instructionIndex int, +) ([]*ssa.Alloc, bool) { + if block == nil || instructionIndex < 0 || instructionIndex >= len(block.Instrs) { + return nil, false + } + if len(block.Succs) != 0 { + return nil, false + } + suffix := block.Instrs[instructionIndex+1:] + loads := make(map[*ssa.UnOp]*ssa.Alloc) + seenAllocations := make(map[*ssa.Alloc]struct{}) + allocations := make([]*ssa.Alloc, 0) + for index, instruction := range suffix { + if _, debug := instruction.(*ssa.DebugRef); debug { + continue + } + switch instruction := instruction.(type) { + case *ssa.UnOp: + // A function with named results materializes those results in entry + // allocas so deferred calls can observe or replace them. x/tools emits + // the final loads after RunDefers and before Return. Accept only an + // exact load from one owner-local allocation; every other operation + // remains outside this terminal reconstruction tail. + alloc, ok := instruction.X.(*ssa.Alloc) + if !ok || instruction.Op != token.MUL || alloc.Parent() != block.Parent() { + return nil, false + } + loads[instruction] = alloc + if _, seen := seenAllocations[alloc]; !seen { + seenAllocations[alloc] = struct{}{} + allocations = append(allocations, alloc) + } + case *ssa.Return: + for _, remaining := range suffix[index+1:] { + if _, debug := remaining.(*ssa.DebugRef); !debug { + return nil, false + } + } + // Every accepted reconstruction load must flow directly to this + // terminal Return. This keeps the exception narrower than the general + // pure-SSA validator and prevents a future SSA shape from smuggling a + // computation into the post-cleanup continuation. + for load := range loads { + referrers := load.Referrers() + if referrers == nil || len(*referrers) == 0 { + return nil, false + } + for _, referrer := range *referrers { + if referrer == instruction { + continue + } + if _, debug := referrer.(*ssa.DebugRef); !debug { + return nil, false + } + } + } + return allocations, true + default: + return nil, false + } + } + return nil, false +} + +// coroStaticTerminalReconstructionAllocations returns the deterministic union +// of ordinary heap cells whose values are reconstructed after RunDefers. Only +// source-entry cells are eligible: moving a conditional or loop allocation to +// the coroutine prologue would change its execution count. Stack/frame cells +// need no special treatment because coroFrameAlloc already defines them in the +// physical ramp. +func coroStaticTerminalReconstructionAllocations(fn *ssa.Function) ([]*ssa.Alloc, error) { + if fn == nil { + return nil, nil + } + selected := make(map[*ssa.Alloc]struct{}) + for _, block := range fn.Blocks { + for instructionIndex, instruction := range block.Instrs { + if _, ok := instruction.(*ssa.RunDefers); !ok { + continue + } + allocations, ok := coroStaticRunDefersReconstructionAllocations(block, instructionIndex) + if !ok { + return nil, fmt.Errorf("RunDefers in block %d is not followed only by named-result reloads and the terminal Return", block.Index) + } + for _, allocation := range allocations { + if !allocation.Heap { + continue + } + if allocation.Block() == nil || allocation.Block().Index != 0 { + return nil, fmt.Errorf("RunDefers terminal heap allocation %q is outside source block zero", allocation.Name()) + } + selected[allocation] = struct{}{} + } + } + } + ordered := make([]*ssa.Alloc, 0, len(selected)) + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + allocation, ok := instruction.(*ssa.Alloc) + if !ok { + continue + } + if _, keep := selected[allocation]; keep { + ordered = append(ordered, allocation) + } + } + } + return ordered, nil +} + +// x/tools creates one implicit exceptional Return block for every function +// containing a syntactic defer, even when the source never calls recover. The +// legacy setjmp lowering used that block; the explicit-status cleanup drainer +// does not. Accept only the canonical predecessor-free, return-only shape so +// a real recover path cannot become silently unreachable. +func validateCoroStaticCleanupRecoverBlock(fn *ssa.Function) error { + if fn == nil || fn.Recover == nil { + return nil + } + block := fn.Recover + if len(block.Preds) != 0 || len(block.Succs) != 0 { + return fmt.Errorf("implicit recover block has predecessors=%d successors=%d", len(block.Preds), len(block.Succs)) + } + returns := 0 + for _, instruction := range block.Instrs { + switch instruction.(type) { + case *ssa.DebugRef, *ssa.UnOp, *ssa.Return: + if _, ok := instruction.(*ssa.Return); ok { + returns++ + } + default: + return fmt.Errorf("implicit recover block contains %T", instruction) + } + } + if returns != 1 { + return fmt.Errorf("implicit recover block has %d returns", returns) + } + return nil +} + +func resolveCoroStaticCleanupTarget( + whole *coro.SSAPlan, + caller coro.FunctionPlan, + instruction *ssa.Defer, + universes ...*EmissionUniverse, +) (*ssa.Function, coro.FunctionPlan, coroStaticCleanupTargetKind, error) { + if whole == nil || instruction == nil || instruction.Common() == nil { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("requires an exact compilation CallPlan") + } + common := instruction.Common() + callPlan, ok := whole.CallPlan(instruction) + if !ok { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("defer has no compilation CallPlan") + } + var raw *ssa.Function + var closure *ssa.MakeClosure + switch value := common.Value.(type) { + case *ssa.Function: + raw = value + case *ssa.MakeClosure: + closure = value + var exact bool + raw, exact = closure.Fn.(*ssa.Function) + if !exact || raw == nil || len(raw.FreeVars) == 0 || len(closure.Bindings) != len(raw.FreeVars) { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("captured coroutine defer requires its exact MakeClosure environment") + } + default: + if err := validateCoroManagedDispatchDefer(whole, instruction.Parent(), instruction, callPlan, universes...); err != nil { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("dynamic function defer: %w", err) + } + return nil, coro.FunctionPlan{}, coroStaticCleanupDispatch, nil + } + if raw == nil || common.IsInvoke() || common.StaticCallee() != raw { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("requires an exact static function or captured MakeClosure, not a dynamically selected function, method value, or invoke") + } + if callPlan.Kind != coro.CallDefer || callPlan.Open || callPlan.MayBeNil || len(callPlan.Targets) != 1 { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf( + "requires one closed non-nil defer target, got kind=%v representation=%s open=%t may-be-nil=%t targets=%d", + callPlan.Kind, callPlan.Rep, callPlan.Open, callPlan.MayBeNil, len(callPlan.Targets), + ) + } + target, ok := whole.Function(callPlan.Targets[0]) + if !ok || target == nil || target != raw { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("defer target %q is not its exact canonical static function", callPlan.Targets[0]) + } + targetPlan, ok := whole.FunctionPlan(target) + if !ok || targetPlan.ID != callPlan.Targets[0] { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("defer target %q has no canonical function plan", callPlan.Targets[0]) + } + if target.Signature == nil || target.Signature.Variadic() { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("variadic or signature-less defer target is unsupported") + } + if closure != nil { + valuePlan, exact := whole.ValuePlan(closure) + if !exact || len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 || + valuePlan.Funcs[0].Rep != coro.DirectCoro || valuePlan.Funcs[0].MayBeNil || + len(valuePlan.Funcs[0].Targets) != 1 || valuePlan.Funcs[0].Targets[0] != targetPlan.ID { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("captured coroutine defer has no exact direct coroutine closure plan") + } + if callPlan.Rep != coro.DirectCoro { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("captured MakeClosure defer requires direct coroutine representation, got %s", callPlan.Rep) + } + } + if target.Signature.Recv() != nil { + if err := validateCoroStaticMethodCallOperands(instruction, target, nil); err != nil { + return nil, coro.FunctionPlan{}, 0, err + } + } else if err := validateCoroStaticCleanupOperands(common, target); err != nil { + return nil, coro.FunctionPlan{}, 0, err + } + if coroPhysicalSignatureContainsFunctionValue(coroPhysicalNormalizeSourceSignature(target.Signature)) { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("function-valued defer arguments require dynamic cleanup records") + } + if targetPlan.Exec.Contains(coro.NeedsCleanupFrame) { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("defer target %q registers nested cleanup", targetPlan.ID) + } + + switch callPlan.Rep { + case coro.DirectPlain: + if targetPlan.External != coro.Defined || targetPlan.Emission != coro.EmitPlain || + targetPlan.Primary != coro.PrimaryPlain || targetPlan.FuncRep != coro.DirectPlain || + targetPlan.Demand == coro.NoDemand || targetPlan.Effect != coro.NoSuspend { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf( + "plain defer target %q is not one demanded defined bounded plain entry (external=%s emission=%s primary=%s representation=%s effect=%s demand=%s)", + targetPlan.ID, targetPlan.External, targetPlan.Emission, targetPlan.Primary, + targetPlan.FuncRep, targetPlan.Effect, targetPlan.Demand, + ) + } + return target, targetPlan, coroStaticCleanupPlain, nil + case coro.DirectCoro: + if err := validateCoroAwaitTarget(caller, targetPlan); err != nil { + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("coroutine defer target: %w", err) + } + return target, targetPlan, coroStaticCleanupCoroutine, nil + default: + return nil, coro.FunctionPlan{}, 0, fmt.Errorf("defer target uses unsupported representation %s", callPlan.Rep) + } +} + +func validateCoroStaticCleanupOperands(common *ssa.CallCommon, target *ssa.Function) error { + if common == nil || target == nil || target.Signature == nil || target.Signature.Recv() != nil { + return fmt.Errorf("static cleanup operands require one receiver-free function") + } + signature := coroPhysicalNormalizeSourceSignature(target.Signature) + if signature.Params().Len() != len(target.Params) || len(common.Args) != len(target.Params) { + return fmt.Errorf( + "static cleanup argument shape mismatch: signature=%d SSA-params=%d call-args=%d", + signature.Params().Len(), len(target.Params), len(common.Args), + ) + } + for index, parameter := range target.Params { + if parameter == nil || common.Args[index] == nil || + !types.Identical(parameter.Type(), signature.Params().At(index).Type()) || + !types.Identical(common.Args[index].Type(), parameter.Type()) { + return fmt.Errorf("static cleanup operand %d does not match the target parameter ABI", index) + } + if err := validateCoroPhysicalValueType(parameter.Type(), make(map[types.Type]bool)); err != nil { + return fmt.Errorf("static cleanup operand %d has unsupported type: %w", index, err) + } + } + return nil +} + +// validateCoroStaticCleanupNoUnwind overrides the planner's deliberately +// conservative MayUnwind bit only with an exact lowering audit. It accepts +// pure SSA plus compiler-elided no-call/structured-park intrinsics. Managed +// callees, implicit panic helpers, nested defer, recover, and preemption remain +// closed until child-frame panic outcomes can be propagated to the drainer. +func validateCoroStaticCleanupNoUnwind( + whole *coro.SSAPlan, + universe *EmissionUniverse, + target *ssa.Function, + plan coro.FunctionPlan, + frameRetentionABI string, +) string { + if target == nil || len(target.Blocks) == 0 { + return "target has no defined SSA body" + } + if target.Recover != nil { + return "recover block requires panic-aware cleanup unwinding" + } + if plan.Exec&(coro.NeedsCleanupFrame|coro.NeedsPreempt|coro.OpaqueExec) != 0 { + return "target requires nested cleanup, preemption, or opaque execution" + } + for _, info := range blocks.Infos(target.Blocks) { + if info.InLoop { + return "cyclic cleanup target requires preemption and cancellation masking" + } + } + audit, err := newCoroPhysicalPureSSAAudit(universe, whole, target, frameRetentionABI) + if err != nil { + return "cannot build pure-SSA audit: " + err.Error() + } + for _, block := range target.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled { + if reason != "" { + return fmt.Sprintf("block %d instruction %T: %s", block.Index, instruction, reason) + } + continue + } + switch instruction := instruction.(type) { + case *ssa.DebugRef, *ssa.Jump, *ssa.Return: + case *ssa.If: + if !coroLeafScalar(instruction.Cond.Type()) { + return fmt.Sprintf("block %d has a non-scalar condition", block.Index) + } + case *ssa.Call: + if whole == nil || !whole.ElidesCall(instruction) || universe == nil { + return fmt.Sprintf("block %d has an ordinary managed call", block.Index) + } + semantics, intrinsic, err := coroIntrinsicCallSiteSemantics(universe, instruction) + if err != nil { + return fmt.Sprintf("block %d intrinsic: %v", block.Index, err) + } + if !intrinsic || (semantics != CoroIntrinsicCallInlineNoSuspend && semantics != CoroIntrinsicCallInlineSuspend && + semantics != CoroIntrinsicCallInlineYield) { + return fmt.Sprintf("block %d intrinsic has unproved semantics %d", block.Index, uint8(semantics)) + } + default: + return fmt.Sprintf("block %d instruction %T has no no-unwind lowering proof", block.Index, instruction) + } + } + } + return "" +} + +const ( + coroStaticCleanupContinueComplete uint32 = 1 + coroStaticCleanupContinueRecover uint32 = 2 + coroStaticCleanupContinueFirstRun uint32 = 3 +) + +type coroStaticCleanupSiteState struct { + plan *coroStaticCleanupSitePlan + active llssa.Expr + descriptor llssa.Expr + descriptorType llssa.Type + closureContext llssa.Expr + args []llssa.Expr + nodeType llssa.Type + descriptorField int + closureField int + argsField int +} + +type coroStaticCleanupContinuation struct { + id uint32 + block llssa.BasicBlock +} + +type coroStaticCleanupState struct { + sites []*coroStaticCleanupSiteState + byDefer map[*ssa.Defer]*coroStaticCleanupSiteState + dynamic bool + dynamicHead llssa.Expr + dynamicHeader llssa.Type + dynamicAlloc *ssa.Function + dynamicFree *ssa.Function + continuation llssa.Expr + panicActive llssa.Expr + panicType llssa.Expr + panicData llssa.Expr + entry llssa.BasicBlock + complete llssa.BasicBlock + panic llssa.BasicBlock + run []coroStaticCleanupContinuation +} + +// beginCoroStaticCleanup allocates and initializes every value before the +// initial suspend. A cancellation decision on the first resume can therefore +// safely enter the same empty drainer as every later terminal path. +func (p *context) beginCoroStaticCleanup(b llssa.Builder, plan *coroStaticCleanupPlan) *coroStaticCleanupState { + if plan == nil || len(plan.sites) == 0 { + return nil + } + state := &coroStaticCleanupState{ + sites: make([]*coroStaticCleanupSiteState, 0, len(plan.sites)), + byDefer: make(map[*ssa.Defer]*coroStaticCleanupSiteState, len(plan.sites)), + dynamic: plan.dynamic, + dynamicAlloc: plan.dynamicAlloc, + dynamicFree: plan.dynamicFree, + } + state.continuation = b.AllocaT(p.prog.Uint32()) + b.Store(state.continuation, p.prog.IntVal(0, p.prog.Uint32())) + state.panicActive = b.AllocaT(p.prog.Bool()) + b.Store(state.panicActive, p.prog.BoolVal(false)) + state.panicType = b.AllocaT(p.prog.VoidPtr()) + state.panicData = b.AllocaT(p.prog.VoidPtr()) + b.Store(state.panicType, p.prog.Nil(p.prog.VoidPtr())) + b.Store(state.panicData, p.prog.Nil(p.prog.VoidPtr())) + if state.dynamic { + if state.dynamicAlloc == nil || state.dynamicFree == nil { + panic("dynamic coroutine cleanup lacks frozen allocator/release targets") + } + state.dynamicHead = b.AllocaT(p.prog.VoidPtr()) + b.Store(state.dynamicHead, p.prog.Nil(p.prog.VoidPtr())) + state.dynamicHeader = p.prog.Struct(p.prog.VoidPtr(), p.prog.Uint32()) + } + for _, sitePlan := range plan.sites { + site := &coroStaticCleanupSiteState{ + plan: sitePlan, + descriptorField: -1, + closureField: -1, + } + if !state.dynamic { + site.active = b.AllocaT(p.prog.Bool()) + b.Store(site.active, p.prog.BoolVal(false)) + } + var nodeFields []llssa.Type + if state.dynamic { + nodeFields = append(nodeFields, p.prog.VoidPtr(), p.prog.Uint32()) + } + if sitePlan.kind == coroStaticCleanupDispatch { + if sitePlan.descriptor == nil || sitePlan.signature == nil { + panic("managed descriptor cleanup site has no frozen descriptor/signature") + } + descriptorType := p.type_(sitePlan.descriptor.Type(), llssa.InGo) + closure, ok := types.Unalias(descriptorType.RawType()).Underlying().(*types.Struct) + if !ok || !llssa.IsClosure(closure) { + panic(fmt.Sprintf("managed descriptor cleanup site lowered %s as %s, want canonical closure", sitePlan.descriptor.Type(), descriptorType.RawType())) + } + site.descriptor = b.AllocaT(descriptorType) + site.descriptorType = descriptorType + b.Store(site.descriptor, p.prog.Zero(descriptorType)) + if state.dynamic { + site.descriptorField = len(nodeFields) + nodeFields = append(nodeFields, descriptorType) + } + } + if sitePlan.closure != nil { + if sitePlan.kind != coroStaticCleanupCoroutine || p.emissionUniverse == nil { + panic("captured static cleanup requires a prepared coroutine target") + } + signature, err := p.emissionUniverse.coroPhysicalEntrySourceSignature(sitePlan.target) + if err != nil || signature == nil || signature.Params().Len() == 0 { + panic(fmt.Sprintf("captured static cleanup target %q has no exact context ABI: %v", sitePlan.targetPlan.ID, err)) + } + contextType := p.prog.Type(signature.Params().At(0).Type(), llssa.InGo) + site.closureContext = b.AllocaT(contextType) + b.Store(site.closureContext, p.prog.Nil(contextType)) + if state.dynamic { + site.closureField = len(nodeFields) + nodeFields = append(nodeFields, contextType) + } + } + site.argsField = len(nodeFields) + for _, argument := range sitePlan.instruction.Call.Args { + argumentType := p.type_(argument.Type(), llssa.InGo) + site.args = append(site.args, b.AllocaT(argumentType)) + if state.dynamic { + nodeFields = append(nodeFields, argumentType) + } + } + if state.dynamic { + site.nodeType = p.prog.Struct(nodeFields...) + } + state.sites = append(state.sites, site) + state.byDefer[sitePlan.instruction] = site + } + return state +} + +// bindBlocks runs only after BeginCoro has created its canonical ramp and +// initial-suspend blocks; cleanup implementation blocks must not perturb that +// presplit layout contract. +func (s *coroStaticCleanupState) bindBlocks(function llssa.Function) { + if s == nil { + return + } + s.entry = function.MakeBlock() + s.complete = function.MakeBlock() + s.panic = function.MakeBlock() +} + +func (s *coroStaticCleanupState) register(p *context, b llssa.Builder, instruction *ssa.Defer) { + if s == nil || instruction == nil { + panic("coroutine static cleanup registration has no state or instruction") + } + site := s.byDefer[instruction] + if site == nil { + panic("coroutine defer escaped its static cleanup plan") + } + // SSA values preserve Go's left-to-right evaluation. Save the already + // evaluated exact closure environment, then every receiver/argument, before + // making the record active. The context slot is a typed frame root and stays + // live until the deferred child has completed. + descriptor := llssa.Nil + if site.plan.kind == coroStaticCleanupDispatch { + if site.descriptor.IsNil() || site.plan.descriptor == nil { + panic("managed descriptor cleanup registration has no typed descriptor slot") + } + descriptor = p.compileValue(b, site.plan.descriptor) + closure, ok := types.Unalias(descriptor.RawType()).Underlying().(*types.Struct) + if !ok || !llssa.IsClosure(closure) || site.descriptorType == nil || + !types.Identical(descriptor.RawType(), site.descriptorType.RawType()) { + want := "" + if site.descriptorType != nil { + want = site.descriptorType.RawType().String() + } + panic(fmt.Sprintf("managed descriptor cleanup registration lowered callee as %s, want %s", descriptor.RawType(), want)) + } + if !s.dynamic { + b.Store(site.descriptor, descriptor) + } + } + closureContext := llssa.Nil + if site.plan.closure != nil { + if site.closureContext.IsNil() { + panic("captured coroutine defer has no closure-context slot") + } + closure := p.compileValue(b, site.plan.closure) + closureContext = b.Field(closure, 1) + if !s.dynamic { + b.Store(site.closureContext, closureContext) + } + } + functionKind := p.funcKind(instruction.Call.Value) + if site.plan.kind == coroStaticCleanupDispatch { + functionKind = fnNormal + } + args := p.compileValues(b, instruction.Call.Args, functionKind) + if len(args) != len(site.args) { + panic(fmt.Sprintf("coroutine defer arguments=%d do not match cleanup slots=%d", len(args), len(site.args))) + } + if s.dynamic { + s.pushDynamic(p, b, site, descriptor, closureContext, args) + return + } + for index, argument := range args { + b.Store(site.args[index], argument) + } + b.Store(site.active, b.Prog.BoolVal(true)) +} + +// pushDynamic publishes a fully initialized heterogeneous record with one +// release-store-equivalent compiler order: Go closure/arguments are evaluated +// first, the private node is filled next, and the frame-rooted head changes +// last. No scheduler suspension is permitted in the frozen AllocU edge. +func (s *coroStaticCleanupState) pushDynamic( + p *context, b llssa.Builder, site *coroStaticCleanupSiteState, + descriptor, closureContext llssa.Expr, args []llssa.Expr, +) { + if s == nil || p == nil || site == nil || site.nodeType == nil || s.dynamicHead.IsNil() || + s.dynamicAlloc == nil || site.plan == nil || site.plan.tag == 0 { + panic("dynamic coroutine cleanup push has incomplete frozen state") + } + allocator, _, kind := p.compileFunction(s.dynamicAlloc) + if allocator == nil || kind != goFunc { + panic("dynamic coroutine cleanup AllocU target did not resolve to a Go entry") + } + p.observeCoroSiteRuntimeHelper("AllocU") + raw := b.Call(allocator.Expr, llssa.SizeOf(p.prog, site.nodeType)) + node := b.Convert(p.prog.Pointer(site.nodeType), raw) + b.Store(b.FieldAddr(node, 0), b.Load(s.dynamicHead)) + b.Store(b.FieldAddr(node, 1), p.prog.IntVal(uint64(site.plan.tag), p.prog.Uint32())) + if site.descriptorField >= 0 { + if descriptor.IsNil() { + panic("dynamic managed cleanup push lost its descriptor") + } + b.Store(b.FieldAddr(node, site.descriptorField), descriptor) + } + if site.closureField >= 0 { + if closureContext.IsNil() { + panic("dynamic captured cleanup push lost its closure context") + } + b.Store(b.FieldAddr(node, site.closureField), closureContext) + } + for index, argument := range args { + b.Store(b.FieldAddr(node, site.argsField+index), argument) + } + b.Store(s.dynamicHead, b.Convert(p.prog.VoidPtr(), node)) +} + +func (s *coroStaticCleanupState) enter(b llssa.Builder, continuation uint32) { + if s == nil || s.entry == nil { + panic("coroutine static cleanup entry is not bound") + } + b.Store(s.continuation, b.Prog.IntVal(uint64(continuation), b.Prog.Uint32())) + b.Store(s.panicActive, b.Prog.BoolVal(false)) + b.Store(s.panicType, b.Prog.Nil(b.Prog.VoidPtr())) + b.Store(s.panicData, b.Prog.Nil(b.Prog.VoidPtr())) + b.Jump(s.entry) +} + +func (s *coroStaticCleanupState) enterCompletion(b llssa.Builder) { + s.enter(b, coroStaticCleanupContinueComplete) +} + +// enterCancellation replaces the cleanup base with terminal cancellation but +// deliberately preserves a live panic overlay. An older defer may still +// recover that panic; without recovery the panic wins, while recovery exposes +// the retained Abort/Shutdown base and resumes cancellation propagation. +func (s *coroStaticCleanupState) enterCancellation(b llssa.Builder) { + s.setCancellationBase(b) + s.resume(b) +} + +// setCancellationBase changes only the continuation selected after the last +// cleanup record. It intentionally does not clear or jump: a canceled child +// resume must first consume and reconcile that child's already-published +// Return/Recovered/Panic outcome before re-entering the drainer. +func (s *coroStaticCleanupState) setCancellationBase(b llssa.Builder) { + if s == nil || s.entry == nil { + panic("coroutine cancellation cleanup entry is not bound") + } + b.Store(s.continuation, b.Prog.IntVal(uint64(coroStaticCleanupContinueComplete), b.Prog.Uint32())) + +} + +func (s *coroStaticCleanupState) resume(b llssa.Builder) { + if s == nil || s.entry == nil { + panic("coroutine cleanup resume entry is not bound") + } + b.Jump(s.entry) +} + +func (s *coroStaticCleanupState) enterPanic(b llssa.Builder, typeWord, dataWord llssa.Expr) { + // A panic reached from source execution has no earlier cleanup base. A + // successful recover returns through x/tools' canonical Recover block. + b.Store(s.continuation, b.Prog.IntVal(uint64(coroStaticCleanupContinueRecover), b.Prog.Uint32())) + s.replacePanic(b, typeWord, dataWord) +} + +// replacePanic is used only when a deferred child itself panics. Preserve the +// cleanup base (normal return, Recover, RunDefers, and future cancel/Goexit) +// while replacing the active panic overlay with the child's newer payload. +func (s *coroStaticCleanupState) replacePanic(b llssa.Builder, typeWord, dataWord llssa.Expr) { + s.setPanicOverlay(b, typeWord, dataWord) + s.resume(b) +} + +func (s *coroStaticCleanupState) setPanicOverlay(b llssa.Builder, typeWord, dataWord llssa.Expr) { + if s == nil { + panic("coroutine cleanup panic overlay has no state") + } + b.Store(s.panicActive, b.Prog.BoolVal(true)) + b.Store(s.panicType, b.Convert(b.Prog.VoidPtr(), typeWord)) + b.Store(s.panicData, b.Convert(b.Prog.VoidPtr(), dataWord)) +} + +// recoverAwaitArguments encodes the current panic overlay into the unified V3 +// child handoff. Selects avoid a second runtime hook and keep normal cleanup on +// the same CompletionRecord transaction with nil recovery words. +func (s *coroStaticCleanupState) recoverAwaitArguments( + p *context, b llssa.Builder, +) (mode, typeWord, dataWord llssa.Expr) { + if s == nil || p == nil || p.coroBody() == nil { + panic("coroutine cleanup recovery arguments require an active drainer") + } + active := b.Load(s.panicActive) + mode = b.SelectValue( + active, + b.Prog.IntVal(coroAwaitRecoverDirect, b.Prog.Uint32()), + b.Prog.IntVal(coroAwaitRecoverNone, b.Prog.Uint32()), + ) + typeWord = b.SelectValue(active, b.Load(s.panicType), b.Prog.Nil(b.Prog.VoidPtr())) + dataWord = b.SelectValue(active, b.Load(s.panicData), b.Prog.Nil(b.Prog.VoidPtr())) + return +} + +func (s *coroStaticCleanupState) reconcileDeferredChildReturn( + p *context, b llssa.Builder, status uint64, +) { + if s == nil || p == nil || status != coroAwaitCompletionReturnRecovered { + panic("coroutine cleanup child return has an invalid completion status") + } + valid := p.fn.MakeBlock() + invalid := p.fn.MakeBlock() + active := b.Load(s.panicActive) + b.If(active, valid, invalid) + b.SetBlockEx(valid, llssa.AtEnd, false) + // Preserve the base continuation. This is what makes a panic raised during + // normal RunDefers/cancellation cleanup resume its original control after an + // older defer recovers it. + b.Store(s.panicActive, b.Prog.BoolVal(false)) + b.Store(s.panicType, b.Prog.Nil(b.Prog.VoidPtr())) + b.Store(s.panicData, b.Prog.Nil(b.Prog.VoidPtr())) + merged := p.fn.MakeBlock() + b.Jump(merged) + b.SetBlockEx(invalid, llssa.AtEnd, false) + b.Unreachable() + b.SetBlockEx(merged, llssa.AtEnd, false) +} + +func (s *coroStaticCleanupState) runDefers(b llssa.Builder, _ *ssa.RunDefers) { + if s == nil { + panic("coroutine RunDefers has no static cleanup state") + } + if uint64(len(s.run)) > uint64(^uint32(0)-coroStaticCleanupContinueFirstRun) { + panic("too many coroutine RunDefers continuations") + } + continuation := coroStaticCleanupContinuation{ + id: coroStaticCleanupContinueFirstRun + uint32(len(s.run)), + block: b.Func.MakeBlock(), + } + s.run = append(s.run, continuation) + s.enter(b, continuation.id) + b.SetBlockContinuation(continuation.block) +} + +func (s *coroStaticCleanupState) emit(p *context, b llssa.Builder) { + if s == nil || s.entry == nil || s.complete == nil || s.panic == nil { + panic("coroutine static cleanup blocks are not bound") + } + if s.dynamic { + s.emitDynamic(p, b) + return + } + done := p.fn.MakeBlock() + next := done + // Construct from oldest to newest while wiring each skipped/executed site + // to the already-built older suffix. entry finally points at the newest. + for index := 0; index < len(s.sites); index++ { + site := s.sites[index] + check := p.fn.MakeBlock() + call := p.fn.MakeBlock() + b.SetBlock(check) + b.If(b.Load(site.active), call, next) + b.SetBlock(call) + // Clear before invoking. A panic, cancellation resume, or erroneous + // second RunDefers can never execute this exact record twice. + b.Store(site.active, b.Prog.BoolVal(false)) + args := make([]llssa.Expr, len(site.args)) + for argument := range args { + args[argument] = b.Load(site.args[argument]) + } + s.emitSiteCall(p, b, site, args) + b.Jump(next) + next = check + } + b.SetBlock(s.entry) + b.Jump(next) + + b.SetBlock(done) + s.emitCompletionDispatch(p, b) +} + +// emitDynamic drains the one owner-local heterogeneous LIFO chain. Each node +// is copied into its site's typed frame slots before unlink/free, so a deferred +// coroutine may suspend without retaining an untyped or released allocation. +// Popping before invocation is the dynamic equivalent of clearing a static +// site's active bit: panic, Abort, and Shutdown can re-enter this same loop +// without executing a record twice. +func (s *coroStaticCleanupState) emitDynamic(p *context, b llssa.Builder) { + if s.dynamicHead.IsNil() || s.dynamicHeader == nil || s.dynamicFree == nil { + panic("dynamic coroutine cleanup drainer has incomplete frozen state") + } + done := p.fn.MakeBlock() + nonempty := p.fn.MakeBlock() + invalid := p.fn.MakeBlock() + siteBlocks := make([]llssa.BasicBlock, len(s.sites)) + for index := range siteBlocks { + siteBlocks[index] = p.fn.MakeBlock() + } + + b.SetBlock(s.entry) + record := b.Load(s.dynamicHead) + b.If(b.BinOp(token.NEQ, record, p.prog.Nil(p.prog.VoidPtr())), nonempty, done) + + b.SetBlock(nonempty) + header := b.Convert(p.prog.Pointer(s.dynamicHeader), record) + next := b.Load(b.FieldAddr(header, 0)) + tag := b.Load(b.FieldAddr(header, 1)) + // Unlink before any site-specific work. The record remains valid until its + // typed payload has been copied and the frozen release helper runs below. + b.Store(s.dynamicHead, next) + dispatch := b.Switch(tag, invalid) + for index, site := range s.sites { + if site == nil || site.plan == nil || site.plan.tag == 0 { + panic("dynamic coroutine cleanup site has no stable tag") + } + dispatch.Case(p.prog.IntVal(uint64(site.plan.tag), p.prog.Uint32()), siteBlocks[index]) + } + dispatch.End(b) + + for index, site := range s.sites { + b.SetBlock(siteBlocks[index]) + node := b.Convert(p.prog.Pointer(site.nodeType), record) + if site.descriptorField >= 0 { + b.Store(site.descriptor, b.Load(b.FieldAddr(node, site.descriptorField))) + } + if site.closureField >= 0 { + b.Store(site.closureContext, b.Load(b.FieldAddr(node, site.closureField))) + } + for argument := range site.args { + b.Store(site.args[argument], b.Load(b.FieldAddr(node, site.argsField+argument))) + } + s.releaseDynamicRecord(p, b, site, record) + args := make([]llssa.Expr, len(site.args)) + for argument := range args { + args[argument] = b.Load(site.args[argument]) + } + s.emitSiteCall(p, b, site, args) + b.Jump(s.entry) + } + + b.SetBlock(invalid) + b.Unreachable() + b.SetBlock(done) + s.emitCompletionDispatch(p, b) +} + +func (s *coroStaticCleanupState) releaseDynamicRecord(p *context, b llssa.Builder, site *coroStaticCleanupSiteState, record llssa.Expr) { + if site == nil || site.plan == nil || site.plan.instruction == nil { + panic("dynamic coroutine cleanup release has no exact source SitePlan") + } + finishSite := p.beginCoroRelocatedSiteEmission(site.plan.instruction, coroRuntimeHelperAtCleanup) + defer finishSite() + releaser, _, kind := p.compileFunction(s.dynamicFree) + if releaser == nil || kind != goFunc { + panic("dynamic coroutine cleanup FreeDeferNode target did not resolve to a Go entry") + } + p.observeCoroSiteRuntimeHelper("FreeDeferNode") + b.Call(releaser.Expr, record) +} + +func (s *coroStaticCleanupState) emitSiteCall( + p *context, b llssa.Builder, site *coroStaticCleanupSiteState, args []llssa.Expr, +) { + switch site.plan.kind { + case coroStaticCleanupPlain: + function, _, kind := p.compileFunction(site.plan.target) + if function == nil || kind != goFunc { + panic(fmt.Sprintf("coroutine plain cleanup target %q did not resolve to a Go entry", site.plan.targetPlan.ID)) + } + b.Call(function.Expr, args...) + case coroStaticCleanupCoroutine: + closureContext := llssa.Nil + if site.plan.closure != nil { + if site.closureContext.IsNil() { + panic("captured coroutine cleanup lost its closure-context slot") + } + closureContext = b.Load(site.closureContext) + } + p.compileCoroTargetAwaitWithContextAndRecovery(b, site.plan.target, closureContext, args, s, nil) + case coroStaticCleanupDispatch: + if site.descriptor.IsNil() || site.plan.signature == nil { + panic("managed descriptor cleanup lost its typed descriptor/signature") + } + p.compileCoroManagedDispatchAwaitValueWithRecovery( + b, b.Load(site.descriptor), args, site.plan.signature, s, nil, + ) + default: + panic("coroutine cleanup target has an invalid kind") + } +} + +func (s *coroStaticCleanupState) emitCompletionDispatch(p *context, b llssa.Builder) { + invalid := p.fn.MakeBlock() + baseDispatch := p.fn.MakeBlock() + // The panic overlay wins until one exact deferred child reports + // CompletionReturnRecovered. Only then may the original base continuation + // (normal return, recover-return reconstruction, RunDefers, or future + // cancellation/Goexit) resume. + b.If(b.Load(s.panicActive), s.panic, baseDispatch) + b.SetBlock(baseDispatch) + dispatch := b.Switch(b.Load(s.continuation), invalid) + dispatch.Case(b.Prog.IntVal(uint64(coroStaticCleanupContinueComplete), b.Prog.Uint32()), s.complete) + if p.goFn == nil || p.goFn.Recover == nil { + panic("coroutine cleanup recover continuation has no canonical SSA recover block") + } + dispatch.Case( + b.Prog.IntVal(uint64(coroStaticCleanupContinueRecover), b.Prog.Uint32()), + p.sourceBlock(p.goFn.Recover.Index), + ) + for _, continuation := range s.run { + dispatch.Case(b.Prog.IntVal(uint64(continuation.id), b.Prog.Uint32()), continuation.block) + } + dispatch.End(b) + + b.SetBlock(invalid) + // The continuation is written only by compiler-owned constant stores. An + // unknown value is therefore unreachable IR, not a user-triggerable runtime + // outcome that needs a second scheduler/error hook. + b.Unreachable() +} diff --git a/cl/coro_defer_test.go b/cl/coro_defer_test.go new file mode 100644 index 0000000000..2a756d1ffe --- /dev/null +++ b/cl/coro_defer_test.go @@ -0,0 +1,1111 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroStaticCleanupIRFixture = `package foo +var Sink uint32 +var PanicPayload uint32 + +type Guard struct{} + +func First(value uint32) { Sink = Sink*10 + value } +func Second(value uint32) { Sink = Sink*10 + value } +func (*Guard) Third(value uint32) { Sink = Sink*10 + value } + +func Root(guard *Guard, mode uint32) { + defer First(1) + defer Second(mode + 2) + defer guard.Third(mode + 3) + if mode == 9 { panic(&PanicPayload) } +} +` + +const coroCapturedStaticCleanupIRFixture = `package foo +var Sink uint32 + +func Root(value uint32) { + defer func(add uint32) { Sink = value + add }(7) +} +` + +const coroDynamicCleanupIRFixture = `package foo +var Sink uint32 + +func Cleanup(value uint32) { Sink = Sink*10 + value } +var CleanupFunc func(uint32) = Cleanup + +func Root(limit uint32) { + defer Cleanup(99) + for value := uint32(0); value < limit; value++ { + defer CleanupFunc(value) + } +} +` + +func TestCoroStaticCleanupSharedReturnShape(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func Unlock() {} +func Root(locked, ok bool) (swapped bool) { + if locked { defer Unlock() } + if !ok { return false } + return true +} +`) + root := ssaPkg.Func("Root") + found := 0 + for _, block := range root.Blocks { + for index, instruction := range block.Instrs { + if _, ok := instruction.(*ssa.RunDefers); !ok { + continue + } + found++ + if !coroStaticRunDefersReturns(block, index) { + t.Fatalf("RunDefers block=%d does not accept the exact named-result reload tail: instructions=%v successors=%v", block.Index, block.Instrs, block.Succs) + } + } + } + if found != 2 { + t.Fatalf("shared-return fixture RunDefers count = %d, want 2", found) + } +} + +func TestCoroStaticCleanupIRNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root := compileCoroStaticCleanupIRFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || + !rootPlan.Exec.Contains(coro.NeedsCleanupFrame) || !rootPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Root cleanup plan = %+v, present=%t", rootPlan, ok) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify static cleanup before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Root").String() + for _, symbol := range []string{"foo.First$coro", "foo.Second$coro", "Third$coro"} { + if got := strings.Count(body, symbol); got != 1 { + t.Fatalf("Root cleanup references %s = %d, want one shared guarded call site:\n%s", symbol, got, body) + } + } + for _, forbidden := range []string{"Sigsetjmp", "SetThreadDefer", "GetThreadDefer", "runtime.RunDefers"} { + if strings.Contains(body, forbidden) { + t.Fatalf("stackless cleanup retained legacy defer machinery %q:\n%s", forbidden, body) + } + } + if !strings.Contains(body, "switch i32") || strings.Count(body, "alloca i1") < 3 || + strings.Count(body, "store i1 false") < 3 || strings.Count(body, "store i1 true") < 3 { + t.Fatalf("static cleanup frame/continuation state is incomplete:\n%s", body) + } + if strings.Count(body, "call void @"+coroPanicPrepareHookV1) != 1 || + strings.Count(body, "call void @"+coroCompletePrepareHookV2) != 1 { + t.Fatalf("panic and completion do not share the cleanup drainer:\n%s", body) + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() { + t.Fatalf("CoroSplit did not create Root cleanup resume entry:\n%s", module.String()) + } + post := resume.String() + for _, symbol := range []string{"foo.First$coro", "foo.Second$coro", "Third$coro"} { + if got := strings.Count(post, symbol); got != 1 { + t.Fatalf("post-split cleanup references %s = %d, want one:\n%s", symbol, got, post) + } + } + }) + } +} + +func TestCoroCapturedStaticCleanupIRNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root, closure, target := compileCoroCapturedStaticCleanupFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, rootOK := plan.FunctionPlan(root) + targetPlan, targetOK := plan.FunctionPlan(target) + if !rootOK || rootPlan.Emission != coro.EmitCoroutine || + !rootPlan.Exec.Contains(coro.NeedsCleanupFrame) || !rootPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("captured cleanup root plan = %+v, present=%t", rootPlan, rootOK) + } + if !targetOK || targetPlan.Emission != coro.EmitCoroutine || targetPlan.FuncRep != coro.DirectCoro { + t.Fatalf("captured cleanup target plan = %+v, present=%t", targetPlan, targetOK) + } + cleanup, err := prepareCoroStaticCleanupPlan(root, plan, nil, "", true) + if err != nil { + t.Fatal(err) + } + if cleanup == nil || len(cleanup.sites) != 1 || cleanup.sites[0].closure != closure || + cleanup.sites[0].target != target || cleanup.sites[0].kind != coroStaticCleanupCoroutine { + t.Fatalf("captured static cleanup plan = %+v", cleanup) + } + + assertCoroCapturedCleanupCall(t, requireCoroPhysicalFunction(t, module, "foo.Root"), target.String(), true) + physicalTarget := requireCoroPhysicalFunction(t, module, target.String()) + if got := physicalTarget.ParamsCount(); got != 4 { + t.Fatalf("captured cleanup physical parameters = %d, want (g,out,ctx,add)", got) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify captured cleanup before CoroSplit: %v\n%s", err, module.String()) + } + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() { + t.Fatalf("CoroSplit did not create captured cleanup resume entry:\n%s", module.String()) + } + assertCoroCapturedCleanupCall(t, resume, target.String(), false) + }) + } +} + +func TestCoroDynamicCleanupLIFOIRNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root := compileCoroDynamicCleanupFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || + !rootPlan.Exec.Contains(coro.NeedsCleanupFrame) || !rootPlan.Exec.Contains(coro.NeedsPreempt) || + !rootPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Root dynamic cleanup plan = %+v, present=%t", rootPlan, ok) + } + cleanup, err := prepareCoroStaticCleanupPlan(root, plan, nil, "", true) + if err != nil { + t.Fatal(err) + } + if cleanup == nil || !cleanup.dynamic || cleanup.dynamicTrigger == nil || len(cleanup.sites) != 2 || + cleanup.dynamicAlloc == nil || cleanup.dynamicFree == nil { + t.Fatalf("dynamic cleanup data model = %+v", cleanup) + } + for index, site := range cleanup.sites { + if site == nil || site.tag != uint32(index+1) { + t.Fatalf("dynamic cleanup site %d = %+v, want stable tag %d", index, site, index+1) + } + } + if cleanup.sites[1].kind != coroStaticCleanupDispatch || cleanup.sites[1].descriptor == nil || + cleanup.sites[1].callPlan.Rep != coro.Dispatch || cleanup.sites[1].callPlan.Transport != coro.ManagedTransport { + t.Fatalf("loop cleanup site is not one frozen managed descriptor record: %+v", cleanup.sites[1]) + } + if err := validateCoroDynamicCleanupHelpers(cleanup, plan); err != nil { + t.Fatalf("dynamic cleanup helper certificate: %v", err) + } + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify dynamic cleanup before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Root").String() + for _, required := range []string{ + "AllocU", "FreeDeferNode", "switch i32", "foo.Cleanup$coro", + "llvm.coro.promise", coroAwaitPrepareHookV1, coroFaultPayloadHookV1, + } { + if !strings.Contains(body, required) { + t.Fatalf("dynamic cleanup body lacks %q:\n%s", required, body) + } + } + if !strings.Contains(module.String(), coroPlainDispatchDescriptorPrefix) { + t.Fatalf("dynamic cleanup module lacks the descriptor producer:\n%s", module.String()) + } + for _, forbidden := range []string{"Sigsetjmp", "SetThreadDefer", "GetThreadDefer", "runtime.RunDefers"} { + if strings.Contains(body, forbidden) { + t.Fatalf("dynamic stackless cleanup retained legacy defer machinery %q:\n%s", forbidden, body) + } + } + if got := strings.Count(body, "AllocU"); got != 2 { + t.Fatalf("dynamic cleanup AllocU sites = %d, want one per static defer site:\n%s", got, body) + } + if got := strings.Count(body, "FreeDeferNode"); got != 2 { + t.Fatalf("dynamic cleanup FreeDeferNode sites = %d, want one per dispatch site:\n%s", got, body) + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), "FreeDeferNode") || + !strings.Contains(resume.String(), "foo.Cleanup$coro") { + t.Fatalf("post-split dynamic cleanup lost its pop/free/await loop:\n%s", module.String()) + } + }) + } +} + +func assertCoroCapturedCleanupCall(t *testing.T, function llvm.Value, target string, requireContextLoad bool) { + t.Helper() + var call llvm.Value + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || instruction.CalledValue().Name() != target+"$coro" { + continue + } + if !call.IsNil() { + t.Fatalf("%s invokes captured cleanup %q more than once:\n%s", function.Name(), target, function.String()) + } + call = instruction + } + } + if call.IsNil() { + t.Fatalf("%s does not invoke captured cleanup %q:\n%s", function.Name(), target, function.String()) + } + // (g, out, ctx, add) plus LLVM's called-value operand. The context is the + // exact environment loaded from the registration record, never a nil marker + // used by context-free static cleanup. + if got := call.OperandsCount() - 1; got != 4 { + t.Fatalf("captured cleanup call arguments = %d, want 4:\n%s", got, call.String()) + } + context := call.Operand(2) + if !context.IsAConstantPointerNull().IsNil() || context.IsUndef() { + t.Fatalf("captured cleanup call received an absent context:\n%s", call.String()) + } + if requireContextLoad && context.InstructionOpcode() != llvm.Load { + t.Fatalf("captured cleanup context is not loaded from its registration slot:\n%s", call.String()) + } + if got := countCoroIRDirectCalls(function, coroAwaitPrepareHookV1); got != 1 { + t.Fatalf("%s captured cleanup await_prepare calls = %d, want 1:\n%s", function.Name(), got, function.String()) + } +} + +const coroAwaitCompletionCleanupFixture = `package foo +var Sink uint32 + +func Cleanup(value uint32) { Sink = value } +func Child(value uint32) uint32 { return value + 1 } + +func Parent(value uint32) { + defer Cleanup(value) + Sink = Child(value) +} +` + +func TestCoroAwaitCompletionDrainsParentCleanupNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, parent, child := compileCoroAwaitCompletionCleanupFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + parentPlan, parentOK := plan.FunctionPlan(parent) + childPlan, childOK := plan.FunctionPlan(child) + if !parentOK || parentPlan.Emission != coro.EmitCoroutine || + !parentPlan.Exec.Contains(coro.NeedsCleanupFrame) || !parentPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Parent completion/cleanup plan = %+v, present=%t", parentPlan, parentOK) + } + if !childOK || childPlan.Emission != coro.EmitCoroutine || childPlan.FuncRep != coro.DirectCoro { + t.Fatalf("Child completion plan = %+v, present=%t", childPlan, childOK) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify parent-owned completion before CoroSplit: %v\n%s", err, module.String()) + } + parentRamp := requireCoroPhysicalFunction(t, module, "foo.Parent") + assertCoroAwaitCompletionCleanupControlFlow(t, parentRamp, true) + + runCoroABITestPipeline(t, prog, module) + parentResume := module.NamedFunction("foo.Parent$coro.resume") + if parentResume.IsNil() { + t.Fatalf("CoroSplit did not create Parent completion/cleanup resume:\n%s", module.String()) + } + assertCoroAwaitCompletionCleanupControlFlow(t, parentResume, false) + for _, name := range []string{"foo.Parent$coro", "foo.Parent$coro.destroy"} { + function := module.NamedFunction(name) + if function.IsNil() { + t.Fatalf("CoroSplit did not retain %q:\n%s", name, module.String()) + } + if functionHasReachableDirectCall(function, coroAwaitConsumeHookV1) { + t.Fatalf("parent completion is consumed outside the resume entry %q:\n%s", name, function.String()) + } + } + }) + } +} + +func assertCoroAwaitCompletionCleanupControlFlow(t *testing.T, function llvm.Value, presplit bool) { + t.Helper() + if function.IsNil() { + t.Fatal("cannot inspect nil parent completion function") + } + body := function.String() + await := strings.Index(body, "call void @"+coroAwaitPrepareHookV1) + if await < 0 { + t.Fatalf("%s has no parent-owned child await preparation:\n%s", function.Name(), body) + } + if presplit && !strings.Contains(body[await:], "call i8 @llvm.coro.suspend") { + t.Fatalf("%s consumes child completion before the await suspension/resume edge:\n%s", function.Name(), body) + } + for _, forbidden := range []string{"runtime.Panic", "runtime.RunDefers", "Sigsetjmp", "SetThreadDefer", "GetThreadDefer"} { + if strings.Contains(body, forbidden) { + t.Fatalf("%s child panic outcome retained legacy unwind %q:\n%s", function.Name(), forbidden, body) + } + } + + var normalConsume, canceledConsume, dispatch, canceledDispatch llvm.Value + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || instruction.CalledValue().Name() != coroAwaitConsumeHookV1 { + continue + } + terminator := block.LastInstruction() + if !terminator.IsNil() && terminator.InstructionOpcode() == llvm.Switch && terminator.Operand(0) == instruction && + !coroTestBlockStoresI32(block, coroStaticCleanupContinueComplete) { + if !normalConsume.IsNil() { + t.Fatalf("%s has multiple normal child completion dispatches:\n%s", function.Name(), body) + } + normalConsume, dispatch = instruction, terminator + continue + } + if !terminator.IsNil() && terminator.InstructionOpcode() == llvm.Switch && terminator.Operand(0) == instruction && + coroTestBlockStoresI32(block, coroStaticCleanupContinueComplete) { + if !canceledConsume.IsNil() { + t.Fatalf("%s has multiple canceled child reconciliation paths:\n%s", function.Name(), body) + } + canceledConsume, canceledDispatch = instruction, terminator + continue + } + t.Fatalf("%s child completion consume is not status-dispatched into cleanup:\n%s", function.Name(), body) + } + } + if normalConsume.IsNil() || canceledConsume.IsNil() || dispatch.IsNil() || canceledDispatch.IsNil() { + t.Fatalf("%s lacks distinct normal/canceled child completion reconciliation:\n%s", function.Name(), body) + } + gateFound := false + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || instruction.CalledValue().Name() != coroRunDecisionTakeZeroHookV1 { + continue + } + terminator := block.LastInstruction() + if terminator.IsNil() || terminator.InstructionOpcode() != llvm.Br || terminator.SuccessorsCount() != 2 { + continue + } + first, second := terminator.Successor(0), terminator.Successor(1) + normalBlock, canceledBlock := normalConsume.InstructionParent(), canceledConsume.InstructionParent() + if first == normalBlock && second == canceledBlock || first == canceledBlock && second == normalBlock { + gateFound = true + break + } + } + } + if !gateFound { + t.Fatalf("%s normal/canceled consumes are not mutually exclusive resumed run-decision successors:\n%s", function.Name(), body) + } + var returned, panicked, aborted, shutdown llvm.BasicBlock + for successor := 1; successor < dispatch.SuccessorsCount(); successor++ { + switch dispatch.GetSwitchCaseValue(successor).ZExtValue() { + case coroAwaitCompletionReturn: + returned = dispatch.Successor(successor) + case coroAwaitCompletionPanic: + panicked = dispatch.Successor(successor) + case coroAwaitCompletionAbort: + aborted = dispatch.Successor(successor) + case coroAwaitCompletionShutdown: + shutdown = dispatch.Successor(successor) + } + } + if returned.IsNil() || panicked.IsNil() || aborted.IsNil() || shutdown.IsNil() || + returned == panicked || returned == aborted || returned == shutdown || panicked == aborted || + panicked == shutdown || aborted == shutdown { + t.Fatalf("%s completion switch lacks distinct Return/Panic/Abort/Shutdown cases:\n%s", function.Name(), body) + } + if !coroTestBlockLoadsI32(returned) || !coroTestBlockStoresGlobal(returned, "foo.Sink") { + t.Fatalf("%s Return completion does not load and commit the child result:\n%s", function.Name(), returned.AsValue().String()) + } + if coroTestBlockLoadsI32(panicked) || coroTestBlockStoresGlobal(panicked, "foo.Sink") { + t.Fatalf("%s Panic completion incorrectly reads or commits the child result:\n%s", function.Name(), panicked.AsValue().String()) + } + for _, terminal := range []struct { + name string + block llvm.BasicBlock + status uint32 + }{ + {name: "Abort", block: aborted, status: uint32(coroAwaitCompletionAbort)}, + {name: "Shutdown", block: shutdown, status: uint32(coroAwaitCompletionShutdown)}, + } { + if coroTestBlockLoadsI32(terminal.block) || coroTestBlockStoresGlobal(terminal.block, "foo.Sink") || + !coroTestBlockStoresI32(terminal.block, terminal.status) || + !coroTestBlockStoresI32(terminal.block, coroStaticCleanupContinueComplete) || + !coroTestBlockCanReachDirectCall(terminal.block, "foo.Cleanup") { + t.Fatalf("%s %s completion does not become a cleanup base without reading child results:\n%s", + function.Name(), terminal.name, terminal.block.AsValue().String()) + } + } + if !coroTestBlockStoresI32(returned, coroStaticCleanupContinueFirstRun) || + !coroTestBlockStoresI32(panicked, coroStaticCleanupContinueRecover) { + t.Fatalf("%s Return/Panic outcomes do not select RunDefers/Panic cleanup continuations:\nReturn:\n%s\nPanic:\n%s", + function.Name(), returned.AsValue().String(), panicked.AsValue().String()) + } + returnedTerminator, panickedTerminator := returned.LastInstruction(), panicked.LastInstruction() + if returnedTerminator.InstructionOpcode() != llvm.Br || returnedTerminator.SuccessorsCount() != 1 || + panickedTerminator.InstructionOpcode() != llvm.Br || panickedTerminator.SuccessorsCount() != 1 || + returnedTerminator.Successor(0) != panickedTerminator.Successor(0) { + t.Fatalf("%s Return/Panic completion cases do not enter the shared cleanup drainer:\nReturn:\n%s\nPanic:\n%s", + function.Name(), returned.AsValue().String(), panicked.AsValue().String()) + } + drainer := returnedTerminator.Successor(0) + if !coroTestBlockCanReachDirectCall(drainer, "foo.Cleanup") { + t.Fatalf("%s shared completion join cannot reach the static defer drainer:\n%s", function.Name(), body) + } + for successor := 1; successor < canceledDispatch.SuccessorsCount(); successor++ { + if !coroTestBlockCanReachDirectCall(canceledDispatch.Successor(successor), "foo.Cleanup") { + t.Fatalf("%s canceled child status case %d does not enter the static defer drainer:\n%s", function.Name(), successor, body) + } + } + if coroTestBlockHasDirectCall(panicked, coroPanicPrepareHookV1) { + t.Fatalf("%s Panic completion bypasses the parent cleanup drainer:\n%s", function.Name(), panicked.AsValue().String()) + } + completeCalls := 0 + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || instruction.CalledValue().Name() != coroCompletePrepareHookV2 { + continue + } + completeCalls++ + if got := instruction.OperandsCount() - 1; got != 4 { + t.Fatalf("%s terminal completion arguments = %d, want (g,handle,header,status):\n%s", + function.Name(), got, instruction.String()) + } + status := instruction.Operand(3) + if status.InstructionOpcode() != llvm.Load || status.Type().TypeKind() != llvm.IntegerTypeKind || + status.Type().IntTypeWidth() != 32 { + t.Fatalf("%s terminal completion does not load its frame-local status:\n%s", function.Name(), instruction.String()) + } + } + } + if completeCalls != 1 { + t.Fatalf("%s terminal completion calls = %d, want one shared publication:\n%s", function.Name(), completeCalls, body) + } +} + +func coroTestBlockLoadsI32(block llvm.BasicBlock) bool { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() == llvm.Load && instruction.Type().TypeKind() == llvm.IntegerTypeKind && + instruction.Type().IntTypeWidth() == 32 { + return true + } + } + return false +} + +func coroTestBlockStoresGlobal(block llvm.BasicBlock, name string) bool { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() == llvm.Store && instruction.Operand(1).Name() == name { + return true + } + } + return false +} + +func coroTestBlockStoresI32(block llvm.BasicBlock, value uint32) bool { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Store { + continue + } + stored := instruction.Operand(0) + if stored.Type().TypeKind() == llvm.IntegerTypeKind && stored.Type().IntTypeWidth() == 32 && + !stored.IsAConstantInt().IsNil() && stored.ZExtValue() == uint64(value) { + return true + } + } + return false +} + +func coroTestBlockHasDirectCall(block llvm.BasicBlock, callee string) bool { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() == llvm.Call && instruction.CalledValue().Name() == callee { + return true + } + } + return false +} + +func coroTestBlockCanReachDirectCall(entry llvm.BasicBlock, callee string) bool { + seen := make(map[llvm.BasicBlock]bool) + pending := []llvm.BasicBlock{entry} + for len(pending) != 0 { + block := pending[len(pending)-1] + pending = pending[:len(pending)-1] + if block.IsNil() || seen[block] { + continue + } + seen[block] = true + if coroTestBlockHasDirectCall(block, callee) { + return true + } + terminator := block.LastInstruction() + for successor := 0; successor < terminator.SuccessorsCount(); successor++ { + pending = append(pending, terminator.Successor(successor)) + } + } + return false +} + +func compileCoroAwaitCompletionCleanupFixture( + t *testing.T, + target *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, *ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroAwaitCompletionCleanupFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + parent, child := ssaPkg.Func("Parent"), ssaPkg.Func("Child") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: parent, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == child { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, parent, child +} + +func compileCoroStaticCleanupIRFixture( + t *testing.T, + target *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroStaticCleanupIRFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root, first, second := ssaPkg.Func("Root"), ssaPkg.Func("First"), ssaPkg.Func("Second") + var third *ssa.Function + for _, function := range universe.Functions() { + if function != nil && function.Name() == "Third" && function.Signature != nil && function.Signature.Recv() != nil { + third = function + break + } + } + if third == nil { + prog.Dispose() + t.Fatal("Third method is absent from the emission universe") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == first || function == second || function == third { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root +} + +func compileCoroCapturedStaticCleanupFixture( + t *testing.T, + targetMachine *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, *ssa.MakeClosure, *ssa.Function) { + t.Helper() + testProgram := newEmissionTestProgram() + testProgram.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePackage := testProgram.addPackage(t, llssa.PkgRuntime, `package runtime +import "unsafe" +func AllocU(size uintptr) unsafe.Pointer { + if size == 0 { return nil } + return nil +} +func AllocZ(size uintptr) unsafe.Pointer { + if size == 0 { return nil } + return nil +} +`) + fooPackage := testProgram.addPackage(t, "foo", coroCapturedStaticCleanupIRFixture) + testProgram.ssa.Build() + ssaPkg := fooPackage.ssa + files := []*ast.File{fooPackage.file} + var prog llssa.Program + if targetMachine == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, targetMachine) + } + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePackage.ssa, Files: []*ast.File{runtimePackage.file}}, + {SSA: ssaPkg, Files: files}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + var closure *ssa.MakeClosure + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + deferred, ok := instruction.(*ssa.Defer) + if !ok { + continue + } + closure, _ = deferred.Call.Value.(*ssa.MakeClosure) + break + } + if closure != nil { + break + } + } + if closure == nil { + prog.Dispose() + t.Fatal("captured cleanup fixture has no exact MakeClosure defer") + } + cleanupTarget, ok := closure.Fn.(*ssa.Function) + if !ok || cleanupTarget == nil { + prog.Dispose() + t.Fatal("captured cleanup fixture MakeClosure has no exact function target") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == cleanupTarget { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root, closure, cleanupTarget +} + +func compileCoroDynamicCleanupFixture( + t *testing.T, + targetMachine *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function) { + t.Helper() + testProgram := newEmissionTestProgram() + testProgram.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePackage := testProgram.addPackage(t, llssa.PkgRuntime, `package runtime +import "unsafe" +func AllocU(size uintptr) unsafe.Pointer { + if size == 0 { return nil } + return nil +} +func FreeDeferNode(pointer unsafe.Pointer) { + if pointer == nil { return } +} +`) + fooPackage := testProgram.addPackage(t, "foo", coroDynamicCleanupIRFixture) + testProgram.ssa.Build() + ssaPkg := fooPackage.ssa + files := []*ast.File{fooPackage.file} + var prog llssa.Program + if targetMachine == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, targetMachine) + } + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePackage.ssa, Files: []*ast.File{runtimePackage.file}}, + {SSA: ssaPkg, Files: files}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root, cleanup := ssaPkg.Func("Root"), ssaPkg.Func("Cleanup") + var descriptorDefer *ssa.Defer + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + deferred, ok := instruction.(*ssa.Defer) + if ok && deferred.Call.StaticCallee() == nil { + descriptorDefer = deferred + break + } + } + if descriptorDefer != nil { + break + } + } + if descriptorDefer == nil { + prog.Dispose() + t.Fatal("dynamic cleanup fixture has no function-value defer") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: root, Demand: coro.AsyncDemand}, + {Function: ssaPkg.Func("init"), Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == cleanup { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, NeedsDispatch: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyClosedDynamicCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.SSAClosedDynamicCallCertificate, bool, error) { + if call == descriptorDefer { + return coro.SSAClosedDynamicCallCertificate{Targets: []*ssa.Function{cleanup}}, true, nil + } + return coro.SSAClosedDynamicCallCertificate{}, false, nil + }, + ClassifyUnknownCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.UnknownTarget, error) { + if call == descriptorDefer { + return coro.UnknownManagedDispatch, nil + } + return coro.UnknownManaged, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root +} + +func TestCoroStaticCleanupPlainTargetQuery(t *testing.T) { + const source = `package foo +type Guard struct{} +func (*Guard) release() {} +func Root(guard *Guard) { defer guard.release() } +` + prog, universe, plan, root, target := buildCoroStaticCleanupPlanFixture(t, source) + defer prog.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || !rootPlan.Exec.Contains(coro.NeedsCleanupFrame) { + t.Fatalf("Root plan = %+v, present=%t; want cleanup coroutine", rootPlan, ok) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.Emission != coro.EmitPlain || targetPlan.FuncRep != coro.DirectPlain { + t.Fatalf("release plan = %+v, present=%t; want DirectPlain", targetPlan, ok) + } + cleanup, err := prepareCoroStaticCleanupPlan(root, plan, universe, "", true) + if err != nil { + t.Fatal(err) + } + if cleanup == nil || len(cleanup.sites) != 1 || cleanup.sites[0].target != target || + cleanup.sites[0].kind != coroStaticCleanupPlain || len(cleanup.sites[0].instruction.Call.Args) != 1 { + t.Fatalf("static receiver cleanup = %+v", cleanup) + } + certified, err := universe.CoroStaticCleanupPlainTarget(plan, target, "") + if err != nil || !certified { + t.Fatalf("plain cleanup target certified=%t, err=%v", certified, err) + } +} + +func TestCoroStaticCleanupPlainTargetQueryRejectsOtherConsumers(t *testing.T) { + const source = `package foo +func cleanup() {} +func Root() { defer cleanup(); cleanup() } +` + prog, universe, plan, _, target := buildCoroStaticCleanupPlanFixture(t, source) + defer prog.Dispose() + certified, err := universe.CoroStaticCleanupPlainTarget(plan, target, "") + if err != nil { + t.Fatal(err) + } + if certified { + t.Fatal("plain cleanup target with an ordinary call consumer was certified") + } +} + +func TestCoroStaticCleanupPlanFailsClosed(t *testing.T) { + tests := []struct { + name string + source string + explicit bool + want string + }{ + { + name: "legacy panic ABI", + source: `package foo +func cleanup() {} +func Root() { defer cleanup() } +`, + want: "legacy panic", + }, + { + name: "captured plain closure", + source: `package foo +func Root(value uint32) { defer func() { _ = value }() } +`, + explicit: true, + want: "direct coroutine", + }, + { + name: "dynamic plain closure without no-unwind proof", + source: `package foo +func Root(value uint32, first bool) { + left := func() { _ = value } + right := func() { _ = value + 1 } + selected := left + if !first { selected = right } + defer selected() +} +`, + explicit: true, + want: "no-unwind proof", + }, + { + name: "loop registration without frozen dynamic helpers", + source: `package foo +func cleanup() {} +func Root() { for index := 0; index != 1; index++ { defer cleanup() } } +`, + explicit: true, + want: "AllocU", + }, + { + name: "nested cleanup target", + source: `package foo +func inner() {} +func cleanup() { defer inner() } +func Root() { defer cleanup() } +`, + explicit: true, + want: "nested cleanup", + }, + { + name: "cleanup child panic", + source: `package foo +var Payload uint32 +func cleanup() { panic(&Payload) } +func Root() { defer cleanup() } +`, + explicit: true, + want: "no-unwind proof", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + prog, universe, plan, root, _ := buildCoroStaticCleanupPlanFixture(t, test.source) + defer prog.Dispose() + _, err := prepareCoroStaticCleanupPlan(root, plan, universe, "", test.explicit) + if err == nil || !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(test.want)) { + t.Fatalf("cleanup preflight error = %v, want %q", err, test.want) + } + }) + } +} + +func buildCoroStaticCleanupPlanFixture( + t *testing.T, + source string, +) (llssa.Program, *EmissionUniverse, *coro.SSAPlan, *ssa.Function, *ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + var target *ssa.Function + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if deferred, ok := instruction.(*ssa.Defer); ok { + target = deferred.Call.StaticCallee() + break + } + } + } + return prog, universe, plan, root, target +} diff --git a/cl/coro_delete_builtin_test.go b/cl/coro_delete_builtin_test.go new file mode 100644 index 0000000000..8e7b10be9f --- /dev/null +++ b/cl/coro_delete_builtin_test.go @@ -0,0 +1,57 @@ +//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 ( + "strings" + "testing" + + "golang.org/x/tools/go/ssa" +) + +func TestCoroDeleteBuiltinRequiresFrozenManagedHelpers(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func Root(values map[uint32]uint64, key uint32) { delete(values, key) } +`) + root := ssaPkg.Func("Root") + var call *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + candidate, ok := instruction.(*ssa.Call) + if !ok || candidate.Common() == nil { + continue + } + builtin, ok := candidate.Common().Value.(*ssa.Builtin) + if ok && builtin.Name() == "delete" { + call = candidate + } + } + } + if call == nil { + t.Fatal("fixture has no delete builtin") + } + audit, err := newCoroPhysicalPureSSAAudit(nil, nil, root, "") + if err != nil { + t.Fatal(err) + } + handled, reason := audit.validate(call) + if !handled || !strings.Contains(reason, "structured runtime helper validation requires a frozen emission universe") { + t.Fatalf("delete audit = handled %t, reason %q; want exact managed-helper gate", handled, reason) + } +} diff --git a/cl/coro_dispatch.go b/cl/coro_dispatch.go new file mode 100644 index 0000000000..33a33caf9a --- /dev/null +++ b/cl/coro_dispatch.go @@ -0,0 +1,1264 @@ +/* + * 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 ( + "crypto/sha256" + "encoding/hex" + "fmt" + "go/token" + "go/types" + "strconv" + "strings" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroPlainDispatchVersion = llssa.CoroPlainDispatchVersionV1 + coroPlainDispatchFlags = llssa.CoroPlainDispatchFlagsV1 + coroPlainDispatchDescriptorPrefix = "__llgo_coro_func_descriptor_v1." + coroPlainDispatchThunkPrefix = "__llgo_coro_func_plain_v1." + coroCoroDispatchThunkPrefix = "__llgo_coro_func_coro_v1." +) + +// coroPlainDispatchABI is deliberately target independent of the selected +// function body. Every function with the same canonical callable ABI receives +// the same hash, while its FunctionID digest is used only to make the descriptor +// and thunk symbols target-specific. +type coroPlainDispatchABI struct { + hash [16]byte + signature *types.Signature + resultSlotType types.Type +} + +// validateCoroDynamicDispatchTarget validates the single primary published by +// a v1 function descriptor. Capability and capture are properties of the +// descriptor/produced value, not reasons to clone the source body: a plain +// primary publishes HasPlain and a coroutine primary publishes HasCoro. +func validateCoroDynamicDispatchTarget(fn *ssa.Function, plan coro.FunctionPlan, universes ...*EmissionUniverse) error { + var universe *EmissionUniverse + if len(universes) != 0 { + universe = universes[0] + } + fail := func(format string, args ...any) error { + name := fmt.Sprint(plan.ID) + if fn != nil { + name = fn.String() + } + return fmt.Errorf("coroutine dynamic dispatch ABI: function %q (%s): %s", name, plan.ID, fmt.Sprintf(format, args...)) + } + if fn == nil || plan.External != coro.Defined || len(fn.Blocks) == 0 { + return fail("requires one defined SSA body") + } + rawPlainOnly := plan.RawPlainOnly && plan.ManagedDemand == coro.NoDemand && plan.RawPlainDemand && + plan.Emission == coro.EmitRawPlain && plan.Primary == coro.PrimaryPlain && plan.FuncRep == coro.DirectPlain + if plan.FuncRep != coro.Dispatch && !rawPlainOnly { + return fail("requires descriptor representation, got %s", plan.FuncRep) + } + if plan.Effect.IsOpaque() || plan.Exec.IsOpaque() { + return fail("opaque effect/execution policy requires an open boundary, got effect=%s exec=%s", plan.Effect, plan.Exec) + } + switch plan.Emission { + case coro.EmitPlain: + if plan.Primary != coro.PrimaryPlain || plan.Effect != coro.NoSuspend { + return fail("plain capability requires one exact non-suspending primary, got primary=%s effect=%s", plan.Primary, plan.Effect) + } + case coro.EmitCoroutine: + if plan.Primary != coro.PrimaryCoroutine || !plan.Effect.MaySuspend() { + return fail("coroutine capability requires one suspending primary, got primary=%s effect=%s", plan.Primary, plan.Effect) + } + case coro.EmitRawPlain: + if !rawPlainOnly { + return fail("raw-plain capability requires one exact raw-only primary, got raw-only=%t managed=%s raw=%t primary=%s representation=%s", + plan.RawPlainOnly, plan.ManagedDemand, plan.RawPlainDemand, plan.Primary, plan.FuncRep) + } + default: + return fail("requires one plain or coroutine primary, got emission=%s primary=%s", plan.Emission, plan.Primary) + } + if fn.Signature == nil || fn.Signature.Recv() != nil { + return fail("methods require receiver-aware dispatch lowering") + } + if fn.Signature.Variadic() { + return fail("variadic dispatch is not implemented") + } + directive := "" + if universe == nil { + directive = coroLeafABIDirective(fn) + } else { + var err error + directive, err = coroRawABIDirective(fn, universe) + if err != nil { + return fail("classify ABI directive: %v", err) + } + } + if directive != "" { + return fail("ABI directive %q requires an explicit boundary adapter", directive) + } + if isCgoExternSymbol(fn) { + return fail("cgo entry requires a foreign adapter") + } + genericInstance := coroMaterializedGenericInstance(fn) + boundMethod := false + if strings.HasPrefix(fn.Synthetic, "bound method wrapper for ") { + if err := validateCoroExactBoundMethodWrapper(fn); err != nil { + return fail("invalid bound method wrapper: %v", err) + } + boundMethod = true + } + methodExpression := false + if strings.HasPrefix(fn.Synthetic, "thunk for ") { + if err := validateCoroExactMethodExpressionThunk(fn); err != nil { + return fail("invalid method-expression thunk: %v", err) + } + methodExpression = true + } + if fn.Synthetic != "" && !genericInstance && !boundMethod && !methodExpression { + return fail("synthetic function %q is outside the plain dispatch ABI", fn.Synthetic) + } + if params := fn.TypeParams(); params != nil && params.Len() != 0 && !genericInstance { + return fail("generic declarations are not materialized dispatch bodies") + } + if (len(fn.TypeArgs()) != 0 || fn.Origin() != nil) && !genericInstance { + return fail("generic instances require a frozen instantiated dispatch ABI") + } + if err := validateCoroManagedDispatchSignatureShape(fn.Signature); err != nil { + return fail("signature: %v", err) + } + return nil +} + +// validateCoroPlainDispatchTarget is the first consumer slice's stricter +// contract. It deliberately remains no-capture/plain-only until ordinary +// dynamic call lowering is switched to the shared capability-aware API. +func validateCoroPlainDispatchTarget(fn *ssa.Function, plan coro.FunctionPlan, universes ...*EmissionUniverse) error { + var universe *EmissionUniverse + if len(universes) != 0 { + universe = universes[0] + } + if err := validateCoroDynamicDispatchTarget(fn, plan, universe); err != nil { + return err + } + fail := func(format string, args ...any) error { + return fmt.Errorf("coroutine plain dispatch ABI: function %q (%s): %s", fn.String(), plan.ID, fmt.Sprintf(format, args...)) + } + if plan.Emission != coro.EmitPlain || plan.Primary != coro.PrimaryPlain || plan.Effect != coro.NoSuspend { + return fail("requires plain descriptor emission, got emission=%s primary=%s effect=%s", plan.Emission, plan.Primary, plan.Effect) + } + if plan.Exec.Contains(coro.NeedsPreempt) { + return fail("execution flags %s require coroutine dispatch lowering", plan.Exec) + } + if len(fn.FreeVars) != 0 { + return fail("captured closure requires the capability-aware dynamic call path") + } + if err := validateCoroPlainDispatchSignatureShape(fn.Signature); err != nil { + return fail("signature: %v", err) + } + return nil +} + +// validateCoroManagedDispatchSignatureShape is the source-shape boundary for +// the universal descriptor ABI. Unlike the legacy plain-only descriptor, the +// universal ABI uses LLGo's ordinary physical function declaration and a typed +// result slot, so strings, slices, interfaces, pointers and multiple results do +// not need a special scalar transport. +// +// LLGo's ordinary InGo conversion already lowers every inline function leaf +// recursively to the same two-pointer closure aggregate used by the universal +// descriptor ({descriptor, environment}). The whole-program FuncRepMap owns +// whether each such leaf contains a direct code pointer or a descriptor; this +// signature gate therefore accepts nested function parameters/results without +// inventing a second transport. Producers and consumers remain fail-closed at +// their exact ValuePlan/CallPlan boundaries. +func validateCoroManagedDispatchSignatureShape(sig *types.Signature) error { + if sig == nil { + return fmt.Errorf("missing signature") + } + return nil +} + +// validateCoroPlainDispatchSignatureShape preserves the deliberately narrow +// legacy CallCoroPlainDispatch contract. Managed coroutine callers use the +// capability-aware universal descriptor path above. +func validateCoroPlainDispatchSignatureShape(sig *types.Signature) error { + if sig == nil { + return fmt.Errorf("missing signature") + } + if sig.Results().Len() > 1 { + return fmt.Errorf("multiple results are not implemented") + } + for _, item := range []struct { + role string + tuple *types.Tuple + }{ + {"parameter", sig.Params()}, + {"result", sig.Results()}, + } { + for i := 0; i < item.tuple.Len(); i++ { + if !coroPlainDispatchSourceScalar(item.tuple.At(i).Type()) { + return fmt.Errorf("%s %d type %s is not a supported scalar", item.role, i, item.tuple.At(i).Type()) + } + } + } + return nil +} + +func coroPlainDispatchSourceScalar(typ types.Type) bool { + typ = types.Unalias(typ) + if named, ok := typ.(*types.Named); ok { + return coroPlainDispatchSourceScalar(named.Underlying()) + } + switch value := typ.Underlying().(type) { + case *types.Basic: + info := value.Info() + return value.Kind() == types.UnsafePointer || info&(types.IsBoolean|types.IsInteger|types.IsFloat) != 0 + case *types.Pointer, *types.Map, *types.Chan: + return true + default: + return false + } +} + +// validateCoroCallableTransportValue proves the physical representation of +// every function-containing leaf copied through an interface boundary. +// Managed Go functions use the compilation-wide {descriptor, environment} +// closure, while an exact //llgo:type C function remains one raw code pointer. +// The two transports are orthogonal to their logical Go signature and must +// never be reinterpreted as one another while boxing or asserting a value. +func validateCoroCallableTransportValue( + plan *coro.SSAPlan, + owner *ssa.Function, + value ssa.Value, + universe *EmissionUniverse, +) error { + ownerName := "" + if owner != nil { + ownerName = owner.Name() + } + fail := func(format string, args ...any) error { + return fmt.Errorf("coroutine callable transport ABI: function %q: %s", ownerName, fmt.Sprintf(format, args...)) + } + if plan == nil { + return fail("requires a compilation plan") + } + if owner == nil { + return fail("requires an owning SSA function") + } + if value == nil || value.Type() == nil { + return fail("value is not function-containing") + } + effectiveType := coroCallableEffectiveType(universe, owner, value.Type()) + schema := coroCallableTransportSchema(effectiveType) + if len(schema) == 0 { + return fail("value is not function-containing") + } + valuePlan, found := plan.ValuePlan(value) + if !found || valuePlan.Value != value { + return fail("value %q has no exact function ValuePlan", value.Name()) + } + if len(valuePlan.Funcs) != len(schema) { + return fail("value %q has %d planned function leaves, want %d", value.Name(), len(valuePlan.Funcs), len(schema)) + } + for index, expected := range schema { + leaf := valuePlan.Funcs[index] + if !equalCoroCallablePath(leaf.Path, expected.path) { + return fail("value %q function leaf %d has path %+v, want %+v", value.Name(), index, leaf.Path, expected.path) + } + transport, err := coroCallableLeafTransport(universe, expected.typ) + if err != nil { + return fail("value %q function leaf %d: %v", value.Name(), index, err) + } + if universe == nil { + // Structural unit tests without a frontend universe can still prove + // representation invariants, but cannot independently recover named + // //llgo:type metadata. In production the frozen universe is mandatory. + transport = leaf.Transport + } + if err := validateCoroInterfaceCallableLeaf(leaf, transport); err != nil { + return fail("value %q function leaf %d: %v", value.Name(), index, err) + } + if transport != coro.ManagedTransport { + continue + } + sig, ok := types.Unalias(expected.typ).Underlying().(*types.Signature) + if !ok || sig.Recv() != nil || sig.Variadic() { + return fail("value %q managed function leaf %d requires an ordinary non-variadic signature", value.Name(), index) + } + if params := sig.TypeParams(); params != nil && params.Len() != 0 { + return fail("value %q managed function leaf %d has an unsupported generic signature", value.Name(), index) + } + if params := sig.RecvTypeParams(); params != nil && params.Len() != 0 { + return fail("value %q managed function leaf %d has an unsupported generic receiver signature", value.Name(), index) + } + if err := validateCoroManagedDispatchSignatureShape(sig); err != nil { + return fail("value %q managed function leaf %d signature: %v", value.Name(), index, err) + } + } + if assertion, asserted := value.(*ssa.TypeAssert); asserted { + // Type-assertion results are open values reconstructed from interface + // data. Their exact target set is therefore empty at this boundary; the + // subsequent dynamic call/spawn owns its independently frozen CallPlan. + for index, leaf := range valuePlan.Funcs { + if len(leaf.Targets) != 0 { + return fail("function assertion %q leaf %d unexpectedly claims exact targets", value.Name(), index) + } + if assertion.CommaOk { + if len(leaf.Path) == 0 || leaf.Path[0].Kind != coro.FuncPathTupleElement || leaf.Path[0].Index != 0 || !leaf.MayBeNil { + return fail("comma-ok function assertion %q leaf %d has no exact nullable tuple[0] transport", value.Name(), index) + } + } + } + } + if err := validateCoroPlainDispatchValue(plan, owner, value, universe); err != nil { + return err + } + return nil +} + +type coroCallableTransportLeaf struct { + path []coro.FuncPathStep + typ types.Type +} + +func coroCallableEffectiveType(universe *EmissionUniverse, owner *ssa.Function, typ types.Type) types.Type { + if universe == nil || owner == nil || typ == nil { + return typ + } + prepared := universe.ownerOf(owner) + if prepared == nil { + return typ + } + return universe.effectiveType(prepared, owner, typ) +} + +func coroCallableTransportSchema(typ types.Type) []coroCallableTransportLeaf { + var leaves []coroCallableTransportLeaf + collectCoroCallableTransportSchema(typ, nil, make(map[types.Type]bool), &leaves) + return leaves +} + +func collectCoroCallableTransportSchema( + typ types.Type, + path []coro.FuncPathStep, + visiting map[types.Type]bool, + leaves *[]coroCallableTransportLeaf, +) { + if typ == nil { + return + } + key := types.Unalias(typ) + if _, signature := key.Underlying().(*types.Signature); signature { + *leaves = append(*leaves, coroCallableTransportLeaf{ + path: append([]coro.FuncPathStep(nil), path...), + typ: typ, + }) + return + } + if visiting[key] { + return + } + visiting[key] = true + defer delete(visiting, key) + appendPath := func(kind coro.FuncPathKind, index int) []coro.FuncPathStep { + ret := make([]coro.FuncPathStep, len(path)+1) + copy(ret, path) + ret[len(path)] = coro.FuncPathStep{Kind: kind, Index: index} + return ret + } + switch underlying := key.Underlying().(type) { + case *types.Tuple: + for index := 0; index < underlying.Len(); index++ { + collectCoroCallableTransportSchema(underlying.At(index).Type(), appendPath(coro.FuncPathTupleElement, index), visiting, leaves) + } + case *types.Struct: + for index := 0; index < underlying.NumFields(); index++ { + collectCoroCallableTransportSchema(underlying.Field(index).Type(), appendPath(coro.FuncPathStructField, index), visiting, leaves) + } + case *types.Array: + collectCoroCallableTransportSchema(underlying.Elem(), appendPath(coro.FuncPathArrayElement, -1), visiting, leaves) + case *types.Slice: + collectCoroCallableTransportSchema(underlying.Elem(), appendPath(coro.FuncPathSliceElement, -1), visiting, leaves) + case *types.Map: + collectCoroCallableTransportSchema(underlying.Key(), appendPath(coro.FuncPathMapKey, -1), visiting, leaves) + collectCoroCallableTransportSchema(underlying.Elem(), appendPath(coro.FuncPathMapValue, -1), visiting, leaves) + case *types.Chan: + collectCoroCallableTransportSchema(underlying.Elem(), appendPath(coro.FuncPathChanElement, -1), visiting, leaves) + } +} + +func equalCoroCallablePath(left, right []coro.FuncPathStep) bool { + if len(left) != len(right) { + return false + } + for index := range left { + if left[index] != right[index] { + return false + } + } + return true +} + +func coroCallableLeafTransport(universe *EmissionUniverse, typ types.Type) (coro.FuncTransport, error) { + if typ == nil { + return coro.ManagedTransport, fmt.Errorf("has no source type") + } + if universe == nil || universe.prog == nil || universe.prog.TypeBackground(typ) != llssa.InC { + return coro.ManagedTransport, nil + } + if _, signature := types.Unalias(typ).Underlying().(*types.Signature); !signature { + return coro.ManagedTransport, fmt.Errorf("frontend marked non-function type %s as raw C transport", typ) + } + return coro.RawCCodePointer, nil +} + +func validateCoroInterfaceCallableLeaf(leaf coro.FuncRepLeaf, want coro.FuncTransport) error { + if err := leaf.Transport.Validate(); err != nil { + return err + } + if leaf.Transport != want { + return fmt.Errorf("transport=%s, want %s from frozen frontend type metadata", leaf.Transport, want) + } + switch want { + case coro.ManagedTransport: + if leaf.Rep != coro.Dispatch { + return fmt.Errorf("managed interface leaf requires Dispatch, got %s", leaf.Rep) + } + case coro.RawCCodePointer: + if leaf.Rep != coro.DirectPlain { + return fmt.Errorf("raw C interface leaf requires DirectPlain, got %s", leaf.Rep) + } + default: + return fmt.Errorf("unsupported function transport %s", want) + } + return nil +} + +func validateCoroPlainDispatchConsumers( + plan *coro.SSAPlan, + universe *EmissionUniverse, + interfacePlain *coroClosedInterfacePlainPlan, + managedInterface *coroManagedInterfaceDispatchPlan, +) error { + if plan == nil { + return fmt.Errorf("coroutine plain dispatch ABI requires a compilation plan") + } + for _, function := range plan.Functions() { + if function.Plan.Emission != coro.EmitPlain && function.Plan.Emission != coro.EmitCoroutine { + continue + } + fn := function.Function + for _, param := range fn.Params { + if err := validateCoroPlainDispatchValue(plan, fn, param, universe); err != nil { + return err + } + } + for _, free := range fn.FreeVars { + if err := validateCoroPlainDispatchValue(plan, fn, free, universe); err != nil { + return err + } + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + if store, ok := instr.(*ssa.Store); ok && plan.ElidesConditionalManagedStore(store) { + // The complete closed-cell proof makes this exact descriptor + // producer unobservable. Code generation omits it, so neither + // its EmitNone target nor operand needs descriptor validation. + continue + } + if boxed, ok := instr.(*ssa.MakeInterface); ok && + coroCompilerElidedFunctionAddressBox(plan, universe, fn, boxed) { + // funcPCABI0/funcAddr consume the static SSA function directly; + // neither the transient interface nor its function operand is a + // descriptor producer/consumer. + continue + } + if value, ok := instr.(ssa.Value); ok { + if err := validateCoroPlainDispatchValue(plan, fn, value, universe); err != nil { + return err + } + } + for _, operand := range instr.Operands(nil) { + if operand != nil && *operand != nil { + if err := validateCoroPlainDispatchValue(plan, fn, *operand, universe); err != nil { + return err + } + } + } + if boxed, ok := instr.(*ssa.MakeInterface); ok { + if len(coroCallableTransportSchema(coroCallableEffectiveType(universe, fn, boxed.X.Type()))) != 0 { + if err := validateCoroCallableTransportValue(plan, fn, boxed.X, universe); err != nil { + return coroPlainDispatchInstructionError(fn, instr, err.Error()) + } + } + } + call, ok := instr.(ssa.CallInstruction) + if !ok || plan.ElidesCall(call) { + continue + } + common := call.Common() + if common != nil { + if _, builtin := common.Value.(*ssa.Builtin); builtin { + continue + } + } + callPlan, found := plan.CallPlan(call) + if !found { + return coroPlainDispatchInstructionError(fn, instr, "call has no compilation CallPlan") + } + if callPlan.Rep != coro.Dispatch { + continue + } + if callPlan.Transport != coro.ManagedTransport { + return coroPlainDispatchInstructionError(fn, instr, fmt.Sprintf( + "Dispatch CallPlan requires managed transport, got %s", callPlan.Transport, + )) + } + if managedInterface.acceptsCall(call) { + if callPlan.Open { + if err := validateCoroManagedInterfaceDispatchCall(plan, universe, fn, call, callPlan); err != nil { + return err + } + } + continue + } + if spawn, ok := call.(*ssa.Go); ok { + if _, err := plan.ResolveManagedDispatchSpawn(spawn); err != nil { + return coroPlainDispatchInstructionError(fn, instr, "invalid managed descriptor spawn: "+err.Error()) + } + continue + } + if deferred, ok := call.(*ssa.Defer); ok { + ownerPlan, planned := plan.FunctionPlan(fn) + if !planned || ownerPlan.Emission != coro.EmitCoroutine || !ownerPlan.Exec.Contains(coro.NeedsCleanupFrame) { + return coroPlainDispatchInstructionError(fn, instr, + "managed descriptor defer requires one coroutine cleanup owner") + } + if err := validateCoroManagedDispatchDefer(plan, fn, deferred, callPlan, universe); err != nil { + return err + } + continue + } + if callPlan.SyncDispatch { + if err := validateCoroPlainDispatchCall(plan, fn, call, callPlan, universe); err != nil { + return err + } + continue + } + managedDynamic := callPlan.Unresolved == coro.UnknownManagedDispatch + if !managedDynamic { + if ownerPlan, ok := plan.FunctionPlan(fn); ok && ownerPlan.Emission == coro.EmitCoroutine { + if direct, ok := call.(*ssa.Call); ok { + common := direct.Common() + managedDynamic = common != nil && common.StaticCallee() == nil && !common.IsInvoke() && common.Method == nil + } + } + } + if managedDynamic { + if err := validateCoroManagedDispatchCall(plan, fn, call, callPlan, universe); err != nil { + return err + } + continue + } + if interfacePlain.acceptsCall(call) { + continue + } + if ownerPlan, ok := plan.FunctionPlan(fn); ok && ownerPlan.Emission == coro.EmitCoroutine { + if direct, ok := call.(*ssa.Call); ok { + if dispatch, err := resolveCoroInterfaceDispatchPlan(plan, universe, direct); err == nil && coroInterfaceDispatchNeedsAwait(dispatch) { + continue + } + } + } + if err := validateCoroPlainDispatchCall(plan, fn, call, callPlan, universe); err != nil { + return err + } + } + } + } + return nil +} + +func validateCoroPlainDispatchValue(plan *coro.SSAPlan, owner *ssa.Function, value ssa.Value, universes ...*EmissionUniverse) error { + var universe *EmissionUniverse + if len(universes) != 0 { + universe = universes[0] + } + valuePlan, found := plan.ValuePlan(value) + if !found || !funcRepMapContains(valuePlan.Funcs, coro.Dispatch) { + return nil + } + if len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 { + // Aggregate storage preserves each leaf's independently planned physical + // transport: managed functions are two-pointer descriptors, while exact + // raw C functions remain one direct code pointer. Scalar producers and + // consumers are validated separately. Interface boxing is still checked + // at its instruction boundary below. + for _, leaf := range valuePlan.Funcs { + if leaf.Transport == coro.RawCCodePointer && leaf.Rep == coro.DirectPlain { + continue + } + if leaf.Transport != coro.ManagedTransport || leaf.Rep != coro.Dispatch { + return fmt.Errorf("coroutine plain dispatch ABI: function %q: aggregate value %q has invalid function leaf transport=%s representation=%s", owner.Name(), value.Name(), leaf.Transport, leaf.Rep) + } + } + return nil + } + leaf := valuePlan.Funcs[0] + if leaf.Transport != coro.ManagedTransport || leaf.Rep != coro.Dispatch { + return fmt.Errorf("coroutine plain dispatch ABI: function %q: value %q has a mixed function representation", owner.Name(), value.Name()) + } + if _, ok := types.Unalias(value.Type()).Underlying().(*types.Signature); !ok { + return fmt.Errorf("coroutine plain dispatch ABI: function %q: value %q is not a scalar function value", owner.Name(), value.Name()) + } + if len(leaf.Targets) == 0 { + if !leaf.MayBeNil { + return fmt.Errorf("coroutine plain dispatch ABI: function %q: value %q has no target and is not nil", owner.Name(), value.Name()) + } + return nil + } + for _, targetID := range leaf.Targets { + target, targetPlan, err := coroPlainDispatchPlanTarget(plan, targetID) + if err != nil { + return fmt.Errorf("coroutine plain dispatch ABI: function %q: value %q: %w", owner.Name(), value.Name(), err) + } + if err := validateCoroDynamicDispatchTarget(target, targetPlan, universe); err != nil { + return err + } + } + return nil +} + +func validateCoroPlainDispatchCall(plan *coro.SSAPlan, owner *ssa.Function, call ssa.CallInstruction, callPlan coro.SSACallPlan, universes ...*EmissionUniverse) error { + var universe *EmissionUniverse + if len(universes) != 0 { + universe = universes[0] + } + fail := func(format string, args ...any) error { + return coroPlainDispatchInstructionError(owner, call, fmt.Sprintf(format, args...)) + } + direct, ordinary := call.(*ssa.Call) + if !ordinary || direct == nil || callPlan.Kind != coro.CallDirect || + callPlan.Transport != coro.ManagedTransport { + return fail("descriptor dispatch is supported only for an ordinary direct call instruction") + } + common := direct.Common() + if common == nil || common.StaticCallee() != nil || common.IsInvoke() || common.Method != nil { + return fail("descriptor dispatch requires an ordinary dynamic function call") + } + if callPlan.Open || callPlan.Unresolved == coro.UnknownForeign { + return fail("open or foreign descriptor dispatch is not implemented") + } + if callPlan.SyncDispatch { + ownerPlan, ok := plan.FunctionPlan(owner) + if !ok || ownerPlan.Emission == coro.EmitNone { + return fail("synchronous descriptor dispatch owner has no emitted function plan") + } + } + if len(callPlan.Targets) > 1 { + return fail("multi-target descriptor dispatch is not implemented") + } + if len(callPlan.Targets) == 0 { + if !callPlan.MayBeNil { + return fail("closed descriptor call has no target and is not nil") + } + } else { + targetFn, targetPlan, err := coroPlainDispatchPlanTarget(plan, callPlan.Targets[0]) + if err != nil { + return fail("%v", err) + } + if err := validateCoroPlainDispatchTarget(targetFn, targetPlan, universe); err != nil { + return fail("%v", err) + } + if !types.Identical(common.Signature(), targetFn.Signature) { + return fail("call signature %s does not match target %q signature %s", common.Signature(), targetPlan.ID, targetFn.Signature) + } + } + valuePlan, found := plan.ValuePlan(common.Value) + if !found || len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 || + valuePlan.Funcs[0].Rep != coro.Dispatch || valuePlan.Funcs[0].Transport != coro.ManagedTransport { + return fail("callee has no exact scalar Dispatch ValuePlan") + } + leaf := valuePlan.Funcs[0] + if missing, ok := coroDispatchTargetsSubset(leaf.Targets, callPlan.Targets); !ok { + return fail("callee ValuePlan target %q is absent from CallPlan", missing) + } + if leaf.MayBeNil != callPlan.MayBeNil { + return fail("callee nilability %t conflicts with CallPlan nilability %t", leaf.MayBeNil, callPlan.MayBeNil) + } + return nil +} + +func funcRepMapContains(reps coro.FuncRepMap, want coro.FuncRep) bool { + for _, leaf := range reps { + if leaf.Rep == want { + return true + } + } + return false +} + +func coroPlainDispatchPlanTarget(plan *coro.SSAPlan, id coro.FunctionID) (*ssa.Function, coro.FunctionPlan, error) { + target, found := plan.Function(id) + if !found || target == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("target %q is absent from the compilation plan", id) + } + targetPlan, found := plan.FunctionPlan(target) + if !found || targetPlan.ID != id { + return nil, coro.FunctionPlan{}, fmt.Errorf("target %q has no canonical function plan", id) + } + return target, targetPlan, nil +} + +func coroPlainDispatchInstructionError(fn *ssa.Function, instr ssa.Instruction, reason string) error { + position := token.Position{} + if fn != nil && fn.Prog != nil && fn.Prog.Fset != nil && instr != nil { + position = fn.Prog.Fset.Position(instr.Pos()) + } + return fmt.Errorf("coroutine plain dispatch ABI: function %q at %s: %s", fn.Name(), position, reason) +} + +func nestedFunctionTypePath(typ types.Type) (string, bool) { + seen := make(map[types.Type]bool) + var visit func(types.Type, string, bool) (string, bool) + visit = func(typ types.Type, path string, root bool) (string, bool) { + if typ == nil { + return "", false + } + typ = types.Unalias(typ) + if seen[typ] { + return "", false + } + seen[typ] = true + switch value := typ.(type) { + case *types.Signature: + if !root { + return path, true + } + for i := 0; i < value.Params().Len(); i++ { + if found, ok := visit(value.Params().At(i).Type(), fmt.Sprintf("param[%d]", i), false); ok { + return found, true + } + } + for i := 0; i < value.Results().Len(); i++ { + if found, ok := visit(value.Results().At(i).Type(), fmt.Sprintf("result[%d]", i), false); ok { + return found, true + } + } + case *types.Named: + return visit(value.Underlying(), path+".underlying", false) + case *types.Pointer: + // Pointer identity is part of the canonical logical signature, while + // its physical layout terminates at one opaque pointer. + return "", false + case *types.Array: + return visit(value.Elem(), path+".elem", false) + case *types.Slice, *types.Map, *types.Chan, *types.Interface: + // These are reference/header-shaped physical values. Their logical + // element or method signatures are not copied inline through this + // call ABI, so they do not require recursive function-value lowering. + return "", false + case *types.Struct: + for i := 0; i < value.NumFields(); i++ { + if found, ok := visit(value.Field(i).Type(), fmt.Sprintf("%s.field[%d]", path, i), false); ok { + return found, true + } + } + } + return "", false + } + return visit(typ, "signature", true) +} + +func newCoroPlainDispatchABI(p *context, signature *types.Signature) (coroPlainDispatchABI, error) { + if p == nil || p.prog == nil || signature == nil { + return coroPlainDispatchABI{}, fmt.Errorf("coroutine plain dispatch ABI requires a program and signature") + } + patched, ok := p.patchType(signature).(*types.Signature) + if !ok { + return coroPlainDispatchABI{}, fmt.Errorf("patched dispatch signature is %T", p.patchType(signature)) + } + return newCoroPlainDispatchEffectiveABI(p, patched) +} + +// newCoroPlainDispatchEffectiveABI consumes a signature which has already +// crossed the current emission owner's type-patch boundary. ABI method-table +// materialization obtains exactly that signature while resolving the Ifn_ +// word; patching it a second time can rebuild an interface result graph and +// give the descriptor a different digest from its dynamic call site. +func newCoroPlainDispatchEffectiveABI(p *context, patched *types.Signature) (coroPlainDispatchABI, error) { + if p == nil || p.prog == nil || patched == nil { + return coroPlainDispatchABI{}, fmt.Errorf("coroutine plain dispatch effective ABI requires a program and signature") + } + patched = canonicalCoroPlainDispatchSignature(patched) + physical := p.prog.PhysicalFuncDecl(patched, llssa.InGo) + resultFields := make([]*types.Var, physical.Results().Len()) + for i := range resultFields { + resultFields[i] = types.NewField(token.NoPos, nil, fmt.Sprintf("r%d", i), physical.Results().At(i).Type(), false) + } + resultSlot := types.NewStruct(resultFields, nil) + + qualified := func(pkg *types.Package) string { + if pkg == nil { + return "" + } + return llssa.PathOf(pkg) + } + var key strings.Builder + writeDispatchHashField(&key, "domain", "llgo.coro.func-dispatch.v1") + writeDispatchHashField(&key, "version", strconv.FormatUint(uint64(coroPlainDispatchVersion), 10)) + // Capability and capture are runtime descriptor flags, not signature ABI. + // An open caller cannot know whether its producer is plain/coroutine or + // captured, so all compatible producers must share this hash. + writeDispatchHashField(&key, "closure", "two-pointer:descriptor,env;plain=(env,args)->results;coro=(g,out,env,args)->handle") + writeDispatchHashField(&key, "panic", activeCompilationABI(p.compilation, func(c *Compilation) string { return c.PanicABI }, coro.PanicLegacyABIV0)) + writeDispatchHashField(&key, "func-rep", activeCompilationABI(p.compilation, func(c *Compilation) string { return c.FuncRepABI }, coro.FuncRepABIV1)) + target := p.prog.TargetSpec() + writeDispatchHashField(&key, "triple", target.Triple) + writeDispatchHashField(&key, "cpu", target.CPU) + writeDispatchHashField(&key, "features", target.Features) + writeDispatchHashField(&key, "target-abi", target.TargetABI) + writeDispatchHashField(&key, "data-layout", p.prog.DataLayout()) + writeDispatchHashField(&key, "pointer-bytes", strconv.Itoa(p.prog.PointerSize())) + writeDispatchHashField(&key, "byte-order", strconv.Itoa(int(p.prog.TargetData().ByteOrder()))) + // The ABI identity is structural at every function nesting depth. Parameter + // and result names are source decoration, including inside callback types; + // they must not make an otherwise identical producer and consumer disagree. + writeDispatchHashField(&key, "logical-signature", structuralEmissionABITypeKey(patched)) + writeDispatchHashField(&key, "physical-signature", structuralEmissionABITypeKey(physical)) + if err := appendCoroPlainDispatchTupleLayout(&key, p.prog, "params", physical.Params(), qualified); err != nil { + return coroPlainDispatchABI{}, err + } + if err := appendCoroPlainDispatchTupleLayout(&key, p.prog, "results", physical.Results(), qualified); err != nil { + return coroPlainDispatchABI{}, err + } + if err := appendCoroPlainDispatchTypeLayout(&key, p.prog, "result-slot", resultSlot, qualified, make(map[types.Type]bool)); err != nil { + return coroPlainDispatchABI{}, err + } + sum := sha256.Sum256([]byte(key.String())) + var hash [16]byte + copy(hash[:], sum[:len(hash)]) + return coroPlainDispatchABI{hash: hash, signature: patched, resultSlotType: resultSlot}, nil +} + +// canonicalCoroPlainDispatchSignature removes source parameter/result names. +// go/types identity ignores those names, and a target declaration commonly has +// them while a function-typed parameter at the exact dynamic call does not. +// Letting names enter the descriptor hash would make two ABI-identical sites +// disagree at runtime. +func canonicalCoroPlainDispatchSignature(sig *types.Signature) *types.Signature { + params := make([]*types.Var, sig.Params().Len()) + for i := range params { + params[i] = types.NewParam(token.NoPos, nil, "", sig.Params().At(i).Type()) + } + results := make([]*types.Var, sig.Results().Len()) + for i := range results { + results[i] = types.NewParam(token.NoPos, nil, "", sig.Results().At(i).Type()) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), types.NewTuple(results...), false) +} + +func activeCompilationABI(c *Compilation, value func(*Compilation) string, fallback string) string { + if c != nil { + if current := value(c); current != "" { + return current + } + } + return fallback +} + +func writeDispatchHashField(builder *strings.Builder, name, value string) { + builder.WriteString(strconv.Itoa(len(name))) + builder.WriteByte(':') + builder.WriteString(name) + builder.WriteByte('=') + builder.WriteString(strconv.Itoa(len(value))) + builder.WriteByte(':') + builder.WriteString(value) + builder.WriteByte('\n') +} + +func appendCoroPlainDispatchTupleLayout(builder *strings.Builder, prog llssa.Program, path string, tuple *types.Tuple, qualified types.Qualifier) error { + writeDispatchHashField(builder, path+".count", strconv.Itoa(tuple.Len())) + for i := 0; i < tuple.Len(); i++ { + if err := appendCoroPlainDispatchTypeLayout(builder, prog, fmt.Sprintf("%s[%d]", path, i), tuple.At(i).Type(), qualified, make(map[types.Type]bool)); err != nil { + return err + } + } + return nil +} + +func appendCoroPlainDispatchTypeLayout(builder *strings.Builder, prog llssa.Program, path string, typ types.Type, qualified types.Qualifier, visiting map[types.Type]bool) error { + if typ == nil { + return fmt.Errorf("coroutine plain dispatch ABI: nil type at %s", path) + } + typ = types.Unalias(typ) + writeDispatchHashField(builder, path+".type", structuralEmissionABITypeKey(typ)) + physical := prog.Type(typ, llssa.InC) + writeDispatchHashField(builder, path+".size", strconv.FormatUint(prog.SizeOf(physical), 10)) + writeDispatchHashField(builder, path+".align", strconv.FormatUint(prog.AlignOf(physical), 10)) + if visiting[typ] { + writeDispatchHashField(builder, path+".cycle", "true") + return nil + } + visiting[typ] = true + defer delete(visiting, typ) + switch value := typ.(type) { + case *types.Named: + if _, referenceHeader := types.Unalias(value.Underlying()).(*types.Interface); referenceHeader { + // A named interface's package/name identity, physical size, and + // alignment above completely determine its call transport. Its method + // graph is metadata, not inline data layout. Recursing into it would + // make the ABI digest depend on whether equivalent package type graphs + // share the same go/types object pointers at a recursive receiver edge. + writeDispatchHashField(builder, path+".named-interface", "two-word-header") + return nil + } + return appendCoroPlainDispatchTypeLayout(builder, prog, path+".underlying", value.Underlying(), qualified, visiting) + case *types.Pointer: + writeDispatchHashField(builder, path+".pointer", "opaque") + case *types.Struct: + writeDispatchHashField(builder, path+".fields", strconv.Itoa(value.NumFields())) + for i := 0; i < value.NumFields(); i++ { + writeDispatchHashField(builder, fmt.Sprintf("%s.field[%d].offset", path, i), strconv.FormatUint(prog.OffsetOf(physical, i), 10)) + if err := appendCoroPlainDispatchTypeLayout(builder, prog, fmt.Sprintf("%s.field[%d]", path, i), value.Field(i).Type(), qualified, visiting); err != nil { + return err + } + } + case *types.Array: + writeDispatchHashField(builder, path+".length", strconv.FormatInt(value.Len(), 10)) + return appendCoroPlainDispatchTypeLayout(builder, prog, path+".element", value.Elem(), qualified, visiting) + case *types.Signature: + // A signature here is the first field of LLGo's already-converted + // two-pointer closure aggregate. LLVM opaque pointers make the code word + // layout independent of its pointee declaration; the structural type key + // above still commits the ABI hash to the complete, name-insensitive + // callback signature. + writeDispatchHashField(builder, path+".function-code", "opaque-pointer") + } + return nil +} + +// coroPlainDispatchValuePlan is the one codegen lookup boundary for a +// dynamically callable value. Preflight owns validation of the complete plan; +// emission only observes the exact immutable ValuePlan through this helper. +func (p *context) coroPlainDispatchValuePlan(value ssa.Value) (coro.SSAValuePlan, bool) { + if p == nil || p.compilation == nil || !p.compilation.CoroPlainDispatchActive() { + return coro.SSAValuePlan{}, false + } + plan := p.compilation.CoroPlan + if plan == nil { + return coro.SSAValuePlan{}, false + } + return plan.ValuePlan(value) +} + +func (p *context) tryCompileCoroPlainDispatchFunctionValue(b llssa.Builder, value *ssa.Function) (llssa.Expr, bool) { + valuePlan, found := p.coroPlainDispatchValuePlan(value) + if !found || len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 || valuePlan.Funcs[0].Rep != coro.Dispatch { + return llssa.Expr{}, false + } + if valuePlan.Funcs[0].Transport != coro.ManagedTransport { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: function value %q has Dispatch representation with non-managed transport %s", value.Name(), valuePlan.Funcs[0].Transport)) + } + return p.emitCoroDynamicDispatchValue(b, value, valuePlan.Funcs[0], nil), true +} + +func (p *context) tryCompileCoroPlainDispatchClosure(b llssa.Builder, closure *ssa.MakeClosure) (llssa.Expr, bool) { + valuePlan, found := p.coroPlainDispatchValuePlan(closure) + if !found || len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 || valuePlan.Funcs[0].Rep != coro.Dispatch { + return llssa.Expr{}, false + } + if valuePlan.Funcs[0].Transport != coro.ManagedTransport { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: closure %q has Dispatch representation with non-managed transport %s", closure.Name(), valuePlan.Funcs[0].Transport)) + } + target, ok := closure.Fn.(*ssa.Function) + if !ok || len(closure.Bindings) != len(target.FreeVars) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: closure %q has %d bindings for %d target free variables", closure.Name(), len(closure.Bindings), len(target.FreeVars))) + } + bindings := p.compileValues(b, closure.Bindings, 0) + return p.emitCoroDynamicDispatchValue(b, target, valuePlan.Funcs[0], bindings), true +} + +func (p *context) emitCoroDynamicDispatchValue( + b llssa.Builder, target *ssa.Function, leaf coro.FuncRepLeaf, bindings []llssa.Expr, +) llssa.Expr { + if leaf.Transport != coro.ManagedTransport || leaf.Rep != coro.Dispatch { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: target %q requires managed Dispatch transport, got transport=%s representation=%s", target.Name(), leaf.Transport, leaf.Rep)) + } + entry := p.mustFunctionSymbol(target) + plannedTarget := false + for _, targetID := range leaf.Targets { + if targetID == entry.plan.ID { + plannedTarget = true + break + } + } + if !plannedTarget { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: exact producer %q target %q is absent from its %d planned targets", target.Name(), entry.plan.ID, len(leaf.Targets))) + } + if err := validateCoroDynamicDispatchTarget(entry.function, entry.plan, p.compilation.EmissionUniverse); err != nil { + panic(err) + } + abi, err := newCoroPlainDispatchABI(p, entry.function.Signature) + if err != nil { + panic(err) + } + compile := p.compileFunction + if entry.plan.Emission == coro.EmitRawPlain { + compile = p.compileRawPlainFunction + } + physical, py, ftype := compile(entry.function) + if ftype != goFunc || physical == nil || py != nil { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: target %q did not compile as one Go function", entry.plan.ID)) + } + var rawPhysical llssa.Function + if entry.plan.Emission == coro.EmitCoroutine && p.compilation.CoroPlan.HasRawPlainVariant(entry.function) { + // The managed primary and legacy-stack variant are distinct physical + // capabilities of the same frozen SSA target. Publish the latter only + // when the whole-build plan proves that exact function has an + // independently validated raw body. + rawPhysical, py, ftype = p.compileRawPlainFunction(entry.function) + if ftype != goFunc || rawPhysical == nil || py != nil { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: target %q did not compile its frozen raw-plain variant as one Go function", entry.plan.ID)) + } + } + captured := len(entry.function.FreeVars) != 0 + if len(bindings) != len(entry.function.FreeVars) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: target %q has %d bindings for %d free variables", entry.plan.ID, len(bindings), len(entry.function.FreeVars))) + } + var env llssa.Expr + var closureCtx types.Type + if captured { + // Reuse the canonical LLGo closure allocator/layout instead of creating a + // second environment representation. The selected physical primary may + // be a coroutine ramp, so retag its opaque code pointer with the source + // closure signature solely while MakeClosure constructs {code,env}; only + // the env word is retained in the descriptor value. + ctx := makeClosureCtx(entry.pkgTypes, entry.function.FreeVars) + carrierSig := p.prog.PhysicalFuncDecl(llssa.FuncAddCtx(ctx, abi.signature), llssa.InGo) + // Retag as an opaque function pointer rather than a declaration type: + // LLVM functions themselves have a pointer value while FuncDecl.Type is + // the pointee signature. No call is emitted through this temporary view. + carrier := b.ChangeType(p.prog.Type(carrierSig, llssa.InC), physical.Expr) + closureCtx = carrier.RawType().(*types.Signature).Params().At(0).Type() + legacy := b.MakeClosure(carrier, bindings) + env = b.Field(legacy, 1) + } + targetHash := sha256.Sum256([]byte(entry.plan.ID)) + targetKey := hex.EncodeToString(targetHash[:8]) + "." + hex.EncodeToString(abi.hash[:]) + result := p.prog.Type(abi.resultSlotType, llssa.InC) + descriptorName := coroPlainDispatchDescriptorPrefix + targetKey + descriptor, found := p.coroPlainDescriptors[descriptorName] + if !found { + flags := uint32(0) + var plainEntry, coroEntry llssa.Expr + switch entry.plan.Emission { + case coro.EmitPlain, coro.EmitRawPlain: + flags |= llssa.CoroDispatchFlagHasPlain + plainEntry = p.newCoroDynamicDispatchEntryThunk( + coroPlainDispatchThunkPrefix+targetKey, physical.Expr, abi, entry.plan.Emission, closureCtx, + ) + case coro.EmitCoroutine: + flags |= llssa.CoroDispatchFlagHasCoro + coroEntry = p.newCoroDynamicDispatchEntryThunk( + coroCoroDispatchThunkPrefix+targetKey, physical.Expr, abi, entry.plan.Emission, closureCtx, + ) + if rawPhysical != nil { + flags |= llssa.CoroDispatchFlagHasPlain + plainEntry = p.newCoroDynamicDispatchEntryThunk( + coroPlainDispatchThunkPrefix+targetKey, rawPhysical.Expr, abi, coro.EmitRawPlain, closureCtx, + ) + } + default: + panic(fmt.Errorf("coroutine dynamic dispatch ABI: target %q has unsupported emission %s", entry.plan.ID, entry.plan.Emission)) + } + if !captured { + flags |= llssa.CoroDispatchFlagNoCapture + } + descriptor = p.pkg.NewCoroDispatchDescriptor(descriptorName, llssa.CoroDispatchDescriptorOptions{ + Version: coroPlainDispatchVersion, + Flags: flags, + ABIHash: abi.hash, + Signature: abi.signature, + PlainEntry: plainEntry, + CoroEntry: coroEntry, + Result: result, + }) + if p.coroPlainDescriptors == nil { + p.coroPlainDescriptors = make(map[string]llssa.Expr) + } + p.coroPlainDescriptors[descriptorName] = descriptor + } + return b.MakeCoroDispatchValue(abi.signature, descriptor, env) +} + +// newCoroDynamicDispatchEntryThunk adapts the stable descriptor ABI to the +// selected single primary. Descriptor entries always receive an opaque env at +// a fixed position. A captured LLGo body instead expects its typed leading +// closure context, so the thunk converts and inserts env without cloning the +// body. A no-capture thunk simply drops env. +func (p *context) newCoroDynamicDispatchEntryThunk( + name string, + target llssa.Expr, + abi coroPlainDispatchABI, + emission coro.BodyEmission, + closureCtx types.Type, +) llssa.Expr { + if name == "" || target.IsNil() { + panic("coroutine dynamic dispatch ABI: entry thunk requires a name and target") + } + source := p.prog.PhysicalFuncDecl(abi.signature, llssa.InGo) + var thunkSig *types.Signature + switch emission { + case coro.EmitPlain, coro.EmitRawPlain: + thunkSig = p.prog.CoroDispatchPlainEntrySignature(abi.signature) + case coro.EmitCoroutine: + thunkSig = p.prog.CoroDispatchCoroEntrySignature(abi.signature) + default: + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q has unsupported emission %s", name, emission)) + } + // A deterministic descriptor can be emitted by several package archives. + // Its content-addressed entry thunk must use matching coalescible linkage; + // otherwise identical ABI type-data consumers become duplicate definitions + // at the final link. + thunk := p.pkg.NewFuncEx(name, thunkSig, llssa.InC, false, true) + if !types.Identical(thunk.RawType(), thunkSig) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q conflicts with an existing signature", name)) + } + if thunk.HasBody() { + return thunk.Expr + } + + targetSig, ok := target.RawType().(*types.Signature) + if !ok || targetSig.Variadic() { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q target has no ordinary physical signature", name)) + } + targetParam := 0 + thunkSourceBase := 1 + if emission == coro.EmitCoroutine { + if targetSig.Results().Len() != 1 || !types.Identical(targetSig.Results().At(0).Type(), types.Typ[types.UnsafePointer]) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q target does not return one handle", name)) + } + for i := 0; i < 2; i++ { + if targetSig.Params().Len() <= targetParam || !types.Identical(targetSig.Params().At(targetParam).Type(), types.Typ[types.UnsafePointer]) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q target hidden parameter %d is not unsafe.Pointer", name, i)) + } + targetParam++ + } + thunkSourceBase = 3 + } else if !types.Identical(targetSig.Results(), source.Results()) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q target result signature does not match the source ABI", name)) + } + if closureCtx != nil { + if targetSig.Params().Len() <= targetParam || !types.Identical(targetSig.Params().At(targetParam).Type(), closureCtx) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q target closure context is absent or has the wrong type", name)) + } + targetParam++ + } + if targetSig.Params().Len()-targetParam != source.Params().Len() { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q target has %d source parameters, want %d", name, targetSig.Params().Len()-targetParam, source.Params().Len())) + } + for i := 0; i < source.Params().Len(); i++ { + if !types.Identical(targetSig.Params().At(targetParam+i).Type(), source.Params().At(i).Type()) { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: thunk %q target source parameter %d has the wrong type", name, i)) + } + } + b := thunk.MakeBody(1) + physicalArgs := make([]llssa.Expr, 0, source.Params().Len()+3) + if emission == coro.EmitCoroutine { + physicalArgs = append(physicalArgs, thunk.PhysicalParam(0), thunk.PhysicalParam(1)) + } + if closureCtx != nil { + envIndex := 0 + if emission == coro.EmitCoroutine { + envIndex = 2 + } + physicalArgs = append(physicalArgs, b.Convert(p.prog.Type(closureCtx, llssa.InC), thunk.PhysicalParam(envIndex))) + } + for i := 0; i < source.Params().Len(); i++ { + physicalArgs = append(physicalArgs, thunk.PhysicalParam(thunkSourceBase+i)) + } + ret := b.Call(target, physicalArgs...) + if targetSig.Results().Len() == 0 { + b.Return() + } else { + b.Return(ret) + } + b.EndBuild() + b.Dispose() + return thunk.Expr +} + +func (p *context) tryCompileCoroPlainDispatchCall(b llssa.Builder, call *ssa.Call) (llssa.Expr, bool) { + if call == nil || p.hasCoroPhysicalBody() || p.compilation == nil || p.compilation.CoroPlan == nil { + return llssa.Expr{}, false + } + callPlan, found := p.compilation.CoroPlan.CallPlan(call) + if !found || callPlan.Rep != coro.Dispatch { + return llssa.Expr{}, false + } + if callPlan.Transport != coro.ManagedTransport { + panic(fmt.Errorf("coroutine dynamic dispatch ABI: call %q has Dispatch representation with non-managed transport %s", call.String(), callPlan.Transport)) + } + if p.compilation.coroClosedInterfacePlain.acceptsCall(call) { + // Preserve the ordinary LLGo itab invoke. The closed candidate proof is + // a scheduling constraint, not a second function-value representation. + return llssa.Expr{}, false + } + if err := validateCoroPlainDispatchCall(p.compilation.CoroPlan, call.Parent(), call, callPlan, p.compilation.EmissionUniverse); err != nil { + panic(err) + } + return p.emitCoroPlainDispatchCall(b, call, false), true +} + +func (p *context) compileCoroPhysicalPlainDispatch( + b llssa.Builder, call *ssa.Call, instructionPlan coroPhysicalInstructionPlan, +) llssa.Expr { + if !p.hasCoroPhysicalBody() || call == nil || instructionPlan.control != coroPhysicalControlPlainDispatch { + panic("coroutine plain dispatch escaped its frozen physical control recipe") + } + return p.emitCoroPlainDispatchCall(b, call, true) +} + +func (p *context) emitCoroPlainDispatchCall(b llssa.Builder, call *ssa.Call, physical bool) llssa.Expr { + p.recordCallerLocationForCall(b, &call.Call) + p.emitPCLineLabel(b, call.Pos()) + fn := p.compileValue(b, call.Call.Value) + args := p.compileValues(b, call.Call.Args, fnNormal) + abi, err := newCoroPlainDispatchABI(p, call.Call.Signature()) + if err != nil { + panic(err) + } + result := p.prog.Type(abi.resultSlotType, llssa.InC) + opts := llssa.CoroPlainDispatchCallOptions{ + Version: coroPlainDispatchVersion, + Flags: coroPlainDispatchFlags, + ABIHash: abi.hash, + Result: result, + } + // Go evaluates the callee and arguments before a nil-function panic. In a + // physical coroutine, own that edge through the explicit-status fault ABI + // so this compiler-generated descriptor operation cannot introduce a + // hidden runtime.AssertNilDeref dependency after emission closure. + if physical { + p.compileCoroImplicitNilAccessGuard(b, b.Field(fn, 0)) + opts.DescriptorNonNil = true + } + return b.CallCoroPlainDispatch(fn, args, opts) +} diff --git a/cl/coro_dispatch_producer_test.go b/cl/coro_dispatch_producer_test.go new file mode 100644 index 0000000000..7bb192d249 --- /dev/null +++ b/cl/coro_dispatch_producer_test.go @@ -0,0 +1,708 @@ +//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/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +func TestCoroExactBoundMethodWrapperShape(t *testing.T) { + const source = `package foo +type Reader interface { Read() int } +type Counter struct { value int } +func (counter *Counter) Read() int { return counter.value } +func Concrete(counter *Counter) func() int { return counter.Read } +func Interface(reader Reader) func() int { return reader.Read } +` + ssaPkg, _, _ := buildGoSSAPkg(t, source) + wrappers := make(map[string]*ssa.Function) + for _, name := range []string{"Concrete", "Interface"} { + function := ssaPkg.Func(name) + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + closure, ok := instruction.(*ssa.MakeClosure) + if !ok { + continue + } + wrapper, ok := closure.Fn.(*ssa.Function) + if ok { + wrappers[name] = wrapper + } + } + } + } + for _, name := range []string{"Concrete", "Interface"} { + wrapper := wrappers[name] + if wrapper == nil { + t.Fatalf("%s has no bound method wrapper", name) + } + if err := validateCoroExactBoundMethodWrapper(wrapper); err != nil { + t.Fatalf("%s bound method wrapper rejected: %v\n%s", name, err, wrapper.String()) + } + } + concrete := wrappers["Concrete"] + original := concrete.Synthetic + concrete.Synthetic += " forged" + if err := validateCoroExactBoundMethodWrapper(concrete); err == nil { + t.Fatal("forged bound method identity was accepted") + } + concrete.Synthetic = original +} + +func TestCoroExactMethodExpressionThunkShape(t *testing.T) { + const source = `package foo +type Counter struct{} +func (*Counter) Release() {} +var Cleanup = (*Counter).Release +` + ssaPkg, _, _ := buildGoSSAPkg(t, source) + var thunk *ssa.Function + for function := range ssautil.AllFunctions(ssaPkg.Prog) { + if function != nil && function.Synthetic == "thunk for func (*foo.Counter).Release()" { + thunk = function + break + } + } + if thunk == nil { + t.Fatal("fixture has no method-expression thunk") + } + if err := validateCoroExactMethodExpressionThunk(thunk); err != nil { + t.Fatalf("canonical method-expression thunk rejected: %v\n%s", err, thunk.String()) + } + original := thunk.Synthetic + thunk.Synthetic += " forged" + if err := validateCoroExactMethodExpressionThunk(thunk); err == nil { + t.Fatal("forged method-expression identity was accepted") + } + thunk.Synthetic = original +} + +func TestCoroDynamicDispatchProducerCapturedPlainClosure(t *testing.T) { + const source = `package foo +func Root(seed int) func(int) int { + return func(value int) int { return seed + value } +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + if len(root.AnonFuncs) != 1 || len(root.AnonFuncs[0].FreeVars) != 1 { + t.Fatalf("Root anonymous functions = %+v; want one closure with one free variable", root.AnonFuncs) + } + target := root.AnonFuncs[0] + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.FuncRep != coro.Dispatch || targetPlan.Emission != coro.EmitPlain { + t.Fatalf("captured target plan = %+v, present=%t; want a descriptor-backed plain primary", targetPlan, ok) + } + + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + }}, + ) + if err != nil { + t.Fatalf("compile captured descriptor producer: %v", err) + } + module := compiled.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify captured descriptor producer: %v\n%s", err, module.String()) + } + descriptor := coroDispatchProducerOnlyGlobalWithPrefix(t, module, coroPlainDispatchDescriptorPrefix) + if got := descriptor.Initializer().Operand(1).ZExtValue(); got != uint64(llssa.CoroDispatchFlagHasPlain) { + t.Fatalf("captured plain descriptor flags = %#x, want HasPlain without NoCapture", got) + } + thunk := coroDispatchProducerOnlyFunctionWithPrefix(t, module, coroPlainDispatchThunkPrefix) + call := coroDispatchProducerOnlyCallTo(t, thunk, "") + if got := call.OperandsCount() - 1; got != 2 { + t.Fatalf("captured plain thunk target arguments = %d, want ctx+source argument", got) + } + if call.Operand(0).C != thunk.Param(0).C || call.Operand(1).C != thunk.Param(1).C { + t.Fatalf("captured plain thunk did not reorder descriptor (env,arg) to target (ctx,arg):\n%s", module.String()) + } + ir := module.String() + if !strings.Contains(ir, "runtime/internal/runtime.AllocU") { + t.Fatalf("captured descriptor producer did not reuse MakeClosure environment allocation:\n%s", ir) + } + if !strings.Contains(ir, "insertvalue { ptr, ptr }") || !strings.Contains(ir, ", ptr %") { + t.Fatalf("captured descriptor producer did not materialize a non-nil descriptor environment:\n%s", ir) + } +} + +func TestCoroDynamicDispatchProducerElidesDormantConditionalPublication(t *testing.T) { + const source = `package foo +var slot func() +func Target() {} +func Root() { slot = Target } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + target := ssaPkg.Func("Target") + var publication *ssa.Store + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + store, ok := instruction.(*ssa.Store) + if ok && store.Val == target { + publication = store + } + } + } + if publication == nil { + t.Fatal("Root has no direct Target Store") + } + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyConditionalManagedStoreReference: func(owner *ssa.Function, store *ssa.Store) (*ssa.Function, bool, error) { + if owner == root && store == publication { + return target, true, nil + } + return nil, false, nil + }, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, planned := plan.FunctionPlan(target) + if !planned || targetPlan.ManagedDemand != coro.NoDemand || targetPlan.RawPlainDemand || + targetPlan.Emission != coro.EmitNone || targetPlan.FuncRep != coro.Dispatch || plan.HasRawPlainVariant(target) || + !plan.ElidesConditionalManagedStore(publication) { + t.Fatalf("dormant conditional descriptor target = %+v/%t, variant=%t elided=%t", targetPlan, planned, plan.HasRawPlainVariant(target), plan.ElidesConditionalManagedStore(publication)) + } + valuePlan, planned := plan.ValuePlan(target) + if !planned || len(valuePlan.Funcs) != 1 || valuePlan.Funcs[0].Rep != coro.Dispatch { + t.Fatalf("raw-only Store ValuePlan = %+v/%t", valuePlan, planned) + } + + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + }}, + ) + if err != nil { + t.Fatalf("compile dormant conditional descriptor producer: %v", err) + } + module := compiled.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify dormant conditional descriptor producer: %v\n%s", err, module.String()) + } + if strings.Contains(module.String(), coroPlainDispatchDescriptorPrefix) || strings.Contains(module.String(), "foo.Target") { + t.Fatalf("dormant conditional publication materialized a descriptor or target:\n%s", module.String()) + } +} + +func TestCoroDynamicDispatchProducerPublishesMixedPlainAndCoroTarget(t *testing.T) { + const source = `package foo +var slot func() +func Target() {} +func Root() { slot = Target } +func Managed() { Target() } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + target := ssaPkg.Func("Target") + managed := ssaPkg.Func("Managed") + var publication *ssa.Store + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + store, ok := instruction.(*ssa.Store) + if ok && store.Val == target { + publication = store + } + } + } + if publication == nil { + t.Fatal("Root has no direct Target Store") + } + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: root, Demand: coro.SyncDemand}, + {Function: managed, Demand: coro.AsyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == target { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyConditionalManagedStoreReference: func(owner *ssa.Function, store *ssa.Store) (*ssa.Function, bool, error) { + if owner == root && store == publication { + return target, true, nil + } + return nil, false, nil + }, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, planned := plan.FunctionPlan(target) + if !planned || targetPlan.ManagedDemand == coro.NoDemand || targetPlan.RawPlainDemand || + targetPlan.RawPlainOnly || targetPlan.Emission != coro.EmitCoroutine || targetPlan.FuncRep != coro.Dispatch || + plan.HasRawPlainVariant(target) || plan.ElidesConditionalManagedStore(publication) { + t.Fatalf("managed descriptor target = %+v/%t, variant=%t", targetPlan, planned, plan.HasRawPlainVariant(target)) + } + valuePlan, planned := plan.ValuePlan(target) + if !planned || len(valuePlan.Funcs) != 1 || valuePlan.Funcs[0].Rep != coro.Dispatch { + t.Fatalf("mixed Store ValuePlan = %+v/%t", valuePlan, planned) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.FuncRepABI = coro.FuncRepABIV1 + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile mixed descriptor producer: %v", err) + } + module := compiled.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify mixed descriptor producer: %v\n%s", err, module.String()) + } + descriptor := coroDispatchProducerOnlyGlobalWithPrefix(t, module, coroPlainDispatchDescriptorPrefix) + wantFlags := uint64(llssa.CoroDispatchFlagHasCoro | llssa.CoroDispatchFlagNoCapture) + if got := descriptor.Initializer().Operand(1).ZExtValue(); got != wantFlags { + t.Fatalf("mixed descriptor flags = %#x, want %#x", got, wantFlags) + } + coroThunk := coroDispatchProducerOnlyFunctionWithPrefix(t, module, coroCoroDispatchThunkPrefix) + coroCall := coroDispatchProducerOnlyCallTo(t, coroThunk, "") + if got := coroCall.CalledValue().Name(); got != "foo.Target"+coroPrimarySuffix { + t.Fatalf("mixed coroutine thunk target = %q, want managed primary foo.Target%s", got, coroPrimarySuffix) + } +} + +func TestCoroDynamicDispatchProducerCoroThunkDropsNilEnvironment(t *testing.T) { + prog := newLLSSAProg(t) + defer prog.Dispose() + pkg := prog.NewPackage("dispatchproducer", "example.com/dispatchproducer") + logical := types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewParam(token.NoPos, nil, "value", types.Typ[types.Int])), + types.NewTuple(types.NewParam(token.NoPos, nil, "result", types.Typ[types.Int])), + false, + ) + hidden := []*types.Var{ + types.NewParam(token.NoPos, nil, "__llgo_g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "__llgo_out", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "value", types.Typ[types.Int]), + } + physical := types.NewSignatureType( + nil, nil, nil, types.NewTuple(hidden...), + types.NewTuple(types.NewParam(token.NoPos, nil, "handle", types.Typ[types.UnsafePointer])), false, + ) + target := pkg.NewFunc("target$coro", physical, llssa.InC) + targetBody := target.MakeBody(1) + targetBody.Return(prog.Nil(prog.VoidPtr())) + targetBody.EndBuild() + targetBody.Dispose() + resultSlot := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "r0", types.Typ[types.Int], false), + }, nil) + abi := coroPlainDispatchABI{signature: logical, resultSlotType: resultSlot} + ctx := &context{prog: prog, pkg: pkg} + thunkName := coroCoroDispatchThunkPrefix + "focused" + thunkExpr := ctx.newCoroDynamicDispatchEntryThunk(thunkName, target.Expr, abi, coro.EmitCoroutine, nil) + descriptorName := coroPlainDispatchDescriptorPrefix + "focused" + descriptor := pkg.NewCoroDispatchDescriptor(descriptorName, llssa.CoroDispatchDescriptorOptions{ + Version: llssa.CoroDispatchVersionV1, + Flags: llssa.CoroDispatchFlagHasCoro | llssa.CoroDispatchFlagNoCapture, + Signature: logical, + CoroEntry: thunkExpr, + Result: prog.Type(resultSlot, llssa.InC), + }) + producerSig := types.NewSignatureType( + nil, nil, nil, nil, + types.NewTuple(types.NewParam(token.NoPos, nil, "", logical)), false, + ) + producer := pkg.NewFunc("producer", producerSig, llssa.InGo) + producerBody := producer.MakeBody(1) + producerBody.Return(producerBody.MakeCoroDispatchValue(logical, descriptor, llssa.Nil)) + producerBody.EndBuild() + producerBody.Dispose() + + module := pkg.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify coroutine descriptor producer: %v\n%s", err, module.String()) + } + global := module.NamedGlobal(descriptorName) + if global.IsNil() { + t.Fatal("coroutine descriptor global is absent") + } + if got := global.Initializer().Operand(1).ZExtValue(); got != uint64(llssa.CoroDispatchFlagHasCoro|llssa.CoroDispatchFlagNoCapture) { + t.Fatalf("coroutine descriptor flags = %#x, want HasCoro|NoCapture", got) + } + thunk := module.NamedFunction(thunkName) + call := coroDispatchProducerOnlyCallTo(t, thunk, target.Name()) + if got := call.OperandsCount() - 1; got != 3 { + t.Fatalf("coroutine thunk target arguments = %d, want g+out+source argument", got) + } + if call.Operand(0).C != thunk.Param(0).C || call.Operand(1).C != thunk.Param(1).C || call.Operand(2).C != thunk.Param(3).C { + t.Fatalf("coroutine thunk did not drop descriptor env and preserve (g,out,args) order:\n%s", module.String()) + } +} + +func TestCoroDynamicDispatchProducerCapturedCoroThunkInsertsEnvironment(t *testing.T) { + prog := newLLSSAProg(t) + defer prog.Dispose() + pkg := prog.NewPackage("captureddispatchproducer", "example.com/captureddispatchproducer") + logical := types.NewSignatureType( + nil, nil, nil, + types.NewTuple(types.NewParam(token.NoPos, nil, "value", types.Typ[types.Int])), + types.NewTuple(types.NewParam(token.NoPos, nil, "result", types.Typ[types.Int])), + false, + ) + closureCtx := types.NewPointer(types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "seed", types.Typ[types.Int], false), + }, nil)) + hidden := []*types.Var{ + types.NewParam(token.NoPos, nil, "__llgo_g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "__llgo_out", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "__llgo_ctx", closureCtx), + types.NewParam(token.NoPos, nil, "value", types.Typ[types.Int]), + } + physical := types.NewSignatureType( + nil, nil, nil, types.NewTuple(hidden...), + types.NewTuple(types.NewParam(token.NoPos, nil, "handle", types.Typ[types.UnsafePointer])), false, + ) + target := pkg.NewFunc("target$coro", physical, llssa.InC) + targetBody := target.MakeBody(1) + targetBody.Return(prog.Nil(prog.VoidPtr())) + targetBody.EndBuild() + targetBody.Dispose() + resultSlot := types.NewStruct([]*types.Var{ + types.NewField(token.NoPos, nil, "r0", types.Typ[types.Int], false), + }, nil) + abi := coroPlainDispatchABI{signature: logical, resultSlotType: resultSlot} + ctx := &context{prog: prog, pkg: pkg} + thunkName := coroCoroDispatchThunkPrefix + "captured" + thunkExpr := ctx.newCoroDynamicDispatchEntryThunk(thunkName, target.Expr, abi, coro.EmitCoroutine, closureCtx) + descriptorName := coroPlainDispatchDescriptorPrefix + "captured" + pkg.NewCoroDispatchDescriptor(descriptorName, llssa.CoroDispatchDescriptorOptions{ + Version: llssa.CoroDispatchVersionV1, + Flags: llssa.CoroDispatchFlagHasCoro, + Signature: logical, + CoroEntry: thunkExpr, + Result: prog.Type(resultSlot, llssa.InC), + }) + + module := pkg.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify captured coroutine descriptor producer: %v\n%s", err, module.String()) + } + global := module.NamedGlobal(descriptorName) + if global.IsNil() { + t.Fatal("captured coroutine descriptor global is absent") + } + if got := global.Initializer().Operand(1).ZExtValue(); got != uint64(llssa.CoroDispatchFlagHasCoro) { + t.Fatalf("captured coroutine descriptor flags = %#x, want HasCoro without NoCapture", got) + } + thunk := module.NamedFunction(thunkName) + call := coroDispatchProducerOnlyCallTo(t, thunk, target.Name()) + if got := call.OperandsCount() - 1; got != 4 { + t.Fatalf("captured coroutine thunk target arguments = %d, want g+out+ctx+source argument", got) + } + for i := 0; i < 4; i++ { + if call.Operand(i).C != thunk.Param(i).C { + t.Fatalf("captured coroutine thunk target argument %d does not preserve descriptor (g,out,env,arg) order:\n%s", i, module.String()) + } + } +} + +func TestCoroDynamicDispatchProducerAcceptsMultiTargetScalarValue(t *testing.T) { + const source = `package foo +func A(value int) int { return value + 1 } +func B(value int) int { return value + 2 } +func Root(which bool) func(int) int { + var fn func(int) int + if which { fn = A } else { fn = B } + return fn +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + foundMultiTarget := false + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + value, ok := instruction.(ssa.Value) + if !ok { + continue + } + valuePlan, planned := plan.ValuePlan(value) + if planned && len(valuePlan.Funcs) == 1 && valuePlan.Funcs[0].Rep == coro.Dispatch && len(valuePlan.Funcs[0].Targets) == 2 { + foundMultiTarget = true + } + } + } + if !foundMultiTarget { + t.Fatal("Root has no scalar Dispatch value carrying both A and B") + } + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + }}, + ) + if err != nil { + t.Fatalf("compile multi-target descriptor producer: %v", err) + } + if err := llvm.VerifyModule(compiled.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify multi-target descriptor producer: %v\n%s", err, compiled.Module().String()) + } + descriptors := 0 + for global := compiled.Module().FirstGlobal(); !global.IsNil(); global = llvm.NextGlobal(global) { + if strings.HasPrefix(global.Name(), coroPlainDispatchDescriptorPrefix) { + descriptors++ + } + } + if descriptors != 2 { + t.Fatalf("multi-target descriptor globals = %d, want one each for A and B\n%s", descriptors, compiled.Module().String()) + } +} + +func TestCoroDynamicDispatchProducerUsesTypedMultiResultABI(t *testing.T) { + const source = `package foo +func Target(fd int, data []byte) (int, error) { return fd + len(data), nil } +func Root() func(int, []byte) (int, error) { return Target } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + target := ssaPkg.Func("Target") + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.FuncRep != coro.Dispatch || targetPlan.Emission != coro.EmitPlain { + t.Fatalf("multi-result Target plan = %+v, present=%t; want plain Dispatch producer", targetPlan, ok) + } + if err := validateCoroDynamicDispatchTarget(target, targetPlan); err != nil { + t.Fatalf("multi-result slice/error descriptor target rejected: %v", err) + } + + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + }}, + ) + if err != nil { + t.Fatalf("compile multi-result descriptor producer: %v", err) + } + module := compiled.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify multi-result descriptor producer: %v\n%s", err, module.String()) + } + descriptor := coroDispatchProducerOnlyGlobalWithPrefix(t, module, coroPlainDispatchDescriptorPrefix) + if got := descriptor.Initializer().Operand(1).ZExtValue(); got != uint64(llssa.CoroDispatchFlagHasPlain|llssa.CoroDispatchFlagNoCapture) { + t.Fatalf("multi-result descriptor flags = %#x, want HasPlain|NoCapture", got) + } + if descriptor.Initializer().Operand(6).ZExtValue() == 0 || descriptor.Initializer().Operand(7).ZExtValue() == 0 { + t.Fatal("multi-result descriptor did not publish its typed result-slot layout") + } + thunk := coroDispatchProducerOnlyFunctionWithPrefix(t, module, coroPlainDispatchThunkPrefix) + call := coroDispatchProducerOnlyCallTo(t, thunk, "") + if call.Type().TypeKind() != llvm.StructTypeKind { + t.Fatalf("multi-result thunk target call type = %v, want tuple struct", call.Type().TypeKind()) + } +} + +func coroDispatchProducerOnlyGlobalWithPrefix(t *testing.T, module llvm.Module, prefix string) llvm.Value { + t.Helper() + var found llvm.Value + for global := module.FirstGlobal(); !global.IsNil(); global = llvm.NextGlobal(global) { + if !strings.HasPrefix(global.Name(), prefix) { + continue + } + if !found.IsNil() { + t.Fatalf("multiple globals with prefix %q", prefix) + } + found = global + } + if found.IsNil() { + t.Fatalf("no global with prefix %q", prefix) + } + return found +} + +func coroDispatchProducerOnlyFunctionWithPrefix(t *testing.T, module llvm.Module, prefix string) llvm.Value { + t.Helper() + var found llvm.Value + for function := module.FirstFunction(); !function.IsNil(); function = llvm.NextFunction(function) { + if !strings.HasPrefix(function.Name(), prefix) { + continue + } + if !found.IsNil() { + t.Fatalf("multiple functions with prefix %q", prefix) + } + found = function + } + if found.IsNil() { + t.Fatalf("no function with prefix %q", prefix) + } + return found +} + +func coroDispatchProducerOnlyCallTo(t *testing.T, function llvm.Value, targetName string) llvm.Value { + t.Helper() + var found llvm.Value + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || targetName != "" && instruction.CalledValue().Name() != targetName { + continue + } + if !found.IsNil() { + t.Fatalf("function %q has multiple matching calls to %q", function.Name(), targetName) + } + found = instruction + } + } + if found.IsNil() { + t.Fatalf("function %q has no matching call to %q", function.Name(), targetName) + } + return found +} diff --git a/cl/coro_dispatch_test.go b/cl/coro_dispatch_test.go new file mode 100644 index 0000000000..d09d7f31ab --- /dev/null +++ b/cl/coro_dispatch_test.go @@ -0,0 +1,322 @@ +//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/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroDispatchNamedInterfaceLayoutIgnoresEquivalentMethodGraphIdentity(t *testing.T) { + prog := newLLSSAProg(t) + defer prog.Dispose() + newType := func(shadowReceiver bool) *types.Named { + pkg := types.NewPackage("internal/reflectlite", "reflectlite") + name := types.NewTypeName(token.NoPos, pkg, "Type", nil) + named := types.NewNamed(name, types.NewInterfaceType(nil, nil).Complete(), nil) + if previous := pkg.Scope().Insert(name); previous != nil { + t.Fatalf("insert package Type = %v", previous) + } + receiverType := types.Type(named) + if shadowReceiver { + shadowName := types.NewTypeName(token.NoPos, pkg, "Type", nil) + receiverType = types.NewNamed(shadowName, types.NewInterfaceType(nil, nil).Complete(), nil) + } + methodSignature := types.NewSignatureType( + types.NewVar(token.NoPos, pkg, "recv", receiverType), nil, nil, + types.NewTuple(), + types.NewTuple(types.NewVar(token.NoPos, pkg, "", named)), + false, + ) + method := types.NewFunc(token.NoPos, pkg, "Elem", methodSignature) + named.SetUnderlying(types.NewInterfaceType([]*types.Func{method}, nil).Complete()) + return named + } + var shared, structurallyEqual strings.Builder + if err := appendCoroPlainDispatchTypeLayout(&shared, prog, "result", newType(false), nil, make(map[types.Type]bool)); err != nil { + t.Fatal(err) + } + if err := appendCoroPlainDispatchTypeLayout(&structurallyEqual, prog, "result", newType(true), nil, make(map[types.Type]bool)); err != nil { + t.Fatal(err) + } + if shared.String() != structurallyEqual.String() { + t.Fatalf("named interface layout depends on recursive method object identity:\n%s\n!=\n%s", shared.String(), structurallyEqual.String()) + } +} + +func TestCoroPlainDispatchCompilesClosedSingletonFunctionValue(t *testing.T) { + const source = `package foo + +func Target(value int) int { return value + 1 } + +func Apply(fn func(int) int, value int) int { + if fn == nil { + return 0 + } + return fn(value) +} + +func Root() int { return Apply(Target, 41) } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + target := ssaPkg.Func("Target") + apply := ssaPkg.Func("Apply") + dynamicCall := coroPlainDispatchOnlyDynamicCall(t, apply) + hashContext := &context{ + prog: prog, + goProg: ssaPkg.Prog, + goTyps: ssaPkg.Pkg, + goPkg: ssaPkg, + emissionUniverse: universe, + } + targetABI, err := newCoroPlainDispatchABI(hashContext, target.Signature) + if err != nil { + t.Fatal(err) + } + callABI, err := newCoroPlainDispatchABI(hashContext, dynamicCall.Common().Signature()) + if err != nil { + t.Fatal(err) + } + if targetABI.hash != callABI.hash { + t.Fatalf("target ABI hash %x differs from name-less call signature hash %x", targetABI.hash, callABI.hash) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: ssaPkg.Func("Root"), Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyClosedDynamicCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.SSAClosedDynamicCallCertificate, bool, error) { + if call != dynamicCall { + return coro.SSAClosedDynamicCallCertificate{}, false, nil + } + return coro.SSAClosedDynamicCallCertificate{Targets: []*ssa.Function{target}, MayBeNil: true, SyncDispatch: true}, true, nil + }, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.FuncRep != coro.Dispatch || targetPlan.Emission != coro.EmitPlain || targetPlan.Primary != coro.PrimaryPlain || targetPlan.Effect != coro.NoSuspend { + t.Fatalf("Target plan = %+v, present=%t; want one descriptor-backed plain body", targetPlan, ok) + } + callPlan, ok := plan.CallPlan(dynamicCall) + if !ok || callPlan.Rep != coro.Dispatch || callPlan.Open || !callPlan.SyncDispatch || !callPlan.MayBeNil || + len(callPlan.Targets) != 1 || callPlan.Targets[0] != targetPlan.ID { + t.Fatalf("Apply dynamic CallPlan = %+v, present=%t; want closed synchronous nullable singleton Dispatch", callPlan, ok) + } + + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + }}, + ) + if err != nil { + t.Fatalf("compile plain dispatch package: %v", err) + } + module := compiled.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify plain dispatch module: %v\n%s", err, module.String()) + } + ir := module.String() + for _, marker := range []string{ + coroPlainDispatchDescriptorPrefix, + coroPlainDispatchThunkPrefix, + "llvm.trap", + "AssertNilDeref", + "coro.dispatch.result.size.invalid", + "coro.dispatch.result.align.invalid", + } { + if !strings.Contains(ir, marker) { + t.Fatalf("plain dispatch IR is missing %q:\n%s", marker, ir) + } + } + if strings.Contains(ir, coroPrimarySuffix) { + t.Fatalf("plain descriptor unexpectedly emitted a second coroutine body:\n%s", ir) + } + if got := strings.Count(ir, "define i64 @foo.Target("); got != 1 { + t.Fatalf("Target plain body definitions = %d, want exactly one:\n%s", got, ir) + } +} + +func TestCoroPlainDispatchGateAndTargetShapeFailClosed(t *testing.T) { + pkg, plan := buildCoroEntryTestPlan(t) + boxedPlan, ok := plan.FunctionPlan(pkg.Func("Boxed")) + if !ok || boxedPlan.FuncRep != coro.Dispatch { + t.Fatalf("Boxed plan = %+v, present=%t", boxedPlan, ok) + } + entry := plannedFunctionSymbol{function: pkg.Func("Boxed"), plan: boxedPlan, planned: true, coroPlan: plan} + if err := entry.checkSupported(); err == nil || !strings.Contains(err.Error(), "unimplemented dispatch descriptor") { + t.Fatalf("gate-off dispatch error = %v", err) + } + entry.plainDispatch = true + if err := entry.checkSupported(); err != nil { + t.Fatalf("gate-on plain target rejected: %v", err) + } + + badSignatures := []struct { + name string + src string + want string + }{ + {"multiple results", "func Bad() (int, int) { return 1, 2 }", "multiple results"}, + {"aggregate parameter", "func Bad(value string) { _ = value }", "not a supported scalar"}, + {"variadic", "func Bad(values ...int) { _ = values }", "variadic"}, + {"nested function", "func Bad(value func()) { _ = value }", "not a supported scalar"}, + } + for _, test := range badSignatures { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, "package foo\n"+test.src) + fn := ssaPkg.Func("Bad") + plan := coro.FunctionPlan{ + ID: "bad", + Effect: coro.NoSuspend, + Emission: coro.EmitPlain, + FuncRep: coro.Dispatch, + External: coro.Defined, + Primary: coro.PrimaryPlain, + } + err := validateCoroPlainDispatchTarget(fn, plan) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("target validation error = %v, want substring %q", err, test.want) + } + }) + } +} + +func TestCoroPlainDispatchCompilesZeroBindingClosure(t *testing.T) { + const source = `package foo + +func Apply(fn func(int) int, value int) int { return fn(value) } + +func Root() int { + fn := func(value int) int { return value + 2 } + return Apply(fn, 40) +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + if len(root.AnonFuncs) != 1 || len(root.AnonFuncs[0].FreeVars) != 0 { + t.Fatalf("Root anonymous functions = %+v, want one zero-binding closure", root.AnonFuncs) + } + target := root.AnonFuncs[0] + apply := ssaPkg.Func("Apply") + dynamicCall := coroPlainDispatchOnlyDynamicCall(t, apply) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyClosedDynamicCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.SSAClosedDynamicCallCertificate, bool, error) { + if call == dynamicCall { + return coro.SSAClosedDynamicCallCertificate{Targets: []*ssa.Function{target}, SyncDispatch: true}, true, nil + } + return coro.SSAClosedDynamicCallCertificate{}, false, nil + }, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.FuncRep != coro.Dispatch || targetPlan.Emission != coro.EmitPlain { + t.Fatalf("zero-binding target plan = %+v, present=%t", targetPlan, ok) + } + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + }}, + ) + if err != nil { + t.Fatalf("compile zero-binding descriptor closure: %v", err) + } + if err := llvm.VerifyModule(compiled.Module(), llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify zero-binding descriptor closure: %v\n%s", err, compiled.Module().String()) + } + ir := compiled.Module().String() + if !strings.Contains(ir, coroPlainDispatchDescriptorPrefix) || !strings.Contains(ir, coroPlainDispatchThunkPrefix) || strings.Contains(ir, coroPrimarySuffix) { + t.Fatalf("zero-binding closure did not use one plain descriptor body:\n%s", ir) + } +} + +func coroPlainDispatchOnlyDynamicCall(t *testing.T, fn *ssa.Function) ssa.CallInstruction { + t.Helper() + var found ssa.CallInstruction + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + call, ok := instr.(ssa.CallInstruction) + if !ok || call.Common() == nil || call.Common().StaticCallee() != nil { + continue + } + if found != nil { + t.Fatalf("function %q has multiple dynamic calls", fn.Name()) + } + found = call + } + } + if found == nil { + t.Fatalf("function %q has no dynamic call", fn.Name()) + } + return found +} diff --git a/cl/coro_dynamic_await.go b/cl/coro_dynamic_await.go new file mode 100644 index 0000000000..f9b09925f4 --- /dev/null +++ b/cl/coro_dynamic_await.go @@ -0,0 +1,207 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// compileCoroManagedDispatchAwait lowers an open Go function-value call +// carried by the universal {descriptor, environment} representation. The +// descriptor publishes exactly the capability of its one primary body: +// bounded plain targets execute inline, while coroutine targets enter the +// same scheduler-owned child transaction as an exact static await. +func (p *context) compileCoroManagedDispatchAwait( + b llssa.Builder, call *ssa.Call, instructionPlan coroPhysicalInstructionPlan, +) llssa.Expr { + if !p.hasCoroPhysicalBody() || call == nil || instructionPlan.control != coroPhysicalControlDispatchAwait { + panic("coroutine managed dispatch await escaped its frozen physical control recipe") + } + + p.recordCallerLocationForCall(b, &call.Call) + p.emitPCLineLabel(b, call.Pos()) + // Evaluate the callee before arguments and every argument left-to-right, + // before probing capabilities or publishing scheduler state. + fn := p.compileValue(b, call.Call.Value) + closure, ok := types.Unalias(fn.RawType()).Underlying().(*types.Struct) + if !ok || !llssa.IsClosure(closure) { + owner := "" + if p.goFn != nil { + owner = p.goFn.String() + } + panic(fmt.Errorf( + "coroutine managed dispatch await: function %q call %q lowered callee %T %q as %s; want the canonical descriptor closure", + owner, call.String(), call.Call.Value, call.Call.Value.String(), fn.RawType(), + )) + } + args := p.compileValues(b, call.Call.Args, fnNormal) + keepaliveSlots := p.compileCoroCallKeepaliveSlots(b, call) + return p.compileCoroManagedDispatchAwaitValue(b, fn, args, call.Call.Signature(), keepaliveSlots) +} + +// compileCoroManagedDispatchAwaitValue is the one capability probe and child +// transaction shared by ordinary function descriptors and interface-method +// descriptors. The caller owns source evaluation order and exact transport +// validation before entering this helper. +func (p *context) compileCoroManagedDispatchAwaitValue( + b llssa.Builder, fn llssa.Expr, args []llssa.Expr, signature *types.Signature, keepaliveSlots []llssa.Expr, +) llssa.Expr { + return p.compileCoroManagedDispatchAwaitValueWithRecovery(b, fn, args, signature, nil, keepaliveSlots) +} + +// compileCoroManagedDispatchAwaitValueWithRecovery is the cleanup-aware core +// of descriptor dispatch. A deferred coroutine target must be a direct child +// of the owner whose drainer supplied cleanup; introducing a wrapper child +// would break Go's direct-recover rule. Ordinary descriptor calls pass nil and +// retain their existing child-outcome behavior. +func (p *context) compileCoroManagedDispatchAwaitValueWithRecovery( + b llssa.Builder, fn llssa.Expr, args []llssa.Expr, signature *types.Signature, + cleanup *coroStaticCleanupState, keepaliveSlots []llssa.Expr, +) llssa.Expr { + body := p.coroBody() + if body == nil { + panic("managed dispatch await requires an active physical coroutine body") + } + abi, err := newCoroPlainDispatchABI(p, signature) + if err != nil { + panic(fmt.Errorf("coroutine managed dispatch await: %w", err)) + } + resultLayout := p.prog.Type(abi.resultSlotType, llssa.InC) + resultSlot := p.coroFrameAlloca(p.prog.Type(abi.resultSlotType, llssa.InGo)) + opts := llssa.CoroDispatchCallOptions{ + Version: coroPlainDispatchVersion, + ABIHash: abi.hash, + Result: resultLayout, + } + // Descriptor validation would otherwise introduce a hidden + // runtime.AssertNilDeref call after the whole-program helper closure was + // frozen. A physical coroutine owns nil-call semantics directly: route nil + // through its explicit-status fault edge once, then let every descriptor + // operation reuse the proven non-nil word. + descriptorWord := b.Field(fn, 0) + // The descriptor value is deliberately checked here, after a defer record + // has been popped, rather than when the defer statement registers it. This + // preserves Go's rule that invoking a nil deferred function panics while + // running the deferred call. A cleanup-internal nil replaces the current + // panic overlay without replacing its normal/RunDefers/cancellation base. + if cleanup == nil { + p.compileCoroImplicitNilAccessGuard(b, descriptorWord) + } else { + fault := p.fn.MakeBlock() + nonNil := p.fn.MakeBlock() + b.If(b.BinOp(token.EQL, descriptorWord, p.prog.Nil(descriptorWord.Type)), fault, nonNil) + b.SetBlockEx(fault, llssa.AtEnd, false) + cleanup.replaceFault(p, b, coroFaultNilV1) + b.SetBlockContinuation(nonNil) + } + opts.DescriptorNonNil = true + + coroutineBlock := p.fn.MakeBlock() + plainBlock := p.fn.MakeBlock() + join := p.fn.MakeBlock() + b.If(b.CoroDispatchHasCoro(fn, opts), coroutineBlock, plainBlock) + + b.SetBlockEx(coroutineBlock, llssa.AtEnd, false) + child := b.CallCoroDispatchCoro( + fn, + body.task, + b.Convert(p.prog.VoidPtr(), resultSlot), + args, + opts, + ) + p.awaitCoroChildWithRecovery(b, child, resultSlot, abi.signature.Results(), cleanup, keepaliveSlots) + b.Jump(join) + + b.SetBlockEx(plainBlock, llssa.AtEnd, false) + plainResult := b.CallCoroDispatchPlain(fn, args, opts) + p.storeCoroDynamicDispatchResult(b, resultSlot, plainResult, abi.signature.Results()) + b.Jump(join) + + b.SetBlockContinuation(join) + return p.loadCoroAwaitResult(b, resultSlot, abi.signature.Results()) +} + +func validateCoroManagedDispatchAwaitShape( + plan *coro.SSAPlan, owner *ssa.Function, call *ssa.Call, callPlan coro.SSACallPlan, +) error { + fail := func(format string, args ...any) error { + name := "" + if owner != nil { + name = owner.Name() + } + return fmt.Errorf("coroutine managed dispatch await: function %q: %s", name, fmt.Sprintf(format, args...)) + } + if plan == nil || owner == nil || call == nil || call.Common() == nil || call.Parent() != owner { + return fail("requires one exact ordinary call in the compilation plan") + } + ownerPlan, ok := plan.FunctionPlan(owner) + if !ok || ownerPlan.Emission != coro.EmitCoroutine || ownerPlan.Primary != coro.PrimaryCoroutine { + return fail("owner is not one coroutine primary") + } + common := call.Common() + if callPlan.Kind != coro.CallDirect || callPlan.Rep != coro.Dispatch || callPlan.Transport != coro.ManagedTransport || + callPlan.SyncDispatch || callPlan.Open && callPlan.Unresolved != coro.UnknownManagedDispatch || common.StaticCallee() != nil || + common.IsInvoke() || common.Method != nil { + return fail( + "requires an ordinary managed descriptor call (and UnknownManagedDispatch when open), got kind=%v representation=%s open=%t unresolved=%v", + callPlan.Kind, callPlan.Rep, callPlan.Open, callPlan.Unresolved, + ) + } + sig := common.Signature() + if sig == nil || sig.Recv() != nil || sig.Variadic() || + typeParamCount(sig.TypeParams()) != 0 || typeParamCount(sig.RecvTypeParams()) != 0 { + return fail("call signature must be receiver-free, non-variadic, and non-generic") + } + valuePlan, ok := plan.ValuePlan(common.Value) + if !ok || len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 || + valuePlan.Funcs[0].Rep != coro.Dispatch || valuePlan.Funcs[0].Transport != coro.ManagedTransport { + return fail("callee has no exact scalar Dispatch ValuePlan") + } + return nil +} + +func (p *context) storeCoroDynamicDispatchResult( + b llssa.Builder, resultSlot, result llssa.Expr, results *types.Tuple, +) { + count := 0 + if results != nil { + count = results.Len() + } + switch count { + case 0: + return + case 1: + b.Store(b.FieldAddr(resultSlot, 0), result) + default: + for index := 0; index < count; index++ { + b.Store(b.FieldAddr(resultSlot, index), b.Extract(result, index)) + } + } +} + +func typeParamCount(list *types.TypeParamList) int { + if list == nil { + return 0 + } + return list.Len() +} diff --git a/cl/coro_dynamic_await_test.go b/cl/coro_dynamic_await_test.go new file mode 100644 index 0000000000..3f9f53fb7a --- /dev/null +++ b/cl/coro_dynamic_await_test.go @@ -0,0 +1,294 @@ +//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 ( + "regexp" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroManagedDispatchAwaitEmitsCapabilityBranchesAndChildHandoff(t *testing.T) { + const source = `package foo + +func Plain(value int) int { return value + 1 } +func Async(value int) int { return value + 2 } + +func Apply(callback func(int) int, value int) int { + return callback(value) +} +` + for _, test := range []struct { + name string + open bool + }{ + {name: "open managed fallback", open: true}, + {name: "closed coroutine singleton"}, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + apply := ssaPkg.Func("Apply") + plain := ssaPkg.Func("Plain") + async := ssaPkg.Func("Async") + dynamicCall := onlyCoroManagedDispatchValidationCall(t, apply) + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA( + ssaPkg.Prog, + coro.Roots{{Function: apply, Demand: coro.AsyncDemand}}, + coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch fn { + case plain: + return coro.SSAFunctionPolicy{NeedsDispatch: true}, nil + case async: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, NeedsDispatch: true}, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + }, + ClassifyUnknownCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.UnknownTarget, error) { + if test.open && call == dynamicCall { + return coro.UnknownManagedDispatch, nil + } + return coro.UnknownManaged, nil + }, + ClassifyClosedDynamicCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.SSAClosedDynamicCallCertificate, bool, error) { + if !test.open && call == dynamicCall { + return coro.SSAClosedDynamicCallCertificate{Targets: []*ssa.Function{async}}, true, nil + } + return coro.SSAClosedDynamicCallCertificate{}, false, nil + }, + }, + ) + if err != nil { + t.Fatal(err) + } + callPlan, ok := plan.CallPlan(dynamicCall) + if !ok || callPlan.Rep != coro.Dispatch { + t.Fatalf("Apply callback CallPlan = %+v, present=%t; want Dispatch", callPlan, ok) + } + if test.open { + if !callPlan.Open || callPlan.Unresolved != coro.UnknownManagedDispatch { + t.Fatalf("Apply callback CallPlan = %+v; want open managed fallback", callPlan) + } + } else if callPlan.Open || len(callPlan.Targets) != 1 { + t.Fatalf("Apply callback CallPlan = %+v; want one closed coroutine target", callPlan) + } + if !test.open { + functionPlan, present := plan.FunctionPlan(async) + if !present || functionPlan.FuncRep != coro.Dispatch || functionPlan.Emission != coro.EmitCoroutine { + t.Fatalf("Async plan = %+v, present=%t; want coroutine Dispatch target", functionPlan, present) + } + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile managed descriptor await: %v", err) + } + module := compiled.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed descriptor await: %v\n%s", err, module.String()) + } + + applyIR := requireCoroPhysicalFunction(t, module, "foo.Apply").String() + if strings.Contains(applyIR, "AssertNilDeref") || + !strings.Contains(applyIR, "call void @"+coroFaultPrepareHookV1) { + t.Fatalf("Apply did not lower the nullable descriptor through its structured coroutine fault edge:\n%s", applyIR) + } + // The capability probe validates the shared descriptor and branches on the + // HasCoro bit (2) before either capability-specific indirect call. + probe := regexp.MustCompile(`(?s)and i32 [^\n]+, 2.*icmp ne i32 [^\n]+, 0.*br i1`).FindStringIndex(applyIR) + if probe == nil { + t.Fatalf("Apply has no HasCoro capability probe and branch:\n%s", applyIR) + } + plainCall := regexp.MustCompile(`call i64 %[-a-zA-Z$._0-9]+\(ptr [^,]+, i64 [^)]+\)`).FindStringIndex(applyIR) + coroCall := regexp.MustCompile(`call ptr %[-a-zA-Z$._0-9]+\(ptr [^,]+, ptr [^,]+, ptr [^,]+, i64 [^)]+\)`).FindStringIndex(applyIR) + if plainCall == nil || coroCall == nil { + t.Fatalf("Apply is missing plain/coroutine descriptor branches (plain=%v coro=%v):\n%s", plainCall, coroCall, applyIR) + } + if !strings.Contains(applyIR, "@llvm.coro.promise") || + !strings.Contains(applyIR, "call void @"+coroAwaitPrepareHookV1) { + t.Fatalf("Apply coroutine descriptor branch does not enter the shared child-await handoff:\n%s", applyIR) + } + await := strings.Index(applyIR, "call void @"+coroAwaitPrepareHookV1) + if await < coroCall[0] || strings.Index(applyIR[await:], "call i8 @llvm.coro.suspend") < 0 { + t.Fatalf("Apply does not publish and suspend after creating its dynamic child:\n%s", applyIR) + } + if !regexp.MustCompile(`store i64 [^,]+, ptr `).MatchString(applyIR[plainCall[0]:]) { + t.Fatalf("Apply plain branch does not merge its result through the shared result slot:\n%s", applyIR) + } + + runCoroABITestPipeline(t, prog, module) + applyResume := module.NamedFunction("foo.Apply$coro.resume") + if applyResume.IsNil() { + t.Fatalf("CoroSplit did not create managed descriptor await resume:\n%s", module.String()) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed descriptor %s branch after CoroSplit: %v\n%s", test.name, err, module.String()) + } + }) + } +} + +func TestCoroManagedDispatchAwaitClosedMixedCertificateRemainsFailClosed(t *testing.T) { + const source = `package foo +func Plain(value int) int { return value + 1 } +func Async(value int) int { return value + 2 } +func Apply(callback func(int) int, value int) int { return callback(value) } +` + ssaPkg, _, _ := buildGoSSAPkg(t, source) + apply := ssaPkg.Func("Apply") + plain := ssaPkg.Func("Plain") + async := ssaPkg.Func("Async") + dynamicCall := onlyCoroManagedDispatchValidationCall(t, apply) + _, err := coro.AnalyzeSSA( + ssaPkg.Prog, + coro.Roots{{Function: apply, Demand: coro.AsyncDemand}}, + coro.SSAConfig{ + MaxPlainInstructions: -1, + ClassifyClosedDynamicCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.SSAClosedDynamicCallCertificate, bool, error) { + if call == dynamicCall { + return coro.SSAClosedDynamicCallCertificate{Targets: []*ssa.Function{plain, async}}, true, nil + } + return coro.SSAClosedDynamicCallCertificate{}, false, nil + }, + }, + ) + // TODO: replace this negative gate with the same end-to-end IR assertions + // above once whole-program function flow can certify more than one exact + // target. Dynamic codegen is already capability-aware; only the closed-flow + // certificate remains singleton in this slice. + if err == nil || !strings.Contains(err.Error(), "only nil or one exact target is supported") { + t.Fatalf("closed mixed certificate result = %v; want the current singleton fail-closed boundary", err) + } +} + +func TestCoroManagedDispatchAwaitSupportsStdlibAggregateABI(t *testing.T) { + const source = `package foo + +func Apply( + callback func(int, []byte, string, any, *byte) (int, error, string, []byte, any, *byte), + fd int, data []byte, label string, value any, pointer *byte, +) (int, error, string, []byte, any, *byte) { + return callback(fd, data, label, value, pointer) +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + apply := ssaPkg.Func("Apply") + dynamicCall := onlyCoroManagedDispatchValidationCall(t, apply) + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA( + ssaPkg.Prog, + coro.Roots{{Function: apply, Demand: coro.AsyncDemand}}, + coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyUnknownCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.UnknownTarget, error) { + if call == dynamicCall { + return coro.UnknownManagedDispatch, nil + } + return coro.UnknownManaged, nil + }, + }, + ) + if err != nil { + t.Fatal(err) + } + callPlan, ok := plan.CallPlan(dynamicCall) + if !ok || callPlan.Rep != coro.Dispatch || !callPlan.Open || callPlan.Unresolved != coro.UnknownManagedDispatch { + t.Fatalf("aggregate Apply CallPlan = %+v, present=%t; want open managed Dispatch", callPlan, ok) + } + if err := validateCoroManagedDispatchCall(plan, apply, dynamicCall, callPlan); err != nil { + t.Fatalf("aggregate managed descriptor call rejected: %v", err) + } + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, apply, "") + if err != nil { + t.Fatal(err) + } + proof := audit.currentFrameRetentionProof() + if got := strings.Join(rootNames(proof.exactCallKeepaliveRoots(dynamicCall)), ","); got != "data,pointer" { + t.Fatalf("managed descriptor child keepalive roots = %q, want data,pointer", got) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile aggregate managed descriptor await: %v", err) + } + module := compiled.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify aggregate managed descriptor await: %v\n%s", err, module.String()) + } + applyIR := requireCoroPhysicalFunction(t, module, "foo.Apply").String() + if !strings.Contains(applyIR, "call void @"+coroAwaitPrepareHookV1) || + strings.Count(applyIR, "extractvalue") < 6 { + t.Fatalf("aggregate descriptor branches did not hand off child and merge six typed results:\n%s", applyIR) + } + runCoroABITestPipeline(t, prog, module) +} diff --git a/cl/coro_dynamic_implements.go b/cl/coro_dynamic_implements.go new file mode 100644 index 0000000000..627024be23 --- /dev/null +++ b/cl/coro_dynamic_implements.go @@ -0,0 +1,116 @@ +/* + * 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 ( + "fmt" + "go/types" +) + +// CoroDynamicImplements evaluates restricted CHA against the same effective +// patched type graph used by code generation. Raw Go SSA retains the original +// invoke interface while a replacement package contributes method receivers +// from its alternate types package; comparing those raw graphs would silently +// produce an empty closed-world target set. +func (u *EmissionUniverse) CoroDynamicImplements(candidate types.Type, iface *types.Interface) (bool, error) { + if u == nil { + return false, fmt.Errorf("coroutine dynamic implementation relation: nil emission universe") + } + if candidate == nil || iface == nil { + return false, fmt.Errorf("coroutine dynamic implementation relation requires candidate and interface types") + } + owner := u.coroDynamicTypeOwner(candidate) + if owner == nil { + owner = u.coroDynamicTypeOwner(iface) + } + if owner == nil { + // Types with no package-owned named edge cannot participate in package + // replacement. Preserve the ordinary exact go/types relation. + return types.Implements(candidate, iface), nil + } + effectiveCandidate := u.effectiveType(owner, nil, candidate) + effectiveInterfaceType := types.Type(iface) + if namedInterface, found, err := u.coroExactNamedInterface(owner, iface); err != nil { + return false, err + } else if found { + effectiveInterfaceType = namedInterface + } else { + effectiveInterfaceType = u.effectiveType(owner, nil, iface) + } + effectiveInterface, ok := types.Unalias(effectiveInterfaceType).Underlying().(*types.Interface) + if !ok { + return false, fmt.Errorf("coroutine dynamic implementation relation: effective invoke type is %T, not an interface", effectiveInterfaceType) + } + effectiveInterface.Complete() + return types.Implements(effectiveCandidate, effectiveInterface), nil +} + +// coroExactNamedInterface recovers the package-level named interface whose +// exact raw Underlying pointer was placed in an SSA invoke. Recovering the +// named edge matters for unexported methods: rebuilding only the anonymous +// interface shape would retain the original method package identity, while +// the replacement receiver correctly carries the alternate package identity. +func (u *EmissionUniverse) coroExactNamedInterface(owner *preparedEmissionPackage, iface *types.Interface) (types.Type, bool, error) { + if u == nil || owner == nil || owner.oldTypes == nil || iface == nil || owner.oldTypes.Scope() == nil { + return nil, false, nil + } + var replacement types.Type + for _, name := range owner.oldTypes.Scope().Names() { + object, ok := owner.oldTypes.Scope().Lookup(name).(*types.TypeName) + if !ok || types.Unalias(object.Type()).Underlying() != iface { + continue + } + candidate := u.effectiveType(owner, nil, object.Type()) + if _, ok := types.Unalias(candidate).Underlying().(*types.Interface); !ok { + return nil, false, fmt.Errorf("coroutine dynamic implementation relation: effective named invoke type %q is %T, not an interface", name, candidate) + } + if replacement != nil && !types.Identical(replacement, candidate) { + return nil, false, fmt.Errorf("coroutine dynamic implementation relation: raw interface has conflicting effective named owners") + } + replacement = candidate + } + return replacement, replacement != nil, nil +} + +func (u *EmissionUniverse) coroDynamicTypeOwner(typ types.Type) *preparedEmissionPackage { + if u == nil || typ == nil { + return nil + } + switch typ := types.Unalias(typ).(type) { + case *types.Pointer: + return u.coroDynamicTypeOwner(typ.Elem()) + case *types.Named: + if object := typ.Obj(); object != nil { + return u.ownerOfTypes(object.Pkg()) + } + case *types.Interface: + typ.Complete() + for index := 0; index < typ.NumExplicitMethods(); index++ { + if method := typ.ExplicitMethod(index); method != nil { + if owner := u.ownerOfTypes(method.Pkg()); owner != nil { + return owner + } + } + } + for index := 0; index < typ.NumEmbeddeds(); index++ { + if owner := u.coroDynamicTypeOwner(typ.EmbeddedType(index)); owner != nil { + return owner + } + } + } + return nil +} diff --git a/cl/coro_dynamic_implements_test.go b/cl/coro_dynamic_implements_test.go new file mode 100644 index 0000000000..464ea998f1 --- /dev/null +++ b/cl/coro_dynamic_implements_test.go @@ -0,0 +1,95 @@ +//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/ast" + "go/types" + "testing" + + "github.com/goplus/llgo/internal/typepatch" + "github.com/goplus/llgo/ssa/abi" + "github.com/goplus/llgo/ssa/ssatest" + "golang.org/x/tools/go/ssa" +) + +func TestCoroDynamicImplementsUsesEffectivePatchedTypes(t *testing.T) { + testProg := newEmissionTestProgram() + original := testProg.addPackage(t, "example.com/emission/p", `package p +type Type interface { Elem() Type; hidden() int } +func Invoke(value Type) Type { return value.Elem() } +`) + alt := testProg.addPackage(t, abi.PatchPathPrefix+"example.com/emission/p", `package p +type Type interface { Elem() Type; hidden() int } +type rtype struct{} +func (rtype) Elem() Type { return nil } +func (rtype) hidden() int { return 0 } +func Materialize() Type { return rtype{} } +`) + testProg.ssa.Build() + prog := ssatest.NewProgram(t, nil) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, Patches{ + "example.com/emission/p": {Alt: alt.ssa, Types: typepatch.Clone(alt.types)}, + }, []EmissionPackage{{ + SSA: original.ssa, Files: []*ast.File{original.file, alt.file}, + }}) + if err != nil { + t.Fatal(err) + } + + invoke := original.ssa.Func("Invoke") + var invokeCall *ssa.Call + for _, block := range invoke.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Common().IsInvoke() { + invokeCall = call + } + } + } + if invokeCall == nil { + t.Fatal("fixture has no interface invoke") + } + iface, ok := invokeCall.Common().Value.Type().Underlying().(*types.Interface) + if !ok { + t.Fatalf("invoke receiver type = %T; want interface", invokeCall.Common().Value.Type().Underlying()) + } + var method *ssa.Function + for _, function := range universe.Functions() { + if function != nil && function.Name() == "Elem" && function.Signature.Recv() != nil && function.Pkg == alt.ssa { + method = function + break + } + } + if method == nil { + t.Fatal("alternate rtype.Elem method is absent from frozen emission universe") + } + receiver := method.Signature.Recv().Type() + if types.Implements(receiver, iface) { + t.Fatal("fixture raw alternate receiver unexpectedly implements original invoke interface") + } + implements, err := universe.CoroDynamicImplements(receiver, iface) + if err != nil { + t.Fatal(err) + } + if !implements { + t.Fatal("effective alternate receiver does not implement effective patched interface") + } +} diff --git a/cl/coro_emission_session.go b/cl/coro_emission_session.go new file mode 100644 index 0000000000..bdc2a3d2d2 --- /dev/null +++ b/cl/coro_emission_session.go @@ -0,0 +1,172 @@ +/* + * 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 ( + "fmt" + + llssa "github.com/goplus/llgo/ssa" +) + +// coroPhysicalEmissionPhase makes installation of a physical body an explicit +// transaction. Prologue callbacks may consume the frozen plan, but ordinary +// source lowering cannot observe the body or source-block map until both have +// been bound atomically. +type coroPhysicalEmissionPhase uint8 + +const ( + coroPhysicalEmissionPrologue coroPhysicalEmissionPhase = iota + 1 + coroPhysicalEmissionBody + coroPhysicalEmissionComplete +) + +// coroPhysicalEmissionSession is the sole mutable owner of state that exists +// only while one physical coroutine body is emitted. Keeping the plan, body, +// nested site ledger, source-block projection, and physical parameter layout in +// one session prevents independently installed context fields from describing +// different functions after a failed or nested emission. +type coroPhysicalEmissionSession struct { + phase coroPhysicalEmissionPhase + plan *coroPhysicalFunctionPlan + body *coroBodyContext + site *coroSiteEmissionObserver + sourceBlocks []llssa.BasicBlock + sourceParamBase int + explicitStatus bool +} + +// beginCoroPhysicalEmission installs the complete prologue-visible portion of +// a session in one operation. Physical emission is deliberately non-nestable: +// deferred function bodies use their own later initializer and must not borrow +// the caller's body, plan, or site observer. +func (p *context) beginCoroPhysicalEmission( + plan *coroPhysicalFunctionPlan, + sourceParamBase int, + explicitStatus bool, +) (*coroPhysicalEmissionSession, func()) { + if p == nil || plan == nil || sourceParamBase < 2 { + panic("coroutine physical emission requires a context, frozen plan, and physical parameter base") + } + if p.coroEmission != nil { + panic("nested coroutine physical emission session") + } + session := &coroPhysicalEmissionSession{ + phase: coroPhysicalEmissionPrologue, + plan: plan, + sourceParamBase: sourceParamBase, + explicitStatus: explicitStatus, + } + p.coroEmission = session + return session, func() { + recovered := recover() + if p.coroEmission != session { + panic("coroutine physical emission session ownership changed before close") + } + p.coroEmission = nil + if recovered != nil { + panic(recovered) + } + if session.site != nil { + panic("coroutine physical emission closed with an active source SitePlan observer") + } + if session.phase != coroPhysicalEmissionComplete { + panic(fmt.Sprintf("coroutine physical emission closed in phase %d", session.phase)) + } + } +} + +// bindCoroPhysicalBody publishes the body and source-block projection together. +// No ordinary lowering consumer can observe one without the other. +func (s *coroPhysicalEmissionSession) bindCoroPhysicalBody( + body *coroBodyContext, + sourceBlocks []llssa.BasicBlock, +) { + if s == nil || s.phase != coroPhysicalEmissionPrologue || s.plan == nil || s.body != nil || body == nil || len(sourceBlocks) == 0 { + panic("coroutine physical body may be bound exactly once after a complete prologue") + } + s.body = body + s.sourceBlocks = sourceBlocks + s.phase = coroPhysicalEmissionBody +} + +func (s *coroPhysicalEmissionSession) completeCoroPhysicalBody(body *coroBodyContext) { + if s == nil || s.phase != coroPhysicalEmissionBody || s.body == nil || s.body != body || s.site != nil { + panic("coroutine physical body may complete exactly once with no active source SitePlan observer") + } + s.phase = coroPhysicalEmissionComplete +} + +// coroBody is intentionally available only to coroutine-specific lowering +// modules. The ordinary SSA compiler uses semantic adapter methods instead of +// reading physical state directly. +func (p *context) coroBody() *coroBodyContext { + if p == nil || p.coroEmission == nil || p.coroEmission.phase != coroPhysicalEmissionBody { + return nil + } + return p.coroEmission.body +} + +func (p *context) hasCoroPhysicalBody() bool { + return p.coroBody() != nil +} + +func (p *context) hasCoroPhysicalEmission() bool { + return p != nil && p.coroEmission != nil +} + +func (p *context) coroEmissionPlan() *coroPhysicalFunctionPlan { + if p == nil || p.coroEmission == nil { + return nil + } + return p.coroEmission.plan +} + +func (p *context) coroEmissionExplicitStatus() bool { + return p != nil && p.coroEmission != nil && p.coroEmission.explicitStatus +} + +func (p *context) coroEmissionSourceParamBase() int { + if p == nil || p.coroEmission == nil { + return 0 + } + return p.coroEmission.sourceParamBase +} + +func (p *context) coroEmissionSourceBlock(index int) (llssa.BasicBlock, bool) { + if p == nil || p.coroEmission == nil || p.coroEmission.phase != coroPhysicalEmissionBody { + return nil, false + } + blocks := p.coroEmission.sourceBlocks + if index < 0 || index >= len(blocks) { + panic(fmt.Sprintf("source basic block index %d is outside coroutine map of length %d", index, len(blocks))) + } + return blocks[index], true +} + +func (p *context) coroEmissionSite() *coroSiteEmissionObserver { + if p == nil || p.coroEmission == nil { + return nil + } + return p.coroEmission.site +} + +func (p *context) setCoroEmissionSite(site *coroSiteEmissionObserver) { + if p == nil || p.coroEmission == nil { + panic("coroutine source SitePlan observer escaped its physical emission session") + } + p.coroEmission.site = site +} diff --git a/cl/coro_emission_session_test.go b/cl/coro_emission_session_test.go new file mode 100644 index 0000000000..0672024c17 --- /dev/null +++ b/cl/coro_emission_session_test.go @@ -0,0 +1,119 @@ +/* + * 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 ( + "fmt" + "strings" + "testing" + + llssa "github.com/goplus/llgo/ssa" +) + +func TestCoroPhysicalEmissionSessionCommitsOneCompleteBody(t *testing.T) { + ctx := &context{} + plan := &coroPhysicalFunctionPlan{} + session, finish := ctx.beginCoroPhysicalEmission(plan, 3, true) + if !ctx.hasCoroPhysicalEmission() || ctx.hasCoroPhysicalBody() { + t.Fatal("prologue must expose the session but not a partial physical body") + } + if got := ctx.coroEmissionPlan(); got != plan { + t.Fatalf("prologue plan = %p, want %p", got, plan) + } + if got := ctx.coroEmissionSourceParamBase(); got != 3 { + t.Fatalf("physical parameter base = %d, want 3", got) + } + if !ctx.coroEmissionExplicitStatus() { + t.Fatal("explicit-status capability was not frozen in the session") + } + + body := &coroBodyContext{} + blocks := make([]llssa.BasicBlock, 1) + session.bindCoroPhysicalBody(body, blocks) + if got := ctx.coroBody(); got != body { + t.Fatalf("bound body = %p, want %p", got, body) + } + if _, ok := ctx.coroEmissionSourceBlock(0); !ok { + t.Fatal("bound source-block projection is not visible with the body") + } + if message := captureCoroEmissionSessionPanic(func() { + session.bindCoroPhysicalBody(&coroBodyContext{}, blocks) + }); !strings.Contains(message, "exactly once") { + t.Fatalf("second body bind panic = %q", message) + } + + session.site = &coroSiteEmissionObserver{} + if message := captureCoroEmissionSessionPanic(func() { + session.completeCoroPhysicalBody(body) + }); !strings.Contains(message, "no active source SitePlan") { + t.Fatalf("completion with active SitePlan panic = %q", message) + } + session.site = nil + session.completeCoroPhysicalBody(body) + if ctx.hasCoroPhysicalBody() { + t.Fatal("completed body remained available to ordinary lowering") + } + finish() + if ctx.hasCoroPhysicalEmission() { + t.Fatal("completed session remained installed") + } +} + +func TestCoroPhysicalEmissionSessionRejectsPartialAndNestedState(t *testing.T) { + ctx := &context{} + plan := &coroPhysicalFunctionPlan{} + session, finish := ctx.beginCoroPhysicalEmission(plan, 2, false) + if message := captureCoroEmissionSessionPanic(func() { + ctx.beginCoroPhysicalEmission(plan, 2, false) + }); !strings.Contains(message, "nested") { + t.Fatalf("nested session panic = %q", message) + } + if message := captureCoroEmissionSessionPanic(finish); !strings.Contains(message, "closed in phase") { + t.Fatalf("partial close panic = %q", message) + } + if ctx.hasCoroPhysicalEmission() { + t.Fatal("failed partial close did not clear the installed session") + } + if session.phase != coroPhysicalEmissionPrologue { + t.Fatalf("failed partial session phase = %d, want prologue", session.phase) + } +} + +func TestCoroPhysicalEmissionSessionPreservesEmissionPanicAndClearsState(t *testing.T) { + ctx := &context{} + message := captureCoroEmissionSessionPanic(func() { + _, finish := ctx.beginCoroPhysicalEmission(&coroPhysicalFunctionPlan{}, 2, false) + defer finish() + panic("sentinel emission failure") + }) + if message != "sentinel emission failure" { + t.Fatalf("emission panic = %q", message) + } + if ctx.hasCoroPhysicalEmission() { + t.Fatal("panicking emission left a partial session installed") + } +} + +func captureCoroEmissionSessionPanic(run func()) (message string) { + defer func() { + if recovered := recover(); recovered != nil { + message = fmt.Sprint(recovered) + } + }() + run() + return "" +} diff --git a/cl/coro_emitter_adapter.go b/cl/coro_emitter_adapter.go new file mode 100644 index 0000000000..85d50da8a7 --- /dev/null +++ b/cl/coro_emitter_adapter.go @@ -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 ( + "fmt" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// This file is the narrow migration boundary between the ordinary SSA +// compiler and physical coroutine emission. Ordinary compile.go/instr.go code +// delegates semantic operations here and never reads coroBodyContext or the +// physical emission session. Each adapter either owns the complete coroutine +// case or reports false so the established plain lowering remains authoritative. + +func (p *context) compileCoroInstructionPrologue(b llssa.Builder, instr ssa.Instruction) bool { + body := p.coroBody() + if body == nil { + return false + } + if _, debug := instr.(*ssa.DebugRef); debug { + p.compileInstr(b, instr) + return true + } + criticalRole := coroCriticalCallNone + criticalDepth := uint32(0) + if body.critical != nil { + var proven bool + criticalDepth, proven = body.critical.beforeDepth[instr] + if !proven { + panic("coroutine critical proof has no instruction input depth") + } + if call, ok := instr.(*ssa.Call); ok { + criticalRole = body.critical.roles[call] + } + } + outerCriticalEnter := criticalRole == coroCriticalCallEnter && criticalDepth == 0 + if criticalDepth == 0 && !outerCriticalEnter { + body.countInstructionAndMaybeYield(b) + } + if !outerCriticalEnter { + body.sourceBlockPollFresh = false + } + return false +} + +func (p *context) compileCoroPatchInitAtBlock(b llssa.Builder) bool { + if !p.hasCoroPhysicalBody() { + return false + } + p.compileCoroPatchInitAwait(b) + return true +} + +func (p *context) coroCurrentSourceCall() *ssa.Call { + observer := p.coroEmissionSite() + if observer == nil { + return nil + } + call, _ := observer.instruction.(*ssa.Call) + return call +} + +// tryCompileCoroPhysicalCall is the sole source-call dispatcher for a physical +// coroutine body. It consumes one frozen instruction plan and reports the +// selected control/operation recipe before delegating operand emission. The +// specialized emitters never re-read CallPlan, feature flags, or raw call +// shape to decide whether they own the site. +func (p *context) tryCompileCoroPhysicalCall(b llssa.Builder, call *ssa.Call) (llssa.Expr, bool) { + if !p.hasCoroPhysicalBody() { + return llssa.Expr{}, false + } + if call == nil { + panic("physical coroutine call dispatcher received a nil source call") + } + instructionPlan, planned := p.plannedCoroPhysicalControl(call) + if !planned { + panic("physical coroutine call has no frozen instruction plan") + } + if instructionPlan.control != coroPhysicalControlNone { + p.observeCoroPhysicalControl(call, instructionPlan.control) + } + switch instructionPlan.control { + case coroPhysicalControlDirectAwait: + return p.compileCoroStaticAwait(b, call, instructionPlan), true + case coroPhysicalControlDispatchAwait: + return p.compileCoroManagedDispatchAwait(b, call, instructionPlan), true + case coroPhysicalControlClosedInterfaceAwait: + return p.compileCoroInterfaceDispatchAwait(b, call, instructionPlan), true + case coroPhysicalControlManagedInterfaceAwait: + return p.compileCoroManagedInterfaceAwait(b, call, instructionPlan), true + case coroPhysicalControlPlainDispatch: + return p.compileCoroPhysicalPlainDispatch(b, call, instructionPlan), true + case coroPhysicalControlNone: + if instructionPlan.operation == coroPhysicalOperationWorkerForeign { + if instructionPlan.operationWorker == nil { + panic("physical coroutine worker call has no frozen foreign shape") + } + p.observeCoroPhysicalOperation(call, instructionPlan.operation) + return p.compileCoroWorkerForeignCall(b, call, *instructionPlan.operationWorker), true + } + return llssa.Expr{}, false + default: + panic(fmt.Sprintf("source call selected incompatible frozen physical control recipe %s", instructionPlan.control)) + } +} + +func (p *context) compileCoroTerminalResultAllocation(allocation *ssa.Alloc) llssa.Expr { + body := p.coroBody() + if body == nil || allocation == nil || !allocation.Heap || allocation.Block() == nil || allocation.Block().Index != 0 { + panic("coroutine terminal-result allocation lost its source-entry heap identity") + } + value := body.terminalResultAllocs[allocation] + if value.IsNil() { + panic("coroutine terminal-result allocation lost its frozen physical storage") + } + return value +} + +func (p *context) compileCoroReturn(b llssa.Builder, results []llssa.Expr) { + body := p.coroBody() + if body == nil { + panic("coroutine return escaped its planned physical body") + } + if body.completion == nil { + panic("coroutine return has no completion block") + } + p.storeCoroLeafResult(b, body.abi, body.resultSlot, results) + b.Jump(body.completion) +} + +func (p *context) compileCoroDefer(b llssa.Builder, instruction *ssa.Defer) { + body := p.coroBody() + if body == nil || body.cleanup == nil { + panic("coroutine defer escaped its frozen cleanup plan") + } + body.cleanup.register(p, b, instruction) +} + +func (p *context) compileCoroRunDefers(b llssa.Builder, instruction *ssa.RunDefers) { + body := p.coroBody() + if body == nil || body.cleanup == nil { + panic("coroutine RunDefers escaped its frozen cleanup plan") + } + body.cleanup.runDefers(b, instruction) +} + +func (p *context) compileCoroSyntheticSelectPanic(b llssa.Builder, instruction *ssa.Panic) { + body := p.coroBody() + if body == nil || instruction == nil { + panic("coroutine select invariant trap escaped its planned physical body") + } + if body.unsupportedRunDecision == nil { + panic("coroutine select invariant panic requires a fail-closed trap block") + } + b.Jump(body.unsupportedRunDecision) +} + +func (p *context) tryCompileCoroFreeVar(b llssa.Builder, fn *ssa.Function, index int) (llssa.Expr, bool) { + if !p.hasCoroPhysicalBody() || len(fn.FreeVars) == 0 { + return llssa.Expr{}, false + } + // Physical captured coroutine entries expose their typed context explicitly + // at (g,out,ctx,...). Do not use Function.FreeVar: that legacy helper + // hard-codes implicit ctx at parameter zero, which is the G word in the + // coroutine ABI. Load per use so the value is dominated in every resumed + // block after CoroSplit. + ctx := b.Load(p.fn.PhysicalParam(2)) + return b.Field(ctx, index), true +} + +func (p *context) coroUsesExplicitStatusFaults() bool { + return p.coroEmissionExplicitStatus() +} diff --git a/cl/coro_entry.go b/cl/coro_entry.go new file mode 100644 index 0000000000..69f1f29daa --- /dev/null +++ b/cl/coro_entry.go @@ -0,0 +1,639 @@ +/* + * 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 ( + "fmt" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const coroPrimarySuffix = "$coro" + +// plannedFunctionSymbol is the single symbol selected for an SSA function. +// Emission selects whether this compilation materializes a body/declaration; +// FuncRep only describes escaped function values and never authorizes a +// second body. +type plannedFunctionSymbol struct { + function *ssa.Function + pkgTypes *types.Package + name string + baseName string + ftype int + plan coro.FunctionPlan + planned bool + physical bool + childAwait bool + programRun bool + channel bool + plainDispatch bool + staticSpawn bool + explicitPanic bool + frameRetentionABI string + coroPlan *coro.SSAPlan + emission *EmissionUniverse + physicalOwner *preparedEmissionPackage + interfacePlain *coroClosedInterfacePlainPlan + managedInterface *coroManagedInterfaceDispatchPlan + patchOriginalInit bool +} + +// resolveFunctionSymbol is shared by function definitions and declarations so +// they cannot independently choose different primary symbols. The physical +// descriptor derives the signature from this exact entry. The zero-value +// compilation and report-only plans deliberately preserve the legacy symbol. +func (p *context) resolveFunctionSymbol(fn *ssa.Function) (plannedFunctionSymbol, error) { + if p.compilation != nil && p.compilation.CoroEntryResolutionActive() && p.compilation.EmissionUniverse != nil { + canonical, ok := p.compilation.EmissionUniverse.Resolve(fn) + if !ok { + _, unresolvedName, _ := p.funcName(fn) + return plannedFunctionSymbol{}, fmt.Errorf("coroutine entry resolution: function %q is absent from the prepared emission universe", unresolvedName) + } + fn = canonical + } + pkgTypes, name, ftype := p.funcName(fn) + if p.compilation != nil && p.compilation.CoroEntryResolutionActive() && p.compilation.EmissionUniverse != nil { + var err error + name, err = p.compilation.EmissionUniverse.physicalName(p.goPkg, fn, name) + if err != nil { + return plannedFunctionSymbol{}, err + } + } + entry := plannedFunctionSymbol{ + function: fn, + pkgTypes: pkgTypes, + name: name, + baseName: name, + ftype: ftype, + } + if ftype != goFunc || p.compilation == nil || !p.compilation.CoroEntryResolutionActive() { + return entry, nil + } + if p.compilation.CoroPlan == nil { + return entry, fmt.Errorf("coroutine entry resolution requires a compilation CoroPlan") + } + plan, ok := p.compilation.CoroPlan.FunctionPlan(fn) + if !ok { + return entry, fmt.Errorf("coroutine entry resolution: function %q is absent from the compilation CoroPlan", name) + } + entry.plan = plan + entry.planned = true + entry.physical = p.compilation.CoroPhysicalABIActive() + entry.childAwait = p.compilation.CoroChildAwaitActive() + entry.programRun = p.compilation.CoroProgramBootstrapActive() + entry.channel = p.compilation.CoroChannelActive() + entry.plainDispatch = p.compilation.CoroPlainDispatchActive() + entry.staticSpawn = p.compilation.CoroClosedStaticSpawnActive() + entry.explicitPanic = p.compilation.CoroExplicitStatusActive() + entry.frameRetentionABI = p.compilation.CoroFrameRetentionABI + entry.coroPlan = p.compilation.CoroPlan + entry.emission = p.compilation.EmissionUniverse + // Symbol resolution can happen in a caller package that merely references + // fn. Its physical proof belongs to fn's body owner; actual multi-owner body + // emission performs its own exact lookup in compileCoroPhysicalBody. + if p.compilation.EmissionUniverse != nil { + entry.physicalOwner = p.compilation.EmissionUniverse.ownerOf(fn) + } + entry.interfacePlain = p.compilation.coroClosedInterfacePlain + entry.managedInterface = p.compilation.coroManagedInterface + ignored := p.compilation.CoroPlan.IgnoresBody(fn) + assemblyCertified := false + if ignored { + if p.compilation.EmissionUniverse == nil { + return entry, fmt.Errorf("coroutine entry resolution: Go-emitted function %q has an ignored SSA body", plan.ID) + } + _, certified, certificateErr := p.compilation.EmissionUniverse.CoroAssemblyNoSuspendCertificate(fn) + if certificateErr != nil { + return entry, certificateErr + } + assemblyCertified = certified + if !assemblyCertified { + return entry, fmt.Errorf("coroutine entry resolution: Go-emitted function %q has an ignored SSA body without a frozen assembly proof", plan.ID) + } + } + if err := validatePlannedFunction(fn, plan, len(fn.Blocks) != 0 && !assemblyCertified); err != nil { + return entry, err + } + if plan.Emission == coro.EmitCoroutine { + entry.name += coroPrimarySuffix + } + return entry, nil +} + +// resolvePatchOriginalInitSymbol selects the private physical role of the +// exact original initializer reached by a compiler-owned patch-init edge. +// Generic function resolution must never infer this role from the function +// pointer alone because source dependency calls to that pointer target the +// public patch initializer instead. +func (p *context) resolvePatchOriginalInitSymbol(fn *ssa.Function) (plannedFunctionSymbol, error) { + entry, err := p.resolveFunctionSymbol(fn) + if err != nil { + return plannedFunctionSymbol{}, err + } + if p.compilation == nil || !p.compilation.CoroEntryResolutionActive() || p.compilation.EmissionUniverse == nil { + return plannedFunctionSymbol{}, fmt.Errorf("coroutine patch original initializer role requires active entry resolution") + } + hidden, err := p.compilation.EmissionUniverse.patchOriginalInitPhysicalName(entry.function) + if err != nil { + return plannedFunctionSymbol{}, err + } + entry.baseName = hidden + entry.name = hidden + if entry.planned && entry.plan.Emission == coro.EmitCoroutine { + entry.name += coroPrimarySuffix + } + entry.patchOriginalInit = true + return entry, nil +} + +func (p *context) mustPatchOriginalInitFunctionSymbol(fn *ssa.Function) plannedFunctionSymbol { + entry, err := p.resolvePatchOriginalInitSymbol(fn) + if err == nil { + err = entry.checkSupported() + } + if err != nil { + panic(err) + } + return entry +} + +func validatePlannedFunction(fn *ssa.Function, plan coro.FunctionPlan, hasEmittedBody bool) error { + if fn == nil { + return fmt.Errorf("coroutine entry resolution: function plan %q has no SSA function", plan.ID) + } + switch plan.Emission { + case coro.EmitNone: + if plan.Demand != coro.NoDemand { + return fmt.Errorf("coroutine entry resolution: non-emitted function %q has demand %s", plan.ID, plan.Demand) + } + return nil + case coro.EmitPlain: + if plan.External != coro.Defined || !hasEmittedBody { + return fmt.Errorf("coroutine entry resolution: plain emission %q has external kind %s and emitted-body=%t", plan.ID, plan.External, hasEmittedBody) + } + case coro.EmitRawPlain: + if plan.External != coro.Defined || !hasEmittedBody || !plan.RawPlainOnly || + plan.ManagedDemand != coro.NoDemand || !plan.RawPlainDemand || + plan.Primary != coro.PrimaryPlain || plan.FuncRep != coro.DirectPlain { + return fmt.Errorf( + "coroutine entry resolution: raw-only emission %q has external=%s emitted-body=%t raw-only=%t managed=%s raw=%t primary=%s representation=%s", + plan.ID, plan.External, hasEmittedBody, plan.RawPlainOnly, plan.ManagedDemand, + plan.RawPlainDemand, plan.Primary, plan.FuncRep, + ) + } + case coro.EmitCoroutine: + if plan.External != coro.Defined || !hasEmittedBody { + return fmt.Errorf("coroutine entry resolution: coroutine emission %q has external kind %s and emitted-body=%t", plan.ID, plan.External, hasEmittedBody) + } + case coro.EmitExternal: + if plan.External == coro.Defined || hasEmittedBody { + return fmt.Errorf("coroutine entry resolution: external emission %q has external kind %s and emitted-body=%t", plan.ID, plan.External, hasEmittedBody) + } + default: + return fmt.Errorf("coroutine entry resolution: function %q has invalid emission kind %d", plan.ID, uint8(plan.Emission)) + } + return nil +} + +func (c *Compilation) plannedFunctionEmittedBody(fn *ssa.Function) (bool, error) { + if c == nil || c.CoroPlan == nil || c.EmissionUniverse == nil || fn == nil { + return false, fmt.Errorf("coroutine entry resolution: cannot classify a nil or unprepared planned function") + } + background, classified, err := c.EmissionUniverse.FunctionBackground(fn) + if err != nil { + return false, fmt.Errorf("coroutine entry resolution: classify frozen frontend ABI for %q: %w", fn.Name(), err) + } + ignored := c.CoroPlan.IgnoresBody(fn) + _, assemblyCertified, assemblyErr := c.EmissionUniverse.CoroAssemblyNoSuspendCertificate(fn) + if assemblyErr != nil { + return false, fmt.Errorf("coroutine entry resolution: classify frozen assembly ABI for %q: %w", fn.Name(), assemblyErr) + } + if assemblyCertified && (!classified || background != llssa.InGo || len(fn.Blocks) != 0) { + return false, fmt.Errorf("coroutine entry resolution: assembly-certified function %q has frontend classified=%t kind=%d body=%t", fn.Name(), classified, background, len(fn.Blocks) != 0) + } + frozenIgnored := classified && background == llssa.InC || assemblyCertified + if ignored != frozenIgnored { + return false, fmt.Errorf("coroutine entry resolution: function %q ignored-body=%t conflicts with frozen frontend background classified=%t kind=%d", fn.Name(), ignored, classified, background) + } + return classified && background == llssa.InGo && len(fn.Blocks) != 0, nil +} + +// omitUnemittedFunction is used only by eager package/type/closure +// enumeration. A real body reference must go through mustFunctionSymbol and +// fail closed instead of silently turning an EmitNone decision into an LLVM +// declaration. +func (p *context) omitUnemittedFunction(fn *ssa.Function) bool { + if p.compilation != nil && p.compilation.CoroEntryResolutionActive() && p.compilation.EmissionUniverse != nil { + canonical, ok := p.compilation.EmissionUniverse.Resolve(fn) + if !ok || canonical == nil { + panic(fmt.Errorf("coroutine eager emission: function %q is absent from the prepared emission universe", fn.Name())) + } + if canonical != fn { + // Bodyless go:linkname declarations and replaced package members are + // aliases, not additional definition owners. Lazy references still + // resolve them to the canonical symbol, but eager enumeration must wait + // for the canonical owner's package to emit the one physical body. + return true + } + } + entry, err := p.resolveFunctionSymbol(fn) + if err != nil { + panic(err) + } + return entry.planned && entry.plan.Emission == coro.EmitNone +} + +// checkSupported rejects plan decisions whose physical ABI is not implemented +// yet. Callers must run this before looking up or creating an LLVM symbol. +func (e plannedFunctionSymbol) checkSupported() error { + return e.checkSupportedWithPhysicalPlan(nil) +} + +func (e plannedFunctionSymbol) checkSupportedWithPhysicalPlan(accept func(*coroPhysicalFunctionPlan) error) error { + if !e.planned { + return nil + } + if e.plan.Emission == coro.EmitNone { + return fmt.Errorf("coroutine entry resolution: function %q has no emitted entry", e.plan.ID) + } + if e.plan.Emission == coro.EmitRawPlain { + if e.plan.FuncRep != coro.DirectPlain || e.plan.Primary != coro.PrimaryPlain || + !e.plan.RawPlainOnly || e.plan.ManagedDemand != coro.NoDemand || !e.plan.RawPlainDemand { + return fmt.Errorf( + "coroutine entry resolution: raw-only function %q has invalid selection (representation=%s primary=%s raw-only=%t managed=%s raw=%t)", + e.plan.ID, e.plan.FuncRep, e.plan.Primary, e.plan.RawPlainOnly, + e.plan.ManagedDemand, e.plan.RawPlainDemand, + ) + } + variant := e.coroPlan != nil && e.coroPlan.HasRawPlainVariant(e.function) + return validatePlannedRawPlainVariant(e.function, e.plan, variant) + } + // EmitPlain remains a legal single native-stack body under the target-wide + // ExplicitStatus identity. The physical-coroutine call-site verifier is the + // authority that forbids entering a MayUnwind plain body from a stackless + // activation; rejecting unrelated synchronous-only bodies here would force + // unnecessary dual versions across the standard library. + if e.plan.FuncRep == coro.Dispatch { + receiverDispatchTarget := e.interfacePlain.acceptsTarget(e.function, e.plan) || + e.managedInterface.acceptsTarget(e.function, e.plan) + if receiverDispatchTarget { + // A plain target keeps the legacy callable itab entry. An async + // receiver method instead uses that word only as a closed dispatch + // discriminator and must still pass the ordinary physical-coroutine + // checks below. + if e.plan.Emission == coro.EmitPlain { + return nil + } + if e.plan.Emission != coro.EmitCoroutine { + return fmt.Errorf("coroutine entry resolution: raw/interface target %q has unsupported emission %s", e.plan.ID, e.plan.Emission) + } + } else { + if !e.plainDispatch { + return fmt.Errorf("coroutine entry resolution: function %q requires an unimplemented dispatch descriptor", e.plan.ID) + } + if err := validateCoroDynamicDispatchTarget(e.function, e.plan, e.emission); err != nil { + return err + } + if e.plan.Emission == coro.EmitPlain { + return nil + } + // A coroutine descriptor publishes only a thin HasCoro entry thunk; + // the single source primary must still pass the complete physical-body + // validation below. + } + } + if e.plan.Emission == coro.EmitCoroutine { + if !e.physical { + return fmt.Errorf("coroutine emission %q requires coroutine physical ABI lowering", e.plan.ID) + } + sourceSig := coroPhysicalNormalizeSourceSignature(e.function.Signature) + if e.emission != nil { + var err error + sourceSig, err = e.emission.coroPhysicalEntrySourceSignature(e.function) + if err != nil { + return err + } + } + if err := validateCoroPhysicalFunctionValueABI(e.plan, sourceSig, e.plainDispatch); err != nil { + return err + } + rawMethodToken := e.interfacePlain.acceptsTarget(e.function, e.plan) || + e.managedInterface.acceptsTarget(e.function, e.plan) + if accept == nil && e.emission != nil && e.emission.coroProgramIR != nil && + e.emission.coroProgramIR.physicalPlansSealed { + _, err := e.emission.coroProgramIR.physicalFunctionPlan(e.function, e.physicalOwner) + return err + } + return validateCoroPhysicalABIForOwner( + e.function, e.plan, e.coroPlan, e.emission, e.physicalOwner, e.childAwait, e.programRun, + e.staticSpawn, e.explicitPanic, e.frameRetentionABI, e.channel, e.plainDispatch, rawMethodToken, + e.interfacePlain, e.managedInterface, accept, + ) + } + if e.plan.Emission == coro.EmitExternal && e.plan.FuncRep == coro.DirectCoro { + return fmt.Errorf("external coroutine emission %q requires coroutine physical ABI lowering", e.plan.ID) + } + return nil +} + +// preflightCoroPlan rejects every unsupported or inconsistent entry before cl +// creates an LLVM package. This includes non-Go/intrinsic functions present in +// the plan: active entry resolution may not silently route an unsupported plan +// through a legacy ABI merely because funcName classifies it specially. +func (c *Compilation) preflightCoroPlan() error { + if c == nil { + return nil + } + if err := c.validateCoroProfile(); err != nil { + return err + } + if !c.CoroProfile.Active() { + return nil + } + if err := c.validateCoroWorkerUniverseTarget(); err != nil { + return err + } + c.coroPreflight.Do(func() { + if err := c.validateCoroABIIdentity(false); err != nil { + c.coroPreflightErr = err + return + } + if c.CoroLoweringFacts.Schema != "" || c.CoroLoweringFactsDigest != "" { + if err := c.validateCoroLoweringFactsIdentity(); err != nil { + c.coroPreflightErr = err + return + } + } + plan := c.CoroPlan + if plan == nil { + c.coroPreflightErr = fmt.Errorf("coroutine entry resolution requires a compilation CoroPlan") + return + } + universe := c.EmissionUniverse + if universe == nil { + c.coroPreflightErr = fmt.Errorf("coroutine entry resolution requires a prepared emission universe") + return + } + if universe.CoroChannelEnabled() != c.CoroChannelActive() { + c.coroPreflightErr = fmt.Errorf("coroutine channel lowering disagrees with the prepared emission universe") + return + } + if universe.CoroWorkerEnabled() != c.CoroWorkerActive() { + c.coroPreflightErr = fmt.Errorf("coroutine worker lowering disagrees with the prepared emission universe") + return + } + if err := universe.ValidateCoroPlan(plan); err != nil { + c.coroPreflightErr = err + return + } + managedInterface, err := analyzeCoroManagedInterfaceDispatchPlan( + plan, universe, c.CoroPlainDispatchActive() && c.CoroChildAwaitActive(), + ) + if err != nil { + c.coroPreflightErr = err + return + } + c.coroManagedInterface = managedInterface + interfacePlain, err := analyzeCoroClosedInterfacePlainPlan( + plan, universe, c.CoroExplicitStatusActive(), c.CoroChildAwaitActive(), managedInterface, + ) + if err != nil { + c.coroPreflightErr = err + return + } + c.coroClosedInterfacePlain = interfacePlain + if c.CoroChildAwaitActive() { + if err := validateCoroRootEntries(plan); err != nil { + c.coroPreflightErr = err + return + } + } + physicalExpected := make(map[emissionFunctionOwnerKey]none) + physicalStage := newCoroPhysicalPlanStage() + for _, function := range plan.Functions() { + hasEmittedBody, err := c.plannedFunctionEmittedBody(function.Function) + if err != nil { + c.coroPreflightErr = err + return + } + if err := validatePlannedFunction(function.Function, function.Plan, hasEmittedBody); err != nil { + c.coroPreflightErr = err + return + } + if function.Plan.Emission == coro.EmitNone { + continue + } + entry := plannedFunctionSymbol{ + function: function.Function, + plan: function.Plan, + planned: true, + physical: c.CoroPhysicalABIActive(), + childAwait: c.CoroChildAwaitActive(), + programRun: c.CoroProgramBootstrapActive(), + channel: c.CoroChannelActive(), + plainDispatch: c.CoroPlainDispatchActive(), + staticSpawn: c.CoroClosedStaticSpawnActive(), + explicitPanic: c.CoroExplicitStatusActive(), + frameRetentionABI: c.CoroFrameRetentionABI, + coroPlan: plan, + emission: universe, + interfacePlain: c.coroClosedInterfacePlain, + managedInterface: c.coroManagedInterface, + } + if c.CoroPhysicalABIActive() && function.Plan.Emission == coro.EmitCoroutine { + owners := universe.sortedUseOwners(function.Function) + if len(owners) == 0 { + c.coroPreflightErr = fmt.Errorf("coroutine physical preflight: function %q has no exact emission owner", function.Plan.ID) + return + } + for _, owner := range owners { + ownerEntry := entry + ownerEntry.physicalOwner = owner + key := emissionFunctionOwnerKey{function: function.Function, owner: owner} + physicalExpected[key] = none{} + if err := ownerEntry.checkSupportedWithPhysicalPlan(func(plan *coroPhysicalFunctionPlan) error { + return physicalStage.freezePhysicalFunctionPlan(plan) + }); err != nil { + c.coroPreflightErr = err + return + } + } + } else if err := entry.checkSupported(); err != nil { + c.coroPreflightErr = err + return + } + if c.CoroPhysicalABIActive() && function.Plan.Emission == coro.EmitCoroutine { + sig, err := universe.coroPhysicalEntrySourceSignature(function.Function) + if err == nil { + err = validateCoroLeafPhysicalSignature(function.Plan, sig) + } + if err == nil { + err = validateCoroPhysicalFunctionValueABI(function.Plan, sig, c.CoroPlainDispatchActive()) + } + if err != nil { + c.coroPreflightErr = err + return + } + } + } + if err := validateCoroRawCFunctionAdapters(plan, universe); err != nil { + c.coroPreflightErr = err + return + } + if err := validateCoroRawPlainConsumers(plan, universe, c.CoroPlainDispatchActive()); err != nil { + c.coroPreflightErr = err + return + } + if c.CoroPhysicalABIActive() { + c.coroPreflightErr = validateCoroPhysicalConsumersCapabilities( + plan, universe, c.CoroChildAwaitActive(), c.CoroClosedStaticSpawnActive(), + c.CoroPlainDispatchActive(), + ) + if c.coroPreflightErr != nil { + return + } + } + if c.CoroPlainDispatchActive() { + c.coroPreflightErr = validateCoroPlainDispatchConsumers( + plan, universe, c.coroClosedInterfacePlain, c.coroManagedInterface, + ) + if c.coroPreflightErr != nil { + return + } + } + if c.CoroPhysicalABIActive() { + if err := universe.coroProgramIR.commitPhysicalFunctionPlans(physicalStage, physicalExpected); err != nil { + c.coroPreflightErr = fmt.Errorf("coroutine physical preflight: commit ProgramIR: %w", err) + return + } + } + }) + return c.coroPreflightErr +} + +func (p *context) mustFunctionSymbol(fn *ssa.Function) plannedFunctionSymbol { + entry, err := p.resolveFunctionSymbol(fn) + if err == nil { + err = entry.checkSupported() + } + if err != nil { + panic(err) + } + return entry +} + +// mustRawPlainFunctionSymbol selects the separately planned legacy Go-ABI body +// for one member of an exactly validated raw synchronous closure. It never +// changes the managed primary selected by mustFunctionSymbol. Captured +// functions are admitted only as internal closure-context variants; publishing +// their address still requires validatePlannedRawPlainEntry and is rejected. +func (p *context) mustRawPlainFunctionSymbol(fn *ssa.Function) plannedFunctionSymbol { + entry, err := p.resolveFunctionSymbol(fn) + if err == nil { + err = entry.checkSupported() + } + return p.mustRawPlainFunctionSymbolFromEntry(entry, err) +} + +// mustRawPlainFunctionSymbolFromEntry preserves an already-selected symbol +// role while choosing its native-stack twin. This matters for the private +// patch-original role: its raw twin is init$hasPatch, never the public init. +func (p *context) mustRawPlainFunctionSymbolFromEntry(entry plannedFunctionSymbol, err error) plannedFunctionSymbol { + if err == nil { + variant := p.compilation != nil && p.compilation.CoroPlan != nil && + p.compilation.CoroPlan.HasRawPlainVariant(entry.function) + err = validatePlannedRawPlainVariant(entry.function, entry.plan, variant) + } + if err == nil && (entry.plan.Emission == coro.EmitCoroutine || entry.plan.Emission == coro.EmitRawPlain) { + if entry.baseName == "" { + err = fmt.Errorf("raw plain entry %q has no frozen base symbol", entry.plan.ID) + } else { + entry.name = entry.baseName + entry.physical = false + } + } + if err != nil { + panic(err) + } + return entry +} + +func validatePlannedRawPlainVariant(fn *ssa.Function, plan coro.FunctionPlan, variant bool) error { + fail := func(format string, args ...any) error { + return fmt.Errorf("raw plain variant %q: %s", plan.ID, fmt.Sprintf(format, args...)) + } + if fn == nil || fn.Signature == nil || len(fn.Blocks) == 0 { + return fail("requires one owned Go body") + } + if !variant || plan.External != coro.Defined || !plan.RawPlainDemand || plan.Demand == coro.NoDemand { + return fail( + "requires a raw-demanded defined RawPlainVariant plan (variant=%t external=%s demand=%s managed=%s raw=%t)", + variant, plan.External, plan.Demand, plan.ManagedDemand, plan.RawPlainDemand, + ) + } + switch plan.Emission { + case coro.EmitPlain: + if plan.RawPlainOnly || plan.ManagedDemand == coro.NoDemand || plan.Primary != coro.PrimaryPlain || + plan.FuncRep == coro.DirectCoro || plan.Effect.MaySuspend() { + return fail( + "plain alias has raw-only=%t managed=%s primary=%s representation=%s effect=%s", + plan.RawPlainOnly, plan.ManagedDemand, plan.Primary, plan.FuncRep, plan.Effect, + ) + } + case coro.EmitCoroutine: + if plan.RawPlainOnly || plan.ManagedDemand == coro.NoDemand || + plan.Primary != coro.PrimaryCoroutine || !plan.Effect.MaySuspend() { + return fail( + "dual body has raw-only=%t managed=%s primary=%s effect=%s, want managed coroutine primary", + plan.RawPlainOnly, plan.ManagedDemand, plan.Primary, plan.Effect, + ) + } + case coro.EmitRawPlain: + if !plan.RawPlainOnly || plan.ManagedDemand != coro.NoDemand || + plan.Primary != coro.PrimaryPlain || plan.FuncRep != coro.DirectPlain { + return fail( + "raw-only body has raw-only=%t managed=%s primary=%s representation=%s", + plan.RawPlainOnly, plan.ManagedDemand, plan.Primary, plan.FuncRep, + ) + } + default: + return fail("unsupported emission %s", plan.Emission) + } + return nil +} + +func validatePlannedRawPlainEntry(fn *ssa.Function, plan coro.FunctionPlan) error { + fail := func(format string, args ...any) error { + return fmt.Errorf("raw plain entry %q: %s", plan.ID, fmt.Sprintf(format, args...)) + } + if fn == nil || fn.Signature == nil || len(fn.FreeVars) != 0 || len(fn.Blocks) == 0 { + return fail("requires one owned non-capturing Go body") + } + if !plan.RawPlainEntry || plan.External != coro.Defined || !plan.RawPlainDemand || plan.Demand == coro.NoDemand { + return fail( + "requires a raw-demanded defined RawPlainEntry plan (entry=%t external=%s demand=%s managed=%s raw=%t)", + plan.RawPlainEntry, plan.External, plan.Demand, plan.ManagedDemand, plan.RawPlainDemand, + ) + } + if err := validatePlannedRawPlainVariant(fn, plan, true); err != nil { + return fail("invalid legacy body: %v", err) + } + return nil +} diff --git a/cl/coro_entry_test.go b/cl/coro_entry_test.go new file mode 100644 index 0000000000..379f179bbd --- /dev/null +++ b/cl/coro_entry_test.go @@ -0,0 +1,677 @@ +//go:build !llgo +// +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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroEntryTestSource = `package foo + +var channel chan int + +func Plain() {} +func Coroutine() { <-channel } +func Boxed() {} +func Box() any { return Boxed } +func External() +` + +func buildCoroEntryTestPlan(t *testing.T) (*ssa.Package, *coro.SSAPlan) { + t.Helper() + pkg, _, _ := buildGoSSAPkg(t, coroEntryTestSource) + plan, err := coro.AnalyzeSSA(pkg.Prog, coro.Roots{ + {Function: pkg.Func("Plain"), Demand: coro.SyncDemand}, + {Function: pkg.Func("Coroutine"), Demand: coro.AsyncDemand}, + {Function: pkg.Func("Box"), Demand: coro.AsyncDemand}, + {Function: pkg.Func("External"), Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == pkg.Func("External") { + return coro.SSAFunctionPolicy{ + Effect: coro.WaitHost, + External: coro.ExternalKnown, + OverrideExternal: true, + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + return pkg, plan +} + +func newCoroEntryTestContext(t *testing.T, pkg *ssa.Package, compilation *Compilation) (*context, func()) { + t.Helper() + prog := newLLSSAProg(t) + ctx := &context{ + prog: prog, + pkg: prog.NewPackage(pkg.Pkg.Name(), pkg.Pkg.Path()), + goProg: pkg.Prog, + goTyps: pkg.Pkg, + goPkg: pkg, + compilation: compilation, + } + return ctx, prog.Dispose +} + +// coroEntryPreflightUniverse is a minimal exact universe for tests that are +// expected to stop in whole-plan preflight before package/codegen validation. +func coroEntryPreflightUniverse(plan *coro.SSAPlan) *EmissionUniverse { + u := &EmissionUniverse{ + required: make(map[*ssa.Function]none), + aliases: make(map[*ssa.Function]*ssa.Function), + functionKinds: make(map[emissionFunctionOwnerKey]int), + finalKeys: make(map[emissionFunctionOwnerKey]string), + useOwners: make(map[*ssa.Function]map[*preparedEmissionPackage]none), + ownerStates: make(map[*ssa.Function]map[*preparedEmissionPackage]emissionFunctionState), + coroProfile: CoroProfileStackless, + } + if plan == nil { + return u + } + owners := make(map[*ssa.Package]*preparedEmissionPackage) + for _, planned := range plan.Functions() { + fn := planned.Function + u.functions = append(u.functions, fn) + u.required[fn] = none{} + owner := owners[fn.Pkg] + if owner == nil { + identity := "test" + if fn.Pkg != nil && fn.Pkg.Pkg != nil { + identity = fn.Pkg.Pkg.Path() + } + owner = &preparedEmissionPackage{identity: identity, pkgPath: identity, ssa: fn.Pkg, order: len(owners)} + owners[fn.Pkg] = owner + } + u.useOwners[fn] = map[*preparedEmissionPackage]none{owner: {}} + u.ownerStates[fn] = map[*preparedEmissionPackage]emissionFunctionState{owner: {state: pkgNormal}} + key := emissionFunctionOwnerKey{function: fn, owner: owner} + u.functionKinds[key] = goFunc + u.finalKeys[key] = managedSymbolKey(goFunc, fn.Name(), "preflight-test") + } + return u +} + +func TestResolveFunctionSymbolUsesPrimaryAndExactPlan(t *testing.T) { + pkg, plan := buildCoroEntryTestPlan(t) + ctx, dispose := newCoroEntryTestContext(t, pkg, &Compilation{ + CoroPlan: plan, CoroProfile: CoroProfileStackless, + }) + defer dispose() + + plain, err := ctx.resolveFunctionSymbol(pkg.Func("Plain")) + if err != nil { + t.Fatal(err) + } + if !plain.planned || plain.plan.Emission != coro.EmitPlain || plain.plan.Primary != coro.PrimaryPlain || strings.HasSuffix(plain.name, coroPrimarySuffix) { + t.Fatalf("plain entry = %+v", plain) + } + if err := plain.checkSupported(); err != nil { + t.Fatalf("plain entry rejected: %v", err) + } + + coroutine, err := ctx.resolveFunctionSymbol(pkg.Func("Coroutine")) + if err != nil { + t.Fatal(err) + } + if !coroutine.planned || coroutine.plan.Emission != coro.EmitCoroutine || coroutine.plan.Primary != coro.PrimaryCoroutine || !strings.HasSuffix(coroutine.name, coroPrimarySuffix) { + t.Fatalf("coroutine entry = %+v", coroutine) + } + if err := coroutine.checkSupported(); err != nil { + t.Fatalf("coroutine primary rejected by the stackless profile: %v", err) + } + + boxed, err := ctx.resolveFunctionSymbol(pkg.Func("Boxed")) + if err != nil { + t.Fatal(err) + } + if boxed.plan.Emission != coro.EmitPlain || boxed.plan.Primary != coro.PrimaryPlain || boxed.plan.FuncRep != coro.Dispatch || strings.HasSuffix(boxed.name, coroPrimarySuffix) { + t.Fatalf("boxed entry = %+v, want one plain primary plus dispatch descriptor", boxed) + } + if err := boxed.checkSupported(); err != nil { + t.Fatalf("descriptor-backed plain primary rejected by the stackless profile: %v", err) + } + + external, err := ctx.resolveFunctionSymbol(pkg.Func("External")) + if err != nil { + t.Fatal(err) + } + if external.plan.Emission != coro.EmitExternal || external.plan.Primary != coro.PrimaryExternal || external.plan.FuncRep != coro.DirectCoro { + t.Fatalf("external entry = %+v, want coroutine external primary", external) + } + if err := external.checkSupported(); err == nil || !strings.Contains(err.Error(), "external coroutine") { + t.Fatalf("external support error = %v", err) + } + + reportOnlyCtx, reportOnlyDispose := newCoroEntryTestContext(t, pkg, &Compilation{CoroPlan: plan}) + defer reportOnlyDispose() + reportOnly, err := reportOnlyCtx.resolveFunctionSymbol(pkg.Func("Coroutine")) + if err != nil { + t.Fatal(err) + } + if reportOnly.planned || strings.HasSuffix(reportOnly.name, coroPrimarySuffix) { + t.Fatalf("report-only entry = %+v, want unchanged legacy entry", reportOnly) + } + + otherPkg, _, _ := buildGoSSAPkg(t, coroEntryTestSource) + otherCtx, otherDispose := newCoroEntryTestContext(t, otherPkg, &Compilation{ + CoroPlan: plan, CoroProfile: CoroProfileStackless, + }) + defer otherDispose() + if _, err := otherCtx.resolveFunctionSymbol(otherPkg.Func("Plain")); err == nil || !strings.Contains(err.Error(), "absent") { + t.Fatalf("other-program resolution error = %v, want exact-pointer plan miss", err) + } +} + +func TestCoroEntryOmitsUndemandedEffectfulComplexFunction(t *testing.T) { + const source = `package foo +func Complex(ch chan int) int { + value := <-ch + if value == 0 { + return 1 + } + return value +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, nil, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + complexPlan, ok := plan.FunctionPlan(ssaPkg.Func("Complex")) + if !ok || complexPlan.Demand != coro.NoDemand || complexPlan.Emission != coro.EmitNone || complexPlan.Primary != coro.PrimaryCoroutine || !complexPlan.Effect.MaySuspend() { + t.Fatalf("Complex plan = %+v, present=%t; want undemanded, non-emitted logical coroutine", complexPlan, ok) + } + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + } + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("undemanded complex function blocked package emission: %v", err) + } + module := pkg.Module() + if !module.NamedFunction("foo.Complex").IsNil() || !module.NamedFunction("foo.Complex"+coroPrimarySuffix).IsNil() { + t.Fatalf("EmitNone function acquired an LLVM symbol:\n%s", module.String()) + } + ir := module.String() + for _, marker := range []string{ + coroPrimarySuffix, + coroDescriptorPrefixV1, + coroRootFactoryDescriptorPrefix, + coroRootPackageAnchorPrefix, + } { + if strings.Contains(ir, marker) { + t.Fatalf("EmitNone package unexpectedly contains coroutine marker %q:\n%s", marker, ir) + } + } +} + +func TestCoroEntryDirectFunctionValueDemandsTargetAndOmitsDeadExternal(t *testing.T) { + const source = `package foo +func External() +func Target() {} +func Owner() bool { return Target != nil } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + owner := ssaPkg.Func("Owner") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: owner, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, ok := plan.FunctionPlan(ssaPkg.Func("Target")) + if !ok || targetPlan.Demand != coro.SyncDemand || targetPlan.Emission != coro.EmitPlain || targetPlan.FuncRep != coro.DirectPlain { + t.Fatalf("Target plan = %+v, present=%t; want demanded direct plain body", targetPlan, ok) + } + externalPlan, ok := plan.FunctionPlan(ssaPkg.Func("External")) + if !ok || externalPlan.Demand != coro.NoDemand || externalPlan.Emission != coro.EmitNone || externalPlan.Primary != coro.PrimaryExternal { + t.Fatalf("External plan = %+v, present=%t; want non-emitted logical external", externalPlan, ok) + } + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + }}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + for _, name := range []string{"foo.Owner", "foo.Target"} { + if module.NamedFunction(name).IsNil() { + t.Fatalf("missing demanded function %q:\n%s", name, module.String()) + } + } + if !module.NamedFunction("foo.External").IsNil() { + t.Fatalf("dead external acquired an LLVM declaration:\n%s", module.String()) + } +} + +func TestCoroEntryDemandedEffectfulComplexFunctionCompiles(t *testing.T) { + const source = `package foo +func Complex(ch chan int) int { + value := <-ch + if value == 0 { + return 1 + } + return value +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + complex := ssaPkg.Func("Complex") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: complex, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + complexPlan, ok := plan.FunctionPlan(complex) + if !ok || complexPlan.Demand != coro.AsyncDemand || complexPlan.Emission != coro.EmitCoroutine { + t.Fatalf("Complex plan = %+v, present=%t; want demanded coroutine emission", complexPlan, ok) + } + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + } + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile demanded channel/control-flow coroutine: %v", err) + } + module := pkg.Module() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify demanded channel/control-flow coroutine: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Complex").String() + if !strings.Contains(body, coroChanRecvParkHookV1) || !strings.Contains(body, "switch i32") { + t.Fatalf("complex coroutine lacks channel park/control-flow lowering:\n%s", body) + } +} + +func TestCoroPhysicalConsumerRejectsReferenceToEmitNone(t *testing.T) { + tests := []struct { + name string + hiddenName string + selectInstr func(ssa.Instruction) bool + want string + }{ + { + name: "call", + hiddenName: "HiddenCall", + selectInstr: func(instr ssa.Instruction) bool { + _, ok := instr.(*ssa.Call) + return ok + }, + want: "non-emitted call target", + }, + { + name: "function value", + hiddenName: "HiddenValue", + selectInstr: func(instr ssa.Instruction) bool { + for _, operand := range instr.Operands(nil) { + if operand != nil { + if _, ok := (*operand).(*ssa.Function); ok { + return true + } + } + } + return false + }, + want: "non-emitted function value", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + const source = `package foo +func Target() {} +func HiddenCall() { Target() } +func HiddenValue() any { return Target } +func Caller() {} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + caller := ssaPkg.Func("Caller") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: caller, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + targetPlan, ok := plan.FunctionPlan(ssaPkg.Func("Target")) + if !ok || targetPlan.Emission != coro.EmitNone { + t.Fatalf("Target plan = %+v, present=%t; want EmitNone before injected inconsistent consumer", targetPlan, ok) + } + var injected ssa.Instruction + for _, block := range ssaPkg.Func(test.hiddenName).Blocks { + for _, instr := range block.Instrs { + if test.selectInstr(instr) { + injected = instr + break + } + } + } + if injected == nil { + t.Fatalf("%s has no instruction suitable for the test", test.hiddenName) + } + // Deliberately mutate SSA after the immutable plan was built. This + // models a stale/mismatched consumer and proves cl will not create a + // declaration for an EmitNone target. + caller.Blocks[0].Instrs = append([]ssa.Instruction{injected}, caller.Blocks[0].Instrs...) + got, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + }}, + ) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("inconsistent emitted consumer = %v, %v; want error containing %q", got, err, test.want) + } + if got != nil { + t.Fatal("consumer preflight returned a partial package") + } + }) + } +} + +func TestCoroEntryResolutionPreflightRejectsWholePlanBeforeCodegen(t *testing.T) { + tests := []struct { + name string + source string + plan func(*ssa.Package) (*coro.SSAPlan, error) + want string + }{ + { + name: "later coroutine body", + source: `package foo +func APlain() {} +func ZCoroutine(ch chan int) { <-ch } +`, + plan: func(pkg *ssa.Package) (*coro.SSAPlan, error) { + return coro.AnalyzeSSA(pkg.Prog, coro.Roots{ + {Function: pkg.Func("APlain"), Demand: coro.SyncDemand}, + {Function: pkg.Func("ZCoroutine"), Demand: coro.AsyncDemand}, + }, coro.SSAConfig{}) + }, + want: "physical ABI", + }, + { + name: "dispatch descriptor", + source: `package foo +func Target() {} +func Box() any { return Target } +`, + plan: func(pkg *ssa.Package) (*coro.SSAPlan, error) { + return coro.AnalyzeSSA(pkg.Prog, coro.Roots{ + {Function: pkg.Func("Box"), Demand: coro.AsyncDemand}, + }, coro.SSAConfig{}) + }, + want: "physical plan commit requires the call SitePlan stage", + }, + { + name: "external coroutine", + source: `package foo; func External()`, + plan: func(pkg *ssa.Package) (*coro.SSAPlan, error) { + external := pkg.Func("External") + return coro.AnalyzeSSA(pkg.Prog, coro.Roots{ + {Function: external, Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == external { + return coro.SSAFunctionPolicy{ + Effect: coro.WaitHost, + External: coro.ExternalKnown, + OverrideExternal: true, + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + }, + want: "requires a defined body", + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + pkg, _, files := buildGoSSAPkg(t, tt.source) + plan, err := tt.plan(pkg) + if err != nil { + t.Fatal(err) + } + observerCalls := 0 + prog := newLLSSAProg(t) + defer prog.Dispose() + got, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, pkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: &Compilation{ + CoroPlan: plan, + EmissionUniverse: coroEntryPreflightUniverse(plan), + CoroPlanObserver: func(*ssa.Package, *coro.SSAPlan) { + observerCalls++ + }, CoroProfile: CoroProfileStackless, + }, + }) + if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("preflight result = %v, %v; want error containing %q", got, err, tt.want) + } + if got != nil { + t.Fatal("preflight failure returned a partial package") + } + if observerCalls != 0 { + t.Fatalf("observer calls = %d, want pre-codegen rejection", observerCalls) + } + }) + } +} + +func TestCoroEntryPreflightUsesFrozenCEmissionInsteadOfStubBlocks(t *testing.T) { + pkg, _, files := buildGoSSAPkg(t, `package foo +var channel chan int +//llgo:link External C.external +func External() { <-channel } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: pkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(pkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + external, ok := universe.Resolve(pkg.Func("External")) + if !ok || external == nil || len(external.Blocks) == 0 { + t.Fatal("fixture has no canonical bodyful C stub") + } + if background, classified, err := universe.FunctionBackground(external); err != nil || !classified || background != llssa.InC { + t.Fatalf("External frozen background = %v, %v, %v; want InC, true, nil", background, classified, err) + } + + buildPlan := func(ignore bool) *coro.SSAPlan { + t.Helper() + plan, err := coro.AnalyzeSSA(pkg.Prog, coro.Roots{{Function: external, Demand: coro.SyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == external { + if !ignore { + return coro.SSAFunctionPolicy{}, nil + } + return coro.SSAFunctionPolicy{ + IgnoreBody: true, + External: coro.ExternalUnknownForeign, + OverrideExternal: true, + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + return plan + } + + ignored := buildPlan(true) + if !ignored.IgnoresBody(external) { + t.Fatal("bodyful C stub was not excluded from the physical plan") + } + if err := (&Compilation{ + CoroPlan: ignored, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + }).preflightCoroPlan(); err == nil || !strings.Contains(err.Error(), "requires a defined body") { + t.Fatalf("bodyful frozen C root preflight error = %v, want defined-body rejection", err) + } + + notIgnored := buildPlan(false) + err = (&Compilation{ + CoroPlan: notIgnored, + EmissionUniverse: universe, CoroProfile: CoroProfileStackless, + }).preflightCoroPlan() + if err == nil || !strings.Contains(err.Error(), "synchronous demand without a planned raw plain entry") { + t.Fatalf("non-ignored C stub preflight error = %v", err) + } +} + +func TestCoroEntryResolutionPreflightRejectsMissingPlanAndCache(t *testing.T) { + pkg, _, files := buildGoSSAPkg(t, `package foo; func F() {}`) + for _, tt := range []struct { + name string + compilation *Compilation + cacheHit bool + want string + }{ + { + name: "missing plan", + compilation: &Compilation{CoroProfile: CoroProfileStackless}, + want: "requires a compilation CoroPlan", + }, + { + name: "missing universe", + compilation: &Compilation{ + CoroPlan: &coro.SSAPlan{}, CoroProfile: CoroProfileStackless, + }, + want: "prepared emission universe", + }, + } { + t.Run(tt.name, func(t *testing.T) { + prog := newLLSSAProg(t) + defer prog.Dispose() + got, _, err := NewPackageExWithEmbedOptions(prog, nil, nil, nil, pkg, files, goembed.VarMap{}, PackageOptions{ + Compilation: tt.compilation, + CacheHit: tt.cacheHit, + }) + if err == nil || !strings.Contains(err.Error(), tt.want) { + t.Fatalf("preflight result = %v, %v; want error containing %q", got, err, tt.want) + } + if got != nil { + t.Fatal("preflight failure returned a partial package") + } + }) + } + + cacheCompilation := &Compilation{ + CoroProfile: CoroProfileStackless, + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, + } + if err := cacheCompilation.validateCoroCacheIdentity(); err == nil || !strings.Contains(err.Error(), "CoroPlanDigest") { + t.Fatalf("cache identity error = %v, want missing CoroPlanDigest rejection", err) + } +} diff --git a/cl/coro_frame_retention.go b/cl/coro_frame_retention.go new file mode 100644 index 0000000000..64961322ab --- /dev/null +++ b/cl/coro_frame_retention.go @@ -0,0 +1,2180 @@ +/* + * 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 ( + "crypto/sha256" + "encoding/hex" + "go/constant" + "go/token" + "go/types" + "sort" + "strconv" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// coroFrameRetentionProof is derived twice from the same immutable SSA and +// frozen emission universe: preflight uses it to accept selected x/tools Heap +// Allocs, and codegen uses it to lower those exact Allocs into the LLVM +// coroutine frame. +// The maps are never exposed outside cl and are immutable after construction. +type coroFrameRetentionProof struct { + // allocations are the exact park-transaction cells reclassified from an + // x/tools Heap Alloc into storage owned by the LLVM coroutine frame. + allocations map[*ssa.Alloc]struct{} + // managedHeapAllocations remain ordinary Go heap allocations. Each fact is + // admitted only after the frozen lowered-call plan proves its exact AllocZ + // path; the resulting pointer may then be conservatively scanned from this + // coroutine frame while it is suspended. + managedHeapAllocations map[*ssa.Alloc]coroFrameRetentionManagedHeapAllocation + // terminalResultAllocations are the exact managed heap cells reloaded after + // RunDefers to reconstruct named results. Codegen defines only this narrow + // subset before the initial suspend so compiler-owned cleanup/cancel + // continuations have a dominating pointer without moving ordinary heap + // allocations out of their source blocks. + terminalResultAllocations map[*ssa.Alloc]struct{} + // exactRoots, stableAddresses, uintptrValues, and callKeepalives are a + // capability proof, not a tracing-GC root map. They name the exact SSA + // values that LLVM may retain in a PhysicalABIV1 coroutine frame under a + // non-moving conservative collector (or no collector), and the exact uses + // for which address/uintptr provenance was proved. A precise or moving + // collector must not consume this profile until the coroutine ABI also + // publishes typed frame maps and relocation barriers. + exactRoots map[ssa.Value]coroFrameRetentionExactRoot + stableAddresses map[coroFrameRetentionAddressUse]coroFrameRetentionAddressFact + uintptrValues map[ssa.Value]coroFrameRetentionUintptrFact + callKeepalives map[*ssa.Call]coroFrameRetentionCallFact + rootDigest string +} + +// This is the sole current root profile. There is intentionally no v1 +// compatibility path. LLVM CoroSplit materializes every SSA pointer live over +// a suspend in the heap-backed coroutine frame. BDWGC allocates that frame with +// scanned MallocUncollectable storage; tinygogc reaches it through the live +// scheduler task and conservatively scans it; nogc/WASM have no tracing +// collector that could reclaim its referent. A future precise or moving +// collector must publish typed frame maps, relocation, and write barriers under +// a new ABI instead of reusing this identity. +const coroFrameRetentionExactRootProfileV2 = "physical-v1.nonmoving-conservative-or-none.exact-roots-managed-heap.v2" + +type coroFrameRetentionManagedHeapAllocation struct { + zeroSized bool + helper string + helperTarget coro.FunctionID + helperEmission coro.BodyEmission +} + +type coroFrameRetentionRootKind uint8 + +const ( + coroFrameRetentionRootInvalid coroFrameRetentionRootKind = iota + coroFrameRetentionRootReceiver + coroFrameRetentionRootPointerParameter + coroFrameRetentionRootSliceParameter + coroFrameRetentionRootLocalSlice + coroFrameRetentionRootLocalAddress + coroFrameRetentionRootClosureFreeVar + coroFrameRetentionRootManagedHeapAllocation +) + +type coroFrameRetentionExactRoot struct { + value ssa.Value + kind coroFrameRetentionRootKind + order int +} + +type coroFrameRetentionAddressUse struct { + value ssa.Value + use ssa.Instruction +} + +type coroFrameRetentionAddressFact struct { + roots []ssa.Value + evidence []ssa.Instruction + // nonNil distinguishes an address whose source is statically non-nil or + // protected by exact dominating SSA evidence from a transport-stable but + // nullable address. The latter is still a valid frame root, but every + // dereference must take the compiler-owned explicit fault edge first. + nonNil bool +} + +type coroFrameRetentionUintptrFact struct { + roots []ssa.Value +} + +type coroFrameRetentionCallKindV1 uint8 + +const ( + coroFrameRetentionCallInvalidV1 coroFrameRetentionCallKindV1 = iota + coroFrameRetentionCallManagedChildV1 + coroFrameRetentionCallWorkerV1 + coroFrameRetentionCallParkOwnerV1 +) + +type coroFrameRetentionCallFact struct { + kind coroFrameRetentionCallKindV1 + roots []ssa.Value + sources []ssa.Value +} + +// exactRootCapabilityProfile is deliberately separate from the target GC +// configuration. The manifest/physical-ABI consumer must match this profile +// only for a non-moving conservative or non-collecting target. +func (p *coroFrameRetentionProof) exactRootCapabilityProfile() string { + if p == nil || p.rootDigest == "" { + return "" + } + return coroFrameRetentionExactRootProfileV2 +} + +// exactRootCapabilityDigest is a read-only identity for all exact root, +// address-use, park transaction, and uintptr-keepalive facts in this proof. +// It is rebuilt from deterministic SSA ordinals rather than SSA pointer +// identity or diagnostic strings. +func (p *coroFrameRetentionProof) exactRootCapabilityDigest() string { + if p == nil { + return "" + } + return p.rootDigest +} + +func (p *coroFrameRetentionProof) exactRetainedRoots() []ssa.Value { + if p == nil || len(p.exactRoots) == 0 { + return nil + } + ordered := make([]coroFrameRetentionExactRoot, 0, len(p.exactRoots)) + for _, root := range p.exactRoots { + ordered = append(ordered, root) + } + sort.Slice(ordered, func(i, j int) bool { return ordered[i].order < ordered[j].order }) + values := make([]ssa.Value, len(ordered)) + for index, root := range ordered { + values[index] = root.value + } + return values +} + +func coroTerminalResultAllocationSetMatches( + proof *coroFrameRetentionProof, + allocations []*ssa.Alloc, +) bool { + if proof == nil || len(proof.terminalResultAllocations) != len(allocations) { + return proof == nil && len(allocations) == 0 + } + seen := make(map[*ssa.Alloc]struct{}, len(allocations)) + for _, allocation := range allocations { + if allocation == nil { + return false + } + if _, duplicate := seen[allocation]; duplicate { + return false + } + seen[allocation] = struct{}{} + if _, selected := proof.terminalResultAllocations[allocation]; !selected { + return false + } + if _, managed := proof.managedHeapAllocations[allocation]; !managed { + return false + } + } + return true +} + +func (p *coroFrameRetentionProof) exactCallKeepaliveRoots(call *ssa.Call) []ssa.Value { + if p == nil || call == nil { + return nil + } + fact, ok := p.callKeepalives[call] + if !ok { + return nil + } + return append([]ssa.Value(nil), fact.roots...) +} + +// exactCallKeepaliveSources returns the exact transport values consumed by a +// bounded call. Unlike provenance roots, these values are guaranteed by Go +// SSA to dominate that call even when the root trace crossed a Phi component. +// Codegen must spill these sources at the call boundary and use roots only as +// the immutable proof that each source carries valid pointer provenance. +func (p *coroFrameRetentionProof) exactCallKeepaliveSources(call *ssa.Call) []ssa.Value { + if p == nil || call == nil { + return nil + } + fact, ok := p.callKeepalives[call] + if !ok { + return nil + } + return append([]ssa.Value(nil), fact.sources...) +} + +func (p *coroFrameRetentionProof) provesDominatedStableAddress(value ssa.Value, use ssa.Instruction) bool { + if p == nil || value == nil || use == nil { + return false + } + fact, ok := p.stableAddresses[coroFrameRetentionAddressUse{value: value, use: use}] + return ok && fact.nonNil +} + +func (p *coroFrameRetentionProof) provesGuardableStableAddress(value ssa.Value, use ssa.Instruction) bool { + if p == nil || value == nil || use == nil { + return false + } + _, ok := p.stableAddresses[coroFrameRetentionAddressUse{value: value, use: use}] + return ok +} + +func (p *coroFrameRetentionProof) requiresImplicitNilFault(value ssa.Value, use ssa.Instruction) bool { + if p == nil || value == nil || use == nil { + return false + } + fact, ok := p.stableAddresses[coroFrameRetentionAddressUse{value: value, use: use}] + return ok && !fact.nonNil +} + +func (p *coroFrameRetentionProof) provesTraceableUintptr(value ssa.Value) bool { + if p == nil || value == nil { + return false + } + _, ok := p.uintptrValues[value] + return ok +} + +func (a *coroPhysicalPureSSAAudit) frameRetainsAllocation(alloc *ssa.Alloc) bool { + proof := a.currentFrameRetentionProof() + if proof == nil { + return false + } + _, ok := proof.allocations[alloc] + return ok +} + +func (a *coroPhysicalPureSSAAudit) frameRetainsManagedHeapAllocation(alloc *ssa.Alloc) bool { + proof := a.currentFrameRetentionProof() + if proof == nil { + return false + } + _, ok := proof.managedHeapAllocations[alloc] + return ok +} + +func (a *coroPhysicalPureSSAAudit) currentFrameRetentionProof() *coroFrameRetentionProof { + if a == nil { + return nil + } + if !a.frameRetentionBuilt { + a.frameRetentionBuilt = true + a.frameRetentionProofCache = a.proveCurrentFrameRetention() + } + return a.frameRetentionProofCache +} + +func (a *coroPhysicalPureSSAAudit) proveCurrentFrameRetention() *coroFrameRetentionProof { + proof := &coroFrameRetentionProof{ + allocations: make(map[*ssa.Alloc]struct{}), + managedHeapAllocations: make(map[*ssa.Alloc]coroFrameRetentionManagedHeapAllocation), + terminalResultAllocations: make(map[*ssa.Alloc]struct{}), + exactRoots: make(map[ssa.Value]coroFrameRetentionExactRoot), + stableAddresses: make(map[coroFrameRetentionAddressUse]coroFrameRetentionAddressFact), + uintptrValues: make(map[ssa.Value]coroFrameRetentionUintptrFact), + callKeepalives: make(map[*ssa.Call]coroFrameRetentionCallFact), + } + if a.universe == nil || a.ctx == nil || a.fn == nil || emitShadowStackInstrumentation { + return proof + } + if a.frameRetentionABI == CoroFrameRetentionParkABIV2 { + a.proveParkFrameRetention(proof) + } + a.proveManagedHeapAllocations(proof) + terminalAllocations, err := coroStaticTerminalReconstructionAllocations(a.fn) + if err != nil { + // Static-cleanup preflight reports the precise structural error. Do not + // publish a root capability digest from a partial proof in the meantime. + return proof + } + for _, allocation := range terminalAllocations { + proof.terminalResultAllocations[allocation] = struct{}{} + } + newCoroFrameRetentionRootBuilder(a, proof).prove() + proof.rootDigest = coroFrameRetentionRootDigest(a, proof) + return proof +} + +func (a *coroPhysicalPureSSAAudit) proveParkFrameRetention(proof *coroFrameRetentionProof) { + if a == nil || proof == nil || a.universe == nil || a.fn == nil { + return + } + type candidate struct { + allocation *ssa.Alloc + prepare *ssa.Call + park *ssa.Call + } + var candidates []candidate + for _, block := range a.fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + semantics, intrinsic, err := coroIntrinsicCallSiteSemantics(a.universe, call) + if err != nil || !intrinsic || semantics != CoroIntrinsicCallInlineSuspend || + call.Common() == nil || len(call.Common().Args) != 2 { + continue + } + allocation := coroFrameRetentionDirectAllocRoot(call.Common().Args[0], make(map[ssa.Value]bool)) + if allocation == nil || allocation.Parent() != a.fn || !allocation.Heap || + a.ctx.skipSyntheticMakeSliceAlloc(allocation) || isEmissionVargsAlloc(a.ctx, allocation) { + continue + } + pointer, pointerOK := types.Unalias(a.typeOf(allocation.Type())).Underlying().(*types.Pointer) + if !pointerOK || coroTypeContainsGCPointer(pointer.Elem(), make(map[types.Type]bool)) { + continue + } + prepare, ok := a.coroParkBorrowPrepare(allocation, call) + if !ok || !coroFrameRetentionAddressUsesMatch( + allocation, + map[*ssa.Call]int{prepare: 0, call: 0}, + nil, + ) { + continue + } + candidates = append(candidates, candidate{allocation: allocation, prepare: prepare, park: call}) + } + } + allocationUses := make(map[*ssa.Alloc]int) + callUses := make(map[*ssa.Call]int) + for _, candidate := range candidates { + allocationUses[candidate.allocation]++ + callUses[candidate.prepare]++ + callUses[candidate.park]++ + } + for _, candidate := range candidates { + if allocationUses[candidate.allocation] != 1 || callUses[candidate.prepare] != 1 || callUses[candidate.park] != 1 { + continue + } + proof.allocations[candidate.allocation] = struct{}{} + } +} + +// coroParkBorrowPrepare selects the sole call which initializes one opaque +// park state before llgo.coroPark. The callable certificate, rather than a C +// symbol allow-list, proves that the call is executor-safe and borrows the +// state only until it returns. This is the extension boundary for every keyed +// event source: adding a source does not change compiler code. +func (a *coroPhysicalPureSSAAudit) coroParkBorrowPrepare(allocation *ssa.Alloc, park *ssa.Call) (*ssa.Call, bool) { + if a == nil || a.universe == nil || allocation == nil || park == nil || + allocation.Parent() != a.fn || park.Parent() != a.fn || park.Block() == nil { + return nil, false + } + aliases := map[ssa.Value]bool{allocation: true} + queue := []ssa.Value{allocation} + for head := 0; head < len(queue); head++ { + value := queue[head] + refs := value.Referrers() + if refs == nil { + return nil, false + } + for _, reference := range *refs { + var alias ssa.Value + switch instruction := reference.(type) { + case *ssa.ChangeType: + if instruction.X == value && coroFrameRetentionPointerLike(instruction.Type()) && coroFrameRetentionPointerLike(value.Type()) { + alias = instruction + } + case *ssa.Convert: + if instruction.X == value && coroFrameRetentionPointerLike(instruction.Type()) && coroFrameRetentionPointerLike(value.Type()) { + alias = instruction + } + } + if alias != nil && !aliases[alias] { + aliases[alias] = true + queue = append(queue, alias) + } + } + } + + var prepare *ssa.Call + for alias := range aliases { + refs := alias.Referrers() + if refs == nil { + return nil, false + } + for _, reference := range *refs { + call, ok := reference.(*ssa.Call) + if !ok || call == park { + continue + } + if prepare != nil && prepare != call || call.Common() == nil || call.Common().IsInvoke() || + len(call.Common().Args) == 0 || call.Common().Args[0] != alias || call.Block() != park.Block() || + coroFrameRetentionInstructionIndex(call) >= coroFrameRetentionInstructionIndex(park) { + return nil, false + } + callee := a.universe.canonicalAlias(call.Common().StaticCallee()) + certificate, certified := a.universe.callableContracts[callee] + if callee == nil || !certified || certificate.Scope != coro.CallableContractScopeDeclaration || + certificate.Contract.Progress != coro.ProgressExecutorSafe || + certificate.Contract.Reentry != coro.ReentryNone || + certificate.Contract.Memory != coro.MemoryBorrowUntilReturn || + (certificate.Contract.Affinity != coro.AffinityCallerThread && certificate.Contract.Affinity != coro.AffinityAnyThread) { + return nil, false + } + prepare = call + } + } + return prepare, prepare != nil +} + +// proveManagedHeapAllocations freezes the exact ordinary Go heap allocations +// whose managed allocator edge and suspended-frame root profile are both +// proven. Unlike proveParkFrameRetention, this never changes code generation +// to an alloca: escape identity and heap lifetime remain those of the source +// *ssa.Alloc. +func (a *coroPhysicalPureSSAAudit) proveManagedHeapAllocations(proof *coroFrameRetentionProof) { + if a == nil || a.fn == nil || proof == nil { + return + } + for _, block := range a.fn.Blocks { + for _, instruction := range block.Instrs { + alloc, ok := instruction.(*ssa.Alloc) + if !ok || !alloc.Heap { + continue + } + if _, frameLocal := proof.allocations[alloc]; frameLocal { + continue + } + fact, reason := a.managedHeapAllocationCapability(alloc) + if reason == "" { + proof.managedHeapAllocations[alloc] = fact + } + } + } +} + +// coroFrameRetentionRootBuilder proves a deliberately small transport model: +// exact source roots may be retained by LLVM's ordinary SSA liveness in a +// stackless coroutine frame, and otherwise-dead pointer sources converted to +// uintptr are attached to the exact bounded child/worker call that needs the +// Go uintptrkeepalive lifetime. It never treats an arbitrary pointer-shaped +// value or function name as evidence. +type coroFrameRetentionRootBuilder struct { + audit *coroPhysicalPureSSAAudit + proof *coroFrameRetentionProof + valueOrder map[ssa.Value]int + instrOrder map[ssa.Instruction]int + parameterPos map[*ssa.Parameter]int +} + +type coroFrameRetentionTrace struct { + roots map[ssa.Value]struct{} + evidence map[ssa.Instruction]struct{} +} + +func newCoroFrameRetentionRootBuilder(audit *coroPhysicalPureSSAAudit, proof *coroFrameRetentionProof) *coroFrameRetentionRootBuilder { + builder := &coroFrameRetentionRootBuilder{ + audit: audit, + proof: proof, + valueOrder: make(map[ssa.Value]int), + instrOrder: make(map[ssa.Instruction]int), + parameterPos: make(map[*ssa.Parameter]int), + } + next := 0 + if audit != nil && audit.fn != nil { + for _, free := range audit.fn.FreeVars { + if free == nil { + continue + } + builder.valueOrder[free] = next + next++ + } + for index, parameter := range audit.fn.Params { + if parameter == nil { + continue + } + builder.parameterPos[parameter] = index + builder.valueOrder[parameter] = next + next++ + } + for _, block := range audit.fn.Blocks { + for _, instruction := range block.Instrs { + builder.instrOrder[instruction] = next + if value, ok := instruction.(ssa.Value); ok { + builder.valueOrder[value] = next + } + next++ + } + } + } + return builder +} + +func (b *coroFrameRetentionRootBuilder) prove() { + if b == nil || b.audit == nil || b.audit.fn == nil || b.proof == nil { + return + } + // Ordinary escaping Allocs keep their Go heap identity. Recording the exact + // SSA pointer here states only that LLVM may spill that pointer into the + // scanned coroutine frame; it does not turn the referent into frame storage. + for allocation := range b.proof.managedHeapAllocations { + b.addExactRoot(allocation, coroFrameRetentionRootManagedHeapAllocation) + } + // First freeze the exact address/use pairs. This includes ordinary local + // struct fields so a later ABI consumer can distinguish "LLVM kept this + // exact alloca/address live" from a blanket local-pointer policy. + for _, block := range b.audit.fn.Blocks { + for _, instruction := range block.Instrs { + switch instruction := instruction.(type) { + case *ssa.FieldAddr: + b.recordStableAddress(instruction, instruction) + case *ssa.IndexAddr: + b.recordStableAddress(instruction, instruction) + case *ssa.UnOp: + if instruction.Op == token.MUL { + b.recordStableAddress(instruction.X, instruction) + } + case *ssa.Store: + b.recordStableAddress(instruction.Addr, instruction) + case *ssa.Slice: + // Slicing a *array retains the pointer transport independently of + // whether bounds are explicit. ExplicitStatus lowering owns the nil + // and bounds branches; this fact certifies only the exact root/use. + if _, pointer := types.Unalias(b.audit.typeOf(instruction.X.Type())).Underlying().(*types.Pointer); pointer { + b.recordStableAddress(instruction.X, instruction) + } + } + } + } + + // A pointer->uintptr value is certified only when every semantic use is a + // value-preserving integer conversion or one exact bounded + // managed-child/worker call. Returning, storing, arithmetic on, dynamically + // dispatching, or passing it to an arbitrary foreign declaration leaves it + // uncertified. The conversion chain is deliberately one-way: converting an + // integer alias back to a pointer is still admitted only by the separate, + // exact same-expression roundtrip proof below. + for _, block := range b.audit.fn.Blocks { + for _, instruction := range block.Instrs { + conversion, ok := instruction.(*ssa.Convert) + if ok && coroFrameRetentionPointerToUintptr(conversion) { + b.proveUintptrKeepalive(conversion) + } + } + } + // Pointer/slice arguments to a static managed child are already typed, but + // their source root still belongs in the digest and in the exact call fact. + // Nil is legal for transport; dereference sites require their own dominance + // proof above. + for _, block := range b.audit.fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + kind, bounded := b.boundedCallKind(call) + if !bounded || call.Common() == nil { + continue + } + for _, argument := range call.Common().Args { + var trace coroFrameRetentionTrace + var traced bool + switch types.Unalias(b.audit.typeOf(argument.Type())).Underlying().(type) { + case *types.Pointer: + trace, traced = b.traceAddress(argument, call, false, make(map[ssa.Value]bool)) + case *types.Slice: + trace, traced = b.traceSlice(argument, call, make(map[ssa.Value]bool)) + case *types.Basic: + if coroFrameRetentionUnsafePointer(argument.Type()) { + trace, traced = b.traceAddress(argument, call, false, make(map[ssa.Value]bool)) + } + } + if traced { + b.mergeCallFact(call, kind, trace.roots, []ssa.Value{argument}) + } + } + } + } +} + +func (b *coroFrameRetentionRootBuilder) recordStableAddress(value ssa.Value, use ssa.Instruction) { + if value == nil || use == nil { + return + } + // Freeze transport/root provenance independently of nil-access safety. A + // nullable parameter is a sound exact frame root; rejecting it here would + // conflate liveness with Go's implicit nil-dereference semantics. + trace, ok := b.traceAddress(value, use, false, make(map[ssa.Value]bool)) + if !ok { + return + } + fact := coroFrameRetentionAddressFact{ + roots: b.sortedValues(trace.roots), + evidence: b.sortedInstructions(trace.evidence), + } + // A second, stricter trace proves that this exact use needs no compiler + // fault edge. It may succeed with no dynamic evidence for globals/allocas; + // retain the explicit boolean rather than overloading evidence length. + if nonNil, proved := b.traceAddress(value, use, true, make(map[ssa.Value]bool)); proved { + fact.roots = b.sortedValues(nonNil.roots) + fact.evidence = b.sortedInstructions(nonNil.evidence) + fact.nonNil = true + } + b.proof.stableAddresses[coroFrameRetentionAddressUse{value: value, use: use}] = fact +} + +func (b *coroFrameRetentionRootBuilder) traceAddress(value ssa.Value, use ssa.Instruction, requireNonNil bool, visiting map[ssa.Value]bool) (coroFrameRetentionTrace, bool) { + trace := newCoroFrameRetentionTrace() + if value == nil || visiting[value] { + return trace, false + } + visiting[value] = true + defer delete(visiting, value) + switch value := value.(type) { + case *ssa.Global: + _, ok := types.Unalias(b.audit.typeOf(value.Type())).Underlying().(*types.Pointer) + return trace, ok + case *ssa.Const: + // x/tools Const.IsNil omits the basic unsafe.Pointer type. The shared + // SSA helper uses the representation-level Value == nil fact after the + // surrounding address trace has already required a pointer-like type. + return trace, !requireNonNil && coroFrameRetentionNilConst(value) + case *ssa.Parameter: + if !coroFrameRetentionPointerLike(value.Type()) { + return trace, false + } + if requireNonNil { + evidence, ok := b.dominatingNonNilEvidence(value, use) + if !ok { + return trace, false + } + trace.addEvidence(evidence...) + } + kind := coroFrameRetentionRootPointerParameter + if index, ok := b.parameterPos[value]; ok && index == 0 && b.audit.fn.Signature != nil && b.audit.fn.Signature.Recv() != nil { + kind = coroFrameRetentionRootReceiver + } + b.addExactRoot(value, kind) + trace.addRoot(value) + return trace, true + case *ssa.FreeVar: + // A FreeVar in a capability-certified captured coroutine entry is a + // pointer to one exact closure cell loaded from the typed descriptor + // environment. The environment and this value may be retained by the + // LLVM coroutine frame, but capture does not prove the cell pointer is + // non-nil: each access still needs dominating evidence or the explicit + // compiler-owned nil-fault edge. + if !b.exactCoroClosureFreeVar(value) { + return trace, false + } + if requireNonNil { + evidence, ok := b.dominatingNonNilEvidence(value, use) + if !ok { + return trace, false + } + trace.addEvidence(evidence...) + } + b.addExactRoot(value, coroFrameRetentionRootClosureFreeVar) + trace.addRoot(value) + return trace, true + case *ssa.Alloc: + kind := coroFrameRetentionRootLocalAddress + if value.Heap { + if _, managed := b.proof.managedHeapAllocations[value]; managed { + kind = coroFrameRetentionRootManagedHeapAllocation + } else if _, retained := b.proof.allocations[value]; !retained { + return trace, false + } + } + if b.audit.ctx != nil && (b.audit.ctx.skipSyntheticMakeSliceAlloc(value) || isEmissionVargsAlloc(b.audit.ctx, value)) { + return trace, false + } + b.addExactRoot(value, kind) + trace.addRoot(value) + return trace, true + case *ssa.FieldAddr: + pointer, ok := types.Unalias(b.audit.typeOf(value.X.Type())).Underlying().(*types.Pointer) + if !ok { + return trace, false + } + structure, ok := types.Unalias(pointer.Elem()).Underlying().(*types.Struct) + if !ok || value.Field < 0 || value.Field >= structure.NumFields() { + return trace, false + } + return b.traceAddress(value.X, use, requireNonNil, visiting) + case *ssa.IndexAddr: + underlying := types.Unalias(b.audit.typeOf(value.X.Type())).Underlying() + switch container := underlying.(type) { + case *types.Pointer: + array, ok := types.Unalias(container.Elem()).Underlying().(*types.Array) + if !ok { + return trace, false + } + if coroConstantIndexInBounds(value.Index, array.Len()) { + return b.traceAddress(value.X, use, requireNonNil, visiting) + } + trace, ok = b.traceAddress(value.X, use, false, visiting) + if !ok { + return trace, false + } + if evidence, bounded := b.dominatingFixedArrayIndexEvidence(value.Index, array.Len(), value); bounded { + trace.addEvidence(evidence...) + if requireNonNil { + nonNil, proved := b.traceAddress(value.X, use, true, visiting) + if !proved { + return newCoroFrameRetentionTrace(), false + } + trace.merge(nonNil) + } + } else if requireNonNil { + // Transporting the fixed-array pointer and its derived address is + // frame-safe, but code generation must take both the bounds fault + // and possible nil fault edges before forming the GEP. + return newCoroFrameRetentionTrace(), false + } + return trace, true + case *types.Slice: + traced, ok := b.traceSlice(value.X, use, visiting) + if !ok { + return trace, false + } + trace = traced + if evidence, bounded := b.dominatingSliceIndexEvidence(value.X, value.Index, value); bounded { + trace.addEvidence(evidence...) + } else if requireNonNil { + // Transporting the slice and derived address is safe under the + // selected frame-root profile, but dereferencing it requires the + // compiler-owned bounds branch first. + return newCoroFrameRetentionTrace(), false + } + return trace, true + default: + return trace, false + } + case *ssa.SliceToArrayPointer: + length, exact := coroSliceToArrayPointerLen(value, b.audit.typeOf) + if !exact { + return trace, false + } + trace, ok := b.traceSlice(value.X, use, visiting) + if !ok { + return trace, false + } + if requireNonNil && length == 0 { + // The zero-length conversion intentionally preserves a nil slice data + // word. Keep an exact dominating p!=nil fact when present; a synthetic + // [0]T value load is recognized separately and does not request this + // strict trace, while every unguarded explicit dereference remains + // guardable through the explicit-status nil fault. + evidence, nonNil := b.dominatingNonNilEvidence(value, use) + if !nonNil { + return newCoroFrameRetentionTrace(), false + } + trace.addEvidence(evidence...) + } + // For N>0, reaching a use of the conversion means its len>=N check + // completed normally, which also proves a non-nil data pointer. + return trace, true + case *ssa.ChangeType: + if value.X != nil && coroFrameRetentionPointerLike(value.Type()) && coroFrameRetentionPointerLike(value.X.Type()) { + return b.traceAddress(value.X, use, requireNonNil, visiting) + } + case *ssa.Convert: + if value.X != nil && coroFrameRetentionPointerLike(value.Type()) && coroFrameRetentionPointerLike(value.X.Type()) { + return b.traceAddress(value.X, use, requireNonNil, visiting) + } + case *ssa.Call: + if coroPhysicalUnsafeAddCall(value, b.audit.typeOf) { + return b.traceAddress(value.Common().Args[0], use, requireNonNil, visiting) + } + case *ssa.Phi: + return b.traceAddressPhiComponent(value, use, requireNonNil, visiting) + } + // A pointer-producing SSA value owned by this function is itself an exact + // transport root under the current non-moving/conservative-or-no-GC frame + // profile. Its producer is audited independently; a dereference additionally + // requires a dominating non-nil fact. + if coroFrameRetentionPointerLike(value.Type()) { + if _, local := b.valueOrder[value]; !local { + return trace, false + } + if requireNonNil { + evidence, ok := b.dominatingNonNilEvidence(value, use) + if !ok { + return trace, false + } + trace.addEvidence(evidence...) + } + b.addExactRoot(value, coroFrameRetentionRootLocalAddress) + trace.addRoot(value) + return trace, true + } + return trace, false +} + +// traceAddressPhiComponent treats mutually recursive pointer phis as one +// transport component. Requiring every recursively visited phi to discover an +// independent seed makes a valid loop SCC depend on DFS order (and rejected +// map bucket loops with several mutually recursive merge nodes). The component +// proof instead traces every external edge exactly once and requires at least +// one such seed; a closed phi-only cycle remains rejected. +func (b *coroFrameRetentionRootBuilder) traceAddressPhiComponent( + root *ssa.Phi, + use ssa.Instruction, + requireNonNil bool, + visiting map[ssa.Value]bool, +) (coroFrameRetentionTrace, bool) { + trace := newCoroFrameRetentionTrace() + if root == nil || len(root.Edges) == 0 || !coroFrameRetentionPointerLike(root.Type()) { + return trace, false + } + component := map[*ssa.Phi]bool{root: true} + queue := []*ssa.Phi{root} + for head := 0; head < len(queue); head++ { + phi := queue[head] + if !coroFrameRetentionPointerLike(phi.Type()) { + return newCoroFrameRetentionTrace(), false + } + for _, edge := range phi.Edges { + if nested, ok := edge.(*ssa.Phi); ok && !component[nested] { + component[nested] = true + queue = append(queue, nested) + } + } + } + + edgeRequiresNonNil := requireNonNil + if requireNonNil { + // One dominating check of the selected merged value proves whichever + // incoming edge reaches this use; transport ownership is still traced + // through every external edge below. + if evidence, guarded := b.dominatingNonNilEvidence(root, use); guarded { + trace.addEvidence(evidence...) + edgeRequiresNonNil = false + } + } + componentVisiting := make(map[ssa.Value]bool, len(visiting)+len(component)) + for value, active := range visiting { + componentVisiting[value] = active + } + for phi := range component { + componentVisiting[phi] = true + } + externalSeeds := 0 + for _, phi := range queue { + for _, edge := range phi.Edges { + if nested, ok := edge.(*ssa.Phi); ok && component[nested] { + continue + } + edgeUse, _ := edge.(ssa.Instruction) + if edgeUse == nil { + edgeUse = phi + } + part, ok := b.traceAddress(edge, edgeUse, edgeRequiresNonNil, componentVisiting) + if !ok { + return newCoroFrameRetentionTrace(), false + } + trace.merge(part) + externalSeeds++ + } + } + return trace, externalSeeds != 0 +} + +func (b *coroFrameRetentionRootBuilder) exactCoroClosureFreeVar(value *ssa.FreeVar) bool { + if b == nil || b.audit == nil || b.audit.fn == nil || b.audit.plan == nil || + b.audit.universe == nil || value == nil || !coroFrameRetentionPointerLike(value.Type()) { + return false + } + found := false + for _, free := range b.audit.fn.FreeVars { + if free == value { + found = true + break + } + } + if !found { + return false + } + function, planned := b.audit.plan.FunctionPlan(b.audit.fn) + if !planned || function.External != coro.Defined || function.Emission != coro.EmitCoroutine || + function.Primary != coro.PrimaryCoroutine || + (function.FuncRep != coro.Dispatch && function.FuncRep != coro.DirectCoro) { + return false + } + effective, err := b.audit.universe.coroPhysicalEntrySourceSignature(b.audit.fn) + return err == nil && effective != nil && effective.Params().Len() != 0 && + coroPhysicalClosureContextMatches(b.audit.fn, effective.Params().At(0).Type()) +} + +func (b *coroFrameRetentionRootBuilder) traceSlice(value ssa.Value, use ssa.Instruction, visiting map[ssa.Value]bool) (coroFrameRetentionTrace, bool) { + trace := newCoroFrameRetentionTrace() + if value == nil { + return trace, false + } + if constantValue, ok := value.(*ssa.Const); ok { + return trace, constantValue.IsNil() + } + if !coroFrameRetentionSliceLike(b.audit.typeOf(value.Type())) { + return trace, false + } + if _, local := b.valueOrder[value]; !local { + return trace, false + } + kind := coroFrameRetentionRootLocalSlice + if _, parameter := value.(*ssa.Parameter); parameter { + kind = coroFrameRetentionRootSliceParameter + } + b.addExactRoot(value, kind) + trace.addRoot(value) + return trace, true +} + +func (b *coroFrameRetentionRootBuilder) proveUintptrKeepalive(conversion *ssa.Convert) { + trace, ok := b.traceAddress(conversion.X, conversion, false, make(map[ssa.Value]bool)) + if !ok { + return + } + aliases, calls, ok := b.boundedUintptrUses(conversion) + if !ok || len(calls) == 0 { + // Go also permits an exact pointer -> uintptr arithmetic -> pointer + // roundtrip in one expression. Keep that proof separate from the + // managed-call uintptrkeepalive proof above: a roundtrip has a single + // linear address-word lifetime and no call terminal that could silently + // broaden the older capability. + aliases, ok = b.exactUintptrRoundtripUses(conversion) + if !ok { + return + } + calls = nil + } + roots := b.sortedValues(trace.roots) + for alias := range aliases { + b.proof.uintptrValues[alias] = coroFrameRetentionUintptrFact{roots: append([]ssa.Value(nil), roots...)} + } + sources := b.sortedValueSet(aliases) + for call, kind := range calls { + b.mergeCallFact(call, kind, trace.roots, sources) + } +} + +// exactUintptrRoundtripUses recognizes the deliberately narrow SSA image of +// the unsafe.Pointer rule that permits address arithmetic between a +// pointer->uintptr conversion and the conversion back to a pointer in the same +// expression. x/tools SSA does not retain expression nodes, so we require the +// stronger structural surrogate used here: one linear semantic-use chain in +// one basic block, with exactly one pointer reconstruction terminal. +// +// A managed child may still suspend between instructions in that block. Under +// the selected non-moving conservative/no-GC profile the uintptr SSA value is +// then spilled in the coroutine frame and remains an address-shaped scanned +// word. This is not a typed root-map or moving-GC proof, and the capability +// profile above intentionally prevents either consumer from claiming it. +func (b *coroFrameRetentionRootBuilder) exactUintptrRoundtripUses(root ssa.Value) (map[ssa.Value]struct{}, bool) { + rootInstruction, ok := root.(ssa.Instruction) + if !ok || rootInstruction.Block() == nil || !coroFrameRetentionUintptrLike(root.Type()) { + return nil, false + } + block := rootInstruction.Block() + aliases := map[ssa.Value]struct{}{root: {}} + queue := []ssa.Value{root} + pointerTerminals := 0 + for head := 0; head < len(queue); head++ { + value := queue[head] + refs := value.Referrers() + if refs == nil { + return nil, false + } + semanticUses := 0 + for _, reference := range *refs { + switch instruction := reference.(type) { + case *ssa.DebugRef: + case *ssa.ChangeType: + if instruction.Block() != block || instruction.X != value || + !coroFrameRetentionUintptrLike(value.Type()) || !coroFrameRetentionUintptrLike(instruction.Type()) { + return nil, false + } + semanticUses++ + if _, seen := aliases[instruction]; !seen { + aliases[instruction] = struct{}{} + queue = append(queue, instruction) + } + case *ssa.Convert: + if instruction.Block() != block || instruction.X != value || !coroFrameRetentionUintptrLike(value.Type()) { + return nil, false + } + semanticUses++ + switch { + case coroFrameRetentionUintptrLike(instruction.Type()): + if _, seen := aliases[instruction]; !seen { + aliases[instruction] = struct{}{} + queue = append(queue, instruction) + } + case coroFrameRetentionPointerLike(instruction.Type()): + pointerTerminals++ + default: + return nil, false + } + case *ssa.BinOp: + if instruction.Block() != block || !coroFrameRetentionUintptrLike(instruction.Type()) || + !coroFrameRetentionUintptrLike(instruction.X.Type()) || !coroFrameRetentionUintptrLike(instruction.Y.Type()) { + return nil, false + } + xProvenance := instruction.X == value + yProvenance := instruction.Y == value + if xProvenance == yProvenance { // neither operand, or value+value + return nil, false + } + other := instruction.Y + if yProvenance { + other = instruction.X + } + if _, alreadyProvenance := aliases[other]; alreadyProvenance || coroFrameRetentionIntegerHasPointerProvenance(other, make(map[ssa.Value]bool)) { + return nil, false + } + switch instruction.Op { + case token.ADD: + case token.SUB: + // Address-minus-offset preserves provenance; offset-minus-address + // does not denote the same allocation. + if !xProvenance { + return nil, false + } + default: + return nil, false + } + semanticUses++ + if _, seen := aliases[instruction]; !seen { + aliases[instruction] = struct{}{} + queue = append(queue, instruction) + } + default: + return nil, false + } + } + // One use is what makes this a single expression-shaped lifetime rather + // than a stored/reused uintptr program variable. It also prevents one + // pointer word from being reconstructed on only some CFG paths. + if semanticUses != 1 { + return nil, false + } + } + if pointerTerminals != 1 { + return nil, false + } + return aliases, true +} + +// coroFrameRetentionIntegerHasPointerProvenance rejects an offset operand that +// is itself derived from another pointer word. Parameters and call results are +// valid scalar offsets; only an SSA derivation that visibly contains a pointer +// conversion is provenance-bearing here. +func coroFrameRetentionIntegerHasPointerProvenance(value ssa.Value, visiting map[ssa.Value]bool) bool { + if value == nil || visiting[value] { + return false + } + visiting[value] = true + defer delete(visiting, value) + switch value := value.(type) { + case *ssa.Convert: + if value.X == nil { + return false + } + if coroFrameRetentionPointerLike(value.X.Type()) && coroFrameRetentionUintptrLike(value.Type()) { + return true + } + if coroFrameRetentionUintptrLike(value.Type()) { + return coroFrameRetentionIntegerHasPointerProvenance(value.X, visiting) + } + case *ssa.ChangeType: + if value.X != nil && coroFrameRetentionUintptrLike(value.Type()) { + return coroFrameRetentionIntegerHasPointerProvenance(value.X, visiting) + } + case *ssa.BinOp: + if coroFrameRetentionUintptrLike(value.Type()) { + return coroFrameRetentionIntegerHasPointerProvenance(value.X, visiting) || + coroFrameRetentionIntegerHasPointerProvenance(value.Y, visiting) + } + case *ssa.Phi: + for _, edge := range value.Edges { + if coroFrameRetentionIntegerHasPointerProvenance(edge, visiting) { + return true + } + } + } + return false +} + +func (b *coroFrameRetentionRootBuilder) boundedUintptrUses(root ssa.Value) (map[ssa.Value]struct{}, map[*ssa.Call]coroFrameRetentionCallKindV1, bool) { + aliases := map[ssa.Value]struct{}{root: {}} + calls := make(map[*ssa.Call]coroFrameRetentionCallKindV1) + queue := []ssa.Value{root} + for head := 0; head < len(queue); head++ { + value := queue[head] + refs := value.Referrers() + if refs == nil { + return nil, nil, false + } + semanticUses := 0 + for _, reference := range *refs { + switch instruction := reference.(type) { + case *ssa.DebugRef: + case *ssa.ChangeType: + if instruction.X != value || !coroFrameRetentionIntegerLike(instruction.Type()) || !coroFrameRetentionIntegerLike(value.Type()) { + return nil, nil, false + } + semanticUses++ + if _, seen := aliases[instruction]; !seen { + aliases[instruction] = struct{}{} + queue = append(queue, instruction) + } + case *ssa.Convert: + if instruction.X != value || !coroFrameRetentionIntegerLike(instruction.Type()) || !coroFrameRetentionIntegerLike(value.Type()) { + return nil, nil, false + } + semanticUses++ + if _, seen := aliases[instruction]; !seen { + aliases[instruction] = struct{}{} + queue = append(queue, instruction) + } + case *ssa.Phi: + if !coroFrameRetentionIntegerLike(instruction.Type()) || !coroFrameRetentionIntegerLike(value.Type()) { + return nil, nil, false + } + semanticUses++ + if _, seen := aliases[instruction]; !seen { + aliases[instruction] = struct{}{} + queue = append(queue, instruction) + } + case *ssa.Call: + if b.exactScalarBitcastTransform(instruction, value) { + semanticUses++ + if _, seen := aliases[instruction]; !seen { + aliases[instruction] = struct{}{} + queue = append(queue, instruction) + } + continue + } + kind, bounded := b.boundedUintptrCallKind(instruction, value) + if !bounded || instruction.Common() == nil { + return nil, nil, false + } + matches := 0 + for _, argument := range instruction.Common().Args { + if argument == value { + matches++ + } + } + if matches == 0 { + return nil, nil, false + } + semanticUses += matches + if previous, exists := calls[instruction]; exists && previous != kind { + return nil, nil, false + } + calls[instruction] = kind + default: + return nil, nil, false + } + } + if semanticUses == 0 { + return nil, nil, false + } + } + return aliases, calls, true +} + +// exactScalarBitcastTransform recognizes one defined, side-effect-free Go SSA +// body that reinterprets all bits of a single scalar parameter as its +// same-width scalar result. The plan checks alone are intentionally +// insufficient: an arbitrary DirectPlain function can still store its input. +// The body proof below binds the call to the exact local +// store -> unsafe-pointer conversions -> load -> return shape, so the result +// may continue the pointer-word provenance chain until its final managed-child +// terminal. +func (b *coroFrameRetentionRootBuilder) exactScalarBitcastTransform(call *ssa.Call, value ssa.Value) bool { + if b == nil || b.audit == nil || b.audit.plan == nil || b.audit.universe == nil || + call == nil || call.Common() == nil || call.Common().IsInvoke() || call.Parent() != b.audit.fn || + value == nil || len(call.Common().Args) != 1 || call.Common().Args[0] != value { + return false + } + callee := call.Common().StaticCallee() + if callee == nil { + return false + } + canonical := b.audit.universe.canonicalAlias(callee) + if canonical == nil || len(canonical.Blocks) != 1 || canonical.Signature == nil || + canonical.Signature.Recv() != nil || canonical.Signature.Variadic() || + canonical.Signature.Params().Len() != 1 || canonical.Signature.Results().Len() != 1 { + return false + } + plan, planned := b.audit.plan.FunctionPlan(canonical) + if !planned || plan.External != coro.Defined || plan.Demand == coro.NoDemand || + plan.Emission != coro.EmitPlain || plan.Primary != coro.PrimaryPlain || plan.FuncRep != coro.DirectPlain || + plan.Effect != coro.NoSuspend || plan.Exec != 0 { + return false + } + source := b.audit.typeOf(canonical.Signature.Params().At(0).Type()) + target := b.audit.typeOf(canonical.Signature.Results().At(0).Type()) + if !types.Identical(b.audit.typeOf(value.Type()), source) || + !types.Identical(b.audit.typeOf(call.Type()), target) { + return false + } + _, exact := coro.ProveSSAExactScalarBitcast(canonical) + return exact +} + +// boundedUintptrCallKind extends the ordinary exact-call proof with one +// compiler-owned composite lowering: builtin print/println. The builtin does +// not have an SSA StaticCallee, but LLSSA lowers each operand through one +// owner-scoped runtime Print* edge. Admit a pointer-derived integer operand +// only when the complete builtin lowering is frozen and the helper for this +// exact operand is a demanded coroutine child. A plain, foreign, elided, or +// otherwise unresolved helper is not a uintptr keepalive terminal. +func (b *coroFrameRetentionRootBuilder) boundedUintptrCallKind(call *ssa.Call, value ssa.Value) (coroFrameRetentionCallKindV1, bool) { + if kind, bounded := b.boundedCallKind(call); bounded { + return kind, true + } + if b.boundedManagedPrintArgument(call, value) { + return coroFrameRetentionCallManagedChildV1, true + } + return coroFrameRetentionCallInvalidV1, false +} + +func (b *coroFrameRetentionRootBuilder) boundedManagedPrintArgument(call *ssa.Call, value ssa.Value) bool { + if b == nil || b.audit == nil || b.audit.plan == nil || b.audit.universe == nil || + call == nil || call.Common() == nil || call.Parent() != b.audit.fn || value == nil { + return false + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "print" && builtin.Name() != "println" || + b.audit.validatePrintBuiltin(call, builtin.Name()) != "" { + return false + } + found := false + for _, argument := range call.Common().Args { + if argument != value { + continue + } + found = true + helper := runtimePrintHelper(b.audit.typeOf(argument.Type())) + target, planned := b.audit.plan.ResolveLoweredCall(b.audit.fn, helper) + if !planned || target == nil { + return false + } + plan, planned := b.audit.plan.FunctionPlan(target) + if !planned || plan.External != coro.Defined || plan.Emission != coro.EmitCoroutine || + plan.Primary != coro.PrimaryCoroutine || + (plan.FuncRep != coro.DirectCoro && plan.FuncRep != coro.Dispatch) || + !plan.Demand.Contains(coro.AsyncDemand) || !plan.Effect.MaySuspend() { + return false + } + } + return found +} + +func (b *coroFrameRetentionRootBuilder) boundedCallKind(call *ssa.Call) (coroFrameRetentionCallKindV1, bool) { + if call == nil || call.Common() == nil || call.Common().IsInvoke() || call.Parent() != b.audit.fn { + return coroFrameRetentionCallInvalidV1, false + } + // A generic park prepare is a bounded lifetime edge only when the immutable + // proof selected its first argument as frame-owned opaque state. The proof + // has already joined the exact call with its borrow-until-return callable + // certificate; no event-source symbol is recovered here. + if len(call.Common().Args) != 0 { + allocation := coroFrameRetentionDirectAllocRoot(call.Common().Args[0], make(map[ssa.Value]bool)) + if _, retained := b.proof.allocations[allocation]; retained { + semantics, intrinsic, err := coroIntrinsicCallSiteSemantics(b.audit.universe, call) + if err == nil && (!intrinsic || semantics != CoroIntrinsicCallInlineSuspend) { + return coroFrameRetentionCallParkOwnerV1, true + } + } + } + if b.audit.universe.CoroWorkerEnabled() { + semantics, intrinsic, err := coroIntrinsicCallSiteSemantics(b.audit.universe, call) + if err == nil && intrinsic && semantics == CoroIntrinsicCallInlineSuspend { + workerCertified := false + if b.audit.plan == nil { + // Report-only physical audits have no lowering authority. They may + // consume the immutable universe proof to inspect frame roots; real + // preflight/codegen always joins it with the exact SSA plan below. + certificate, certified, certificateErr := b.audit.universe.CoroWorkerSyscallCertificate(call) + workerCertified = certificateErr == nil && certified && certificate.ID != "" + } else { + workerCertified = validateCoroWorkerSyscallCall(b.audit.plan, b.audit.universe, call) == nil + } + if workerCertified { + return coroFrameRetentionCallWorkerV1, true + } + } + if _, recognized, foreignErr := validateCoroWorkerForeignCall( + b.audit.plan, b.audit.universe, call, b.audit.universe.prog.PointerSize(), + ); recognized && foreignErr == nil { + return coroFrameRetentionCallWorkerV1, true + } + } + // A capability-aware dynamic descriptor call may create a child just like + // an exact static coroutine call. Admit it only after the same immutable + // CallPlan/ValuePlan/target validation used by preflight, and only when the + // call can actually select a coroutine primary. This keeps pointer, slice, + // and pointer-derived uintptr sources rooted in the parent's frame while the + // dynamically selected child is running. + if b.audit.plan != nil { + if callPlan, planned := b.audit.plan.CallPlan(call); planned && + callPlan.Rep == coro.Dispatch && !callPlan.SyncDispatch && + (callPlan.Open || coroDispatchCallHasCoroutineTarget(b.audit.plan, callPlan)) { + ownerPlan, ownerPlanned := b.audit.plan.FunctionPlan(b.audit.fn) + if ownerPlanned && ownerPlan.Emission == coro.EmitCoroutine && ownerPlan.Primary == coro.PrimaryCoroutine && + validateCoroManagedDispatchCall(b.audit.plan, b.audit.fn, call, callPlan, b.audit.universe) == nil { + return coroFrameRetentionCallManagedChildV1, true + } + } + } + callee := call.Common().StaticCallee() + if callee == nil { + return coroFrameRetentionCallInvalidV1, false + } + canonical := b.audit.universe.canonicalAlias(callee) + if canonical == nil || len(canonical.Blocks) == 0 { + return coroFrameRetentionCallInvalidV1, false + } + if _, frozen := b.audit.universe.required[canonical]; !frozen { + return coroFrameRetentionCallInvalidV1, false + } + return coroFrameRetentionCallManagedChildV1, true +} + +func (b *coroFrameRetentionRootBuilder) mergeCallFact(call *ssa.Call, kind coroFrameRetentionCallKindV1, roots map[ssa.Value]struct{}, sources []ssa.Value) { + if call == nil || kind == coroFrameRetentionCallInvalidV1 { + return + } + fact := b.proof.callKeepalives[call] + if fact.kind != coroFrameRetentionCallInvalidV1 && fact.kind != kind { + delete(b.proof.callKeepalives, call) + return + } + fact.kind = kind + rootSet := make(map[ssa.Value]struct{}, len(fact.roots)+len(roots)) + for _, value := range fact.roots { + rootSet[value] = struct{}{} + } + for value := range roots { + rootSet[value] = struct{}{} + } + sourceSet := make(map[ssa.Value]struct{}, len(fact.sources)+len(sources)) + for _, value := range fact.sources { + sourceSet[value] = struct{}{} + } + for _, value := range sources { + if value != nil { + sourceSet[value] = struct{}{} + } + } + fact.roots = b.sortedValues(rootSet) + fact.sources = b.sortedValueSet(sourceSet) + b.proof.callKeepalives[call] = fact +} + +func (b *coroFrameRetentionRootBuilder) addExactRoot(value ssa.Value, kind coroFrameRetentionRootKind) { + if value == nil || kind == coroFrameRetentionRootInvalid { + return + } + order, ok := b.valueOrder[value] + if !ok { + return + } + if previous, exists := b.proof.exactRoots[value]; exists { + if previous.kind != kind { + delete(b.proof.exactRoots, value) + } + return + } + b.proof.exactRoots[value] = coroFrameRetentionExactRoot{value: value, kind: kind, order: order} +} + +func (b *coroFrameRetentionRootBuilder) dominatingNonNilEvidence(value ssa.Value, use ssa.Instruction) ([]ssa.Instruction, bool) { + if value == nil || use == nil || use.Block() == nil { + return nil, false + } + for _, block := range b.audit.fn.Blocks { + if len(block.Instrs) == 0 || len(block.Succs) != 2 { + continue + } + branch, ok := block.Instrs[len(block.Instrs)-1].(*ssa.If) + if !ok { + continue + } + comparison, ok := branch.Cond.(*ssa.BinOp) + if !ok || (comparison.Op != token.EQL && comparison.Op != token.NEQ) { + continue + } + matches := (comparison.X == value && coroFrameRetentionNilConst(comparison.Y)) || + (comparison.Y == value && coroFrameRetentionNilConst(comparison.X)) + if !matches { + continue + } + successor := 0 + if comparison.Op == token.EQL { + successor = 1 + } + if block.Succs[successor].Dominates(use.Block()) { + return []ssa.Instruction{comparison, branch}, true + } + } + return nil, false +} + +func (b *coroFrameRetentionRootBuilder) dominatingNonEmptySliceEvidence(value ssa.Value, use ssa.Instruction) ([]ssa.Instruction, bool) { + if value == nil || use == nil || use.Block() == nil { + return nil, false + } + for _, block := range b.audit.fn.Blocks { + if len(block.Instrs) == 0 || len(block.Succs) != 2 { + continue + } + branch, ok := block.Instrs[len(block.Instrs)-1].(*ssa.If) + if !ok { + continue + } + comparison, ok := branch.Cond.(*ssa.BinOp) + if !ok { + continue + } + lenCall, zeroOnRight := coroFrameRetentionLenZeroComparison(comparison, value) + if lenCall == nil { + continue + } + successor, proves := coroFrameRetentionPositiveLengthSuccessor(comparison.Op, zeroOnRight) + if proves && block.Succs[successor].Dominates(use.Block()) { + return []ssa.Instruction{lenCall, comparison, branch}, true + } + } + return nil, false +} + +// dominatingSliceIndexEvidence recognizes the two canonical x/tools SSA range +// shapes. In both forms the true edge of `index < len(slice)` dominates the +// IndexAddr, while the induction variable starts at zero (or -1 immediately +// before a +1) and advances by one. The comparison therefore proves both the +// lower and upper bounds without treating an arbitrary slice address as stable. +func (b *coroFrameRetentionRootBuilder) dominatingSliceIndexEvidence( + slice, index ssa.Value, + use ssa.Instruction, +) ([]ssa.Instruction, bool) { + if slice == nil || index == nil || use == nil || use.Block() == nil { + return nil, false + } + if coroFrameRetentionExactZeroIndex(index) { + return b.dominatingNonEmptySliceEvidence(slice, use) + } + if subtraction, ok := index.(*ssa.BinOp); ok && subtraction.Op == token.SUB && + coroFrameRetentionExactLenCall(subtraction.X, slice) != nil && + coroFrameRetentionExactInteger(subtraction.Y, 1) { + evidence, proved := b.dominatingNonEmptySliceEvidence(slice, use) + if proved { + return append(evidence, subtraction), true + } + } + for _, block := range b.audit.fn.Blocks { + if len(block.Instrs) == 0 || len(block.Succs) != 2 { + continue + } + branch, ok := block.Instrs[len(block.Instrs)-1].(*ssa.If) + if !ok { + continue + } + comparison, ok := branch.Cond.(*ssa.BinOp) + if !ok || comparison.Op != token.LSS || comparison.X != index { + continue + } + lenCall := coroFrameRetentionExactLenCall(comparison.Y, slice) + if lenCall == nil || !block.Succs[0].Dominates(use.Block()) { + continue + } + inductionEvidence, ok := coroFrameRetentionNonNegativeRangeIndex(index, block, block.Succs[0], use, 0) + if !ok { + continue + } + evidence := append([]ssa.Instruction(nil), inductionEvidence...) + evidence = append(evidence, lenCall, comparison, branch) + return evidence, true + } + return nil, false +} + +// dominatingFixedArrayIndexEvidence accepts the canonical SSA induction shape +// only when a true loop edge proves index < limit and the constant limit fits +// the frozen array bound. Unlike a slice, the array storage is already part of +// its traced base root; this proof exists solely to make the implicit bounds +// helper unreachable. +func (b *coroFrameRetentionRootBuilder) dominatingFixedArrayIndexEvidence( + index ssa.Value, + bound int64, + use ssa.Instruction, +) ([]ssa.Instruction, bool) { + if b == nil || b.audit == nil || b.audit.fn == nil || + !coro.ProveSSAExactSafeFixedArrayIndex(b.audit.fn, index, bound, use) { + return nil, false + } + for _, block := range b.audit.fn.Blocks { + if len(block.Instrs) == 0 || len(block.Succs) != 2 { + continue + } + branch, ok := block.Instrs[len(block.Instrs)-1].(*ssa.If) + if !ok { + continue + } + comparison, ok := branch.Cond.(*ssa.BinOp) + if !ok || comparison.Op != token.LSS || comparison.X != index || + !coroFrameRetentionIntegerAtMost(comparison.Y, bound) || + !block.Succs[0].Dominates(use.Block()) { + continue + } + limit, ok := coroFrameRetentionExactPositiveInteger(comparison.Y) + if !ok { + continue + } + inductionEvidence, ok := coroFrameRetentionNonNegativeRangeIndex(index, block, block.Succs[0], use, limit) + if !ok { + continue + } + evidence := append([]ssa.Instruction(nil), inductionEvidence...) + evidence = append(evidence, comparison, branch) + return evidence, true + } + return nil, false +} + +func coroFrameRetentionNonNegativeRangeIndex( + index ssa.Value, + header *ssa.BasicBlock, + trueSuccessor *ssa.BasicBlock, + use ssa.Instruction, + constantUpperBound int64, +) ([]ssa.Instruction, bool) { + if index == nil || header == nil || trueSuccessor == nil || use == nil || use.Block() == nil || + !trueSuccessor.Dominates(use.Block()) { + return nil, false + } + basic, ok := types.Unalias(index.Type()).Underlying().(*types.Basic) + if !ok || basic.Info()&types.IsInteger == 0 { + return nil, false + } + if basic.Info()&types.IsUnsigned != 0 { + return nil, true + } + if constantIndex, ok := index.(*ssa.Const); ok { + return nil, constantIndex.Value != nil && constant.Sign(constantIndex.Value) >= 0 + } + if len(header.Succs) != 2 { + return nil, false + } + + var phi *ssa.Phi + var next *ssa.BinOp + initial := int64(0) + indexIsNext := false + if candidate, ok := index.(*ssa.Phi); ok { + phi = candidate + } else if add, ok := index.(*ssa.BinOp); ok && add.Op == token.ADD { + candidate, increment := coroFrameRetentionPhiAndConstant(add.X, add.Y) + if candidate == nil || increment != 1 { + return nil, false + } + phi = candidate + next = add + initial = -1 + indexIsNext = true + } else { + return nil, false + } + if phi.Block() != header || len(header.Preds) < 2 || len(phi.Edges) != len(header.Preds) { + return nil, false + } + if !indexIsNext { + for _, edge := range phi.Edges { + add, ok := edge.(*ssa.BinOp) + if !ok || add.Op != token.ADD { + continue + } + edgePhi, increment := coroFrameRetentionPhiAndConstant(add.X, add.Y) + if edgePhi == phi && increment > 0 { + next = add + break + } + } + } + if next == nil { + return nil, false + } + _, increment := coroFrameRetentionPhiAndConstant(next.X, next.Y) + if increment <= 0 { + return nil, false + } + if constantUpperBound == 0 && increment != 1 { + // len(slice) may be MaxInt; a larger step could overflow after the + // last accepted iteration before the next header comparison. + return nil, false + } + if constantUpperBound > 0 { + maximum, ok := coroFrameRetentionSignedIntegerMax(basic.Kind()) + if !ok || constantUpperBound-1 > maximum || increment > maximum-(constantUpperBound-1) { + return nil, false + } + } + + initialCount, recursiveCount := 0, 0 + var recursivePredecessors []*ssa.BasicBlock + for edgeIndex, edge := range phi.Edges { + predecessor := header.Preds[edgeIndex] + if predecessor == nil { + return nil, false + } + if value, ok := edge.(*ssa.Const); ok && value.Value != nil { + integer, exact := constant.Int64Val(value.Value) + if !exact || integer != initial || header.Dominates(predecessor) { + return nil, false + } + initialCount++ + continue + } + if edge != next || !header.Dominates(predecessor) || !trueSuccessor.Dominates(predecessor) { + return nil, false + } + if indexIsNext { + if next.Block() != header { + return nil, false + } + } else if next.Block() != predecessor { + return nil, false + } + recursiveCount++ + recursivePredecessors = append(recursivePredecessors, predecessor) + } + if initialCount != 1 || recursiveCount == 0 { + return nil, false + } + for _, predecessor := range recursivePredecessors { + if coroFrameRetentionBlockCanReachWithoutCrossing(header.Succs[1], predecessor, header) { + return nil, false + } + } + evidence := []ssa.Instruction{phi, next} + return evidence, true +} + +func coroFrameRetentionBlockCanReachWithoutCrossing(from, target, stop *ssa.BasicBlock) bool { + if from == nil || target == nil { + return false + } + seen := make(map[*ssa.BasicBlock]bool) + queue := []*ssa.BasicBlock{from} + for len(queue) != 0 { + block := queue[0] + queue = queue[1:] + if block == target { + return true + } + if block == nil || block == stop || seen[block] { + continue + } + seen[block] = true + queue = append(queue, block.Succs...) + } + return false +} + +func coroFrameRetentionSignedIntegerMax(kind types.BasicKind) (int64, bool) { + switch kind { + case types.Int8: + return 1<<7 - 1, true + case types.Int16: + return 1<<15 - 1, true + case types.Int32: + return 1<<31 - 1, true + case types.Int64: + return 1<<63 - 1, true + case types.Int: + // Every supported Go target has at least a 32-bit int. This deliberately + // uses the portable lower bound instead of host architecture state. + return 1<<31 - 1, true + default: + return 0, false + } +} + +func coroFrameRetentionPhiAndConstant(left, right ssa.Value) (*ssa.Phi, int64) { + if phi, ok := left.(*ssa.Phi); ok { + if value, ok := right.(*ssa.Const); ok && value.Value != nil { + integer, exact := constant.Int64Val(value.Value) + if exact { + return phi, integer + } + } + } + if phi, ok := right.(*ssa.Phi); ok { + if value, ok := left.(*ssa.Const); ok && value.Value != nil { + integer, exact := constant.Int64Val(value.Value) + if exact { + return phi, integer + } + } + } + return nil, 0 +} + +func newCoroFrameRetentionTrace() coroFrameRetentionTrace { + return coroFrameRetentionTrace{roots: make(map[ssa.Value]struct{}), evidence: make(map[ssa.Instruction]struct{})} +} + +func (t *coroFrameRetentionTrace) addRoot(value ssa.Value) { + if t != nil && value != nil { + t.roots[value] = struct{}{} + } +} + +func (t *coroFrameRetentionTrace) addEvidence(instructions ...ssa.Instruction) { + if t == nil { + return + } + for _, instruction := range instructions { + if instruction != nil { + t.evidence[instruction] = struct{}{} + } + } +} + +func (t *coroFrameRetentionTrace) merge(other coroFrameRetentionTrace) { + for root := range other.roots { + t.addRoot(root) + } + for evidence := range other.evidence { + t.addEvidence(evidence) + } +} + +func (b *coroFrameRetentionRootBuilder) sortedValues(values map[ssa.Value]struct{}) []ssa.Value { + result := b.sortedValueSet(values) + for _, value := range result { + if _, ok := b.valueOrder[value]; !ok { + return nil + } + } + return result +} + +func (b *coroFrameRetentionRootBuilder) sortedValueSet(values map[ssa.Value]struct{}) []ssa.Value { + result := make([]ssa.Value, 0, len(values)) + for value := range values { + result = append(result, value) + } + sort.Slice(result, func(i, j int) bool { return b.valueOrder[result[i]] < b.valueOrder[result[j]] }) + return result +} + +func (b *coroFrameRetentionRootBuilder) sortedInstructions(values map[ssa.Instruction]struct{}) []ssa.Instruction { + result := make([]ssa.Instruction, 0, len(values)) + for value := range values { + result = append(result, value) + } + sort.Slice(result, func(i, j int) bool { return b.instrOrder[result[i]] < b.instrOrder[result[j]] }) + return result +} + +func coroFrameRetentionPointerToUintptr(value *ssa.Convert) bool { + return value != nil && value.X != nil && coroFrameRetentionPointerLike(value.X.Type()) && coroFrameRetentionUintptrLike(value.Type()) +} + +func coroFrameRetentionUintptrLike(typ types.Type) bool { + if typ == nil { + return false + } + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + return ok && basic.Kind() == types.Uintptr +} + +func coroFrameRetentionIntegerLike(typ types.Type) bool { + if typ == nil { + return false + } + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + return ok && basic.Info()&types.IsInteger != 0 +} + +func coroFrameRetentionUnsafePointer(typ types.Type) bool { + if typ == nil { + return false + } + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + return ok && basic.Kind() == types.UnsafePointer +} + +func coroFrameRetentionSliceLike(typ types.Type) bool { + if typ == nil { + return false + } + _, ok := types.Unalias(typ).Underlying().(*types.Slice) + return ok +} + +func coroFrameRetentionNilConst(value ssa.Value) bool { + constant, ok := value.(*ssa.Const) + if !ok || constant.Value != nil { + return false + } + // x/tools/ssa.Const.IsNil deliberately follows its own nillable helper, + // which currently omits unsafe.Pointer even though the Go language permits + // comparing an unsafe.Pointer with nil. Preserve the exact zero-value check + // and recognize pointer-like constants ourselves so this proof matches Go's + // source semantics rather than an x/tools implementation detail. + return constant.IsNil() || coroFrameRetentionPointerLike(constant.Type()) +} + +func coroFrameRetentionExactZeroIndex(value ssa.Value) bool { + return coroFrameRetentionExactInteger(value, 0) +} + +func coroFrameRetentionExactInteger(value ssa.Value, want int64) bool { + constantValue, ok := value.(*ssa.Const) + if !ok || constantValue.Value == nil { + return false + } + integer, exact := constant.Int64Val(constantValue.Value) + return exact && integer == want +} + +func coroFrameRetentionIntegerAtMost(value ssa.Value, bound int64) bool { + integer, ok := coroFrameRetentionExactPositiveInteger(value) + return ok && integer <= bound +} + +func coroFrameRetentionExactPositiveInteger(value ssa.Value) (int64, bool) { + constantValue, ok := value.(*ssa.Const) + if !ok || constantValue.Value == nil { + return 0, false + } + integer, exact := constant.Int64Val(constantValue.Value) + return integer, exact && integer > 0 +} + +func coroFrameRetentionLenZeroComparison(comparison *ssa.BinOp, slice ssa.Value) (*ssa.Call, bool) { + if comparison == nil { + return nil, false + } + if call := coroFrameRetentionExactLenCall(comparison.X, slice); call != nil && coroFrameRetentionExactZeroIndex(comparison.Y) { + return call, true + } + if call := coroFrameRetentionExactLenCall(comparison.Y, slice); call != nil && coroFrameRetentionExactZeroIndex(comparison.X) { + return call, false + } + return nil, false +} + +func coroFrameRetentionExactLenCall(value ssa.Value, operand ssa.Value) *ssa.Call { + call, ok := value.(*ssa.Call) + if !ok || call.Common() == nil || len(call.Common().Args) != 1 || call.Common().Args[0] != operand { + return nil + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "len" { + return nil + } + return call +} + +// zeroOnRight describes "len(s) op 0". Slice lengths are non-negative, so +// these are the only zero comparisons that prove strict positivity without a +// range/value analysis. +func coroFrameRetentionPositiveLengthSuccessor(op token.Token, zeroOnRight bool) (int, bool) { + if zeroOnRight { + switch op { + case token.GTR, token.NEQ: + return 0, true + case token.EQL, token.LEQ: + return 1, true + } + } else { + switch op { + case token.LSS, token.NEQ: + return 0, true + case token.EQL, token.GEQ: + return 1, true + } + } + return 0, false +} + +func coroFrameRetentionRootDigest(a *coroPhysicalPureSSAAudit, proof *coroFrameRetentionProof) string { + if a == nil || a.fn == nil || proof == nil { + return "" + } + builder := newCoroFrameRetentionRootBuilder(a, proof) + valueID := func(value ssa.Value) string { + if value == nil { + return "none" + } + if order, ok := builder.valueOrder[value]; ok { + return "v" + strconv.Itoa(order) + } + return "outside" + } + instructionID := func(instruction ssa.Instruction) string { + if instruction == nil { + return "none" + } + if order, ok := builder.instrOrder[instruction]; ok { + return "i" + strconv.Itoa(order) + } + return "outside" + } + fields := []string{coroFrameRetentionExactRootProfileV2} + for _, rootValue := range proof.exactRetainedRoots() { + root := proof.exactRoots[rootValue] + fields = append(fields, framedEmissionKey( + "root", valueID(root.value), strconv.Itoa(int(root.kind)), structuralEmissionTypeKey(a.typeOf(root.value.Type())), + )) + } + managedAllocationKeys := make([]*ssa.Alloc, 0, len(proof.managedHeapAllocations)) + for allocation := range proof.managedHeapAllocations { + managedAllocationKeys = append(managedAllocationKeys, allocation) + } + sort.Slice(managedAllocationKeys, func(i, j int) bool { + return builder.valueOrder[managedAllocationKeys[i]] < builder.valueOrder[managedAllocationKeys[j]] + }) + for _, allocation := range managedAllocationKeys { + fact := proof.managedHeapAllocations[allocation] + mode := "allocz" + if fact.zeroSized { + mode = "module-zero-sentinel" + } + fields = append(fields, framedEmissionKey( + "managed-heap-allocation", valueID(allocation), structuralEmissionTypeKey(a.typeOf(allocation.Type())), + mode, fact.helper, string(fact.helperTarget), fact.helperEmission.String(), + )) + } + terminalAllocationKeys := make([]*ssa.Alloc, 0, len(proof.terminalResultAllocations)) + for allocation := range proof.terminalResultAllocations { + terminalAllocationKeys = append(terminalAllocationKeys, allocation) + } + sort.Slice(terminalAllocationKeys, func(i, j int) bool { + return builder.valueOrder[terminalAllocationKeys[i]] < builder.valueOrder[terminalAllocationKeys[j]] + }) + for _, allocation := range terminalAllocationKeys { + fields = append(fields, framedEmissionKey( + "cleanup-terminal-result-allocation", valueID(allocation), structuralEmissionTypeKey(a.typeOf(allocation.Type())), + )) + } + addressKeys := make([]coroFrameRetentionAddressUse, 0, len(proof.stableAddresses)) + for key := range proof.stableAddresses { + addressKeys = append(addressKeys, key) + } + sort.Slice(addressKeys, func(i, j int) bool { + left, right := builder.instrOrder[addressKeys[i].use], builder.instrOrder[addressKeys[j].use] + if left != right { + return left < right + } + return builder.valueOrder[addressKeys[i].value] < builder.valueOrder[addressKeys[j].value] + }) + for _, key := range addressKeys { + fact := proof.stableAddresses[key] + nilMode := "guard" + if fact.nonNil { + nilMode = "non-nil" + } + entry := []string{"address", valueID(key.value), instructionID(key.use), structuralEmissionTypeKey(a.typeOf(key.value.Type())), nilMode} + for _, evidence := range fact.evidence { + entry = append(entry, "evidence="+instructionID(evidence)) + } + fields = append(fields, framedEmissionKey(entry...)) + } + uintptrKeys := make([]ssa.Value, 0, len(proof.uintptrValues)) + for value := range proof.uintptrValues { + uintptrKeys = append(uintptrKeys, value) + } + sort.Slice(uintptrKeys, func(i, j int) bool { return builder.valueOrder[uintptrKeys[i]] < builder.valueOrder[uintptrKeys[j]] }) + for _, value := range uintptrKeys { + entry := []string{"uintptr", valueID(value)} + for _, root := range proof.uintptrValues[value].roots { + entry = append(entry, "root="+valueID(root)) + } + fields = append(fields, framedEmissionKey(entry...)) + } + callKeys := make([]*ssa.Call, 0, len(proof.callKeepalives)) + for call := range proof.callKeepalives { + callKeys = append(callKeys, call) + } + sort.Slice(callKeys, func(i, j int) bool { return builder.instrOrder[callKeys[i]] < builder.instrOrder[callKeys[j]] }) + for _, call := range callKeys { + fact := proof.callKeepalives[call] + entry := []string{"call", instructionID(call), strconv.Itoa(int(fact.kind))} + for _, root := range fact.roots { + entry = append(entry, "root="+valueID(root)) + } + for _, source := range fact.sources { + entry = append(entry, "source="+valueID(source)) + } + fields = append(fields, framedEmissionKey(entry...)) + } + allocationKeys := make([]*ssa.Alloc, 0, len(proof.allocations)) + for allocation := range proof.allocations { + allocationKeys = append(allocationKeys, allocation) + } + sort.Slice(allocationKeys, func(i, j int) bool { + return builder.valueOrder[allocationKeys[i]] < builder.valueOrder[allocationKeys[j]] + }) + for _, allocation := range allocationKeys { + fields = append(fields, framedEmissionKey("park-allocation", valueID(allocation))) + } + sum := sha256.Sum256([]byte(framedEmissionKey(fields...))) + return hex.EncodeToString(sum[:]) +} + +func coroFrameRetentionPointerLike(typ types.Type) bool { + underlying := types.Unalias(typ).Underlying() + if _, ok := underlying.(*types.Pointer); ok { + return true + } + basic, ok := underlying.(*types.Basic) + return ok && basic.Kind() == types.UnsafePointer +} + +func coroFrameRetentionDirectAllocRoot(value ssa.Value, visiting map[ssa.Value]bool) *ssa.Alloc { + if value == nil || visiting[value] { + return nil + } + visiting[value] = true + defer delete(visiting, value) + switch value := value.(type) { + case *ssa.Alloc: + return value + case *ssa.ChangeType: + if value.X != nil && coroFrameRetentionPointerLike(value.Type()) && coroFrameRetentionPointerLike(value.X.Type()) { + return coroFrameRetentionDirectAllocRoot(value.X, visiting) + } + case *ssa.Convert: + if value.X != nil && coroFrameRetentionPointerLike(value.Type()) && coroFrameRetentionPointerLike(value.X.Type()) { + return coroFrameRetentionDirectAllocRoot(value.X, visiting) + } + } + return nil +} + +func coroFrameRetentionInstructionIndex(instruction ssa.Instruction) int { + if instruction == nil || instruction.Block() == nil { + return -1 + } + for index, candidate := range instruction.Block().Instrs { + if candidate == instruction { + return index + } + } + return -1 +} + +func coroFrameRetentionAddressUsesMatch(alloc *ssa.Alloc, allowedCalls map[*ssa.Call]int, allowedLoads map[*ssa.UnOp]struct{}) bool { + aliases := make(map[ssa.Value]bool) + queue := []ssa.Value{alloc} + aliases[alloc] = true + for head := 0; head < len(queue); head++ { + value := queue[head] + refs := value.Referrers() + if refs == nil { + return false + } + for _, reference := range *refs { + var alias ssa.Value + switch instruction := reference.(type) { + case *ssa.ChangeType: + if instruction.X == value && coroFrameRetentionPointerLike(instruction.Type()) && coroFrameRetentionPointerLike(value.Type()) { + alias = instruction + } + case *ssa.Convert: + if instruction.X == value && coroFrameRetentionPointerLike(instruction.Type()) && coroFrameRetentionPointerLike(value.Type()) { + alias = instruction + } + } + if alias != nil && !aliases[alias] { + aliases[alias] = true + queue = append(queue, alias) + } + } + } + + seenCalls := make(map[*ssa.Call]bool) + seenLoads := make(map[*ssa.UnOp]bool) + semanticUses := make(map[ssa.Value]int, len(aliases)) + for value := range aliases { + refs := value.Referrers() + if refs == nil { + return false + } + for _, reference := range *refs { + switch instruction := reference.(type) { + case *ssa.DebugRef: + case *ssa.ChangeType: + if instruction.X != value || !aliases[instruction] { + return false + } + semanticUses[value]++ + case *ssa.Convert: + if instruction.X != value || !aliases[instruction] { + return false + } + semanticUses[value]++ + case *ssa.UnOp: + if instruction.Op != token.MUL || instruction.X != value { + return false + } + if _, ok := allowedLoads[instruction]; !ok { + return false + } + seenLoads[instruction] = true + semanticUses[value]++ + case *ssa.Store: + return false + case *ssa.Call: + if seenCalls[instruction] { + continue + } + seenCalls[instruction] = true + want, ok := allowedCalls[instruction] + if !ok || instruction.Common() == nil { + return false + } + matches := 0 + for index, argument := range instruction.Common().Args { + if aliases[argument] { + if index != want { + return false + } + matches++ + } + } + if matches != 1 { + return false + } + semanticUses[value]++ + default: + return false + } + } + } + for alias := range aliases { + if semanticUses[alias] == 0 { + return false + } + } + return len(seenCalls) == len(allowedCalls) && len(seenLoads) == len(allowedLoads) +} diff --git a/cl/coro_frame_retention_test.go b/cl/coro_frame_retention_test.go new file mode 100644 index 0000000000..75ac4bc706 --- /dev/null +++ b/cl/coro_frame_retention_test.go @@ -0,0 +1,280 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroFrameRetentionNilConstRecognizesUnsafePointer(t *testing.T) { + value := ssa.NewConst(nil, types.Typ[types.UnsafePointer]) + if !coroFrameRetentionNilConst(value) { + t.Fatalf("unsafe.Pointer zero constant %v was not recognized as nil", value) + } +} + +const coroFrameRetentionFixture = `package foo + +import "unsafe" + +type ParkState struct { words [16]uintptr } + +//llgo:coro contract foreign.v1 scope=declaration progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +//go:linkname prepare C.__llgo_coro_fixture_prepare +func prepare(unsafe.Pointer, unsafe.Pointer) + +//go:linkname park llgo.coroPark +func park(*ParkState, uint32) + +func Root(addr *uint32) uint32 { + if addr == nil { + return 0 + } + var state ParkState + prepare(unsafe.Pointer(&state), unsafe.Pointer(addr)) + park(&state, 0) + return *addr +} +` + +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) { + source := strings.ReplaceAll(coroFrameRetentionFixture, "__llgo_coro_fixture_prepare", symbol) + prog, ssaPkg, files, universe, proof := prepareCoroFrameRetentionProof( + t, source, CoroFrameRetentionParkABIV2, + ) + defer prog.Dispose() + allocations := coroFrameRetentionHeapAllocs(ssaPkg.Func("Root")) + if len(allocations) != 1 || len(proof.allocations) != 1 { + t.Fatalf("generic park proof selected %d/%d heap allocations, want 1/1", len(proof.allocations), len(allocations)) + } + if _, retained := proof.allocations[allocations[0]]; !retained { + t.Fatal("generic park state was not selected for coroutine-frame storage") + } + + root := ssaPkg.Func("Root") + var prepare *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() == "prepare" { + prepare = call + } + } + } + if prepare == nil { + t.Fatal("fixture has no prepare call") + } + rootedAddr := false + for _, value := range proof.exactCallKeepaliveRoots(prepare) { + rootedAddr = rootedAddr || value == root.Params[0] + } + if !rootedAddr { + t.Fatal("borrow prepare did not retain its typed source key") + } + + plan := analyzeCoroFrameRetentionFixture(t, ssaPkg, universe, root, 1) + compilation := &Compilation{ + CoroPlan: plan, EmissionUniverse: universe, + CoroFrameRetentionABI: CoroFrameRetentionParkABIV2, + } + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + body := requireCoroPhysicalFunction(t, module, "foo.Root").String() + if strings.Contains(body, "AllocZ") || !strings.Contains(body, "alloca %foo.ParkState") || + !strings.Contains(body, "call void @"+symbol) || + strings.Count(body, "call void @"+coroKeyedParkHookV2) != 1 || + strings.Count(body, "call i32 @"+coroKeyedResumeHookV2) != 1 { + t.Fatalf("generic park state did not lower through the single frame-owned path:\n%s", body) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify generic park state before CoroSplit: %v\n%s", err, module.String()) + } + runCoroABITestPipeline(t, prog, module) + if resume := module.NamedFunction("foo.Root$coro.resume"); resume.IsNil() || + strings.Contains(resume.String(), "AllocZ") || !strings.Contains(resume.String(), symbol) { + t.Fatalf("CoroSplit lost frame-owned generic park state:\n%s", module.String()) + } + }) + } +} + +func TestCoroGenericParkStateRetentionRequiresExactLifetimeProof(t *testing.T) { + tests := []struct { + name string + abi string + source string + }{ + {name: "profile absent", source: coroFrameRetentionFixture}, + { + name: "legacy progress-only annotation", + abi: CoroFrameRetentionParkABIV2, + source: strings.Replace(coroFrameRetentionFixture, + "//llgo:coro contract foreign.v1 scope=declaration progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return", + "//llgo:coro noblock", 1), + }, + { + name: "retained memory", + abi: CoroFrameRetentionParkABIV2, + source: strings.Replace(coroFrameRetentionFixture, + "memory=borrow-until-return", "memory=retained", 1), + }, + { + name: "blocking prepare", + abi: CoroFrameRetentionParkABIV2, + source: strings.Replace(coroFrameRetentionFixture, + "progress=executor-safe", "progress=may-block", 1), + }, + { + name: "pointer-bearing opaque state", + abi: CoroFrameRetentionParkABIV2, + source: strings.Replace(coroFrameRetentionFixture, + "type ParkState struct { words [16]uintptr }", "type ParkState struct { pointer unsafe.Pointer }", 1), + }, + { + name: "duplicate prepare owner", + abi: CoroFrameRetentionParkABIV2, + source: strings.Replace(coroFrameRetentionFixture, + "prepare(unsafe.Pointer(&state), unsafe.Pointer(addr))\n\tpark", + "prepare(unsafe.Pointer(&state), unsafe.Pointer(addr))\n\tprepare(unsafe.Pointer(&state), unsafe.Pointer(addr))\n\tpark", 1), + }, + { + name: "missing prepare owner", + abi: CoroFrameRetentionParkABIV2, + source: strings.Replace(coroFrameRetentionFixture, + "\tprepare(unsafe.Pointer(&state), unsafe.Pointer(addr))\n", "", 1), + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + prog, ssaPkg, _, _, proof := prepareCoroFrameRetentionProof(t, test.source, test.abi) + defer prog.Dispose() + if len(proof.allocations) != 0 { + t.Fatalf("uncertified generic park selected %d frame allocations, want zero", len(proof.allocations)) + } + if len(coroFrameRetentionHeapAllocs(ssaPkg.Func("Root"))) == 0 { + t.Fatal("fixture unexpectedly has no escaping park state") + } + }) + } +} + +func prepareCoroFrameRetentionProof(t *testing.T, source, abi string) ( + llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coroFrameRetentionProof, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + audit, err := newCoroPhysicalPureSSAAudit(universe, nil, ssaPkg.Func("Root"), abi) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, ssaPkg, files, universe, audit.currentFrameRetentionProof() +} + +func analyzeCoroFrameRetentionFixture(t *testing.T, ssaPkg *ssa.Package, universe *EmissionUniverse, root *ssa.Function, maxPlain int) *coro.SSAPlan { + t.Helper() + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, FunctionIDs: functionIDs, MaxPlainInstructions: maxPlain, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.MayPark}, nil + } + background, classified, backgroundErr := universe.FunctionBackground(fn) + if backgroundErr != nil || !classified || background != llssa.InC { + return coro.SSAFunctionPolicy{}, backgroundErr + } + certificate, certified, certificateErr := universe.CoroCallableContractCertificate(fn) + if certificateErr != nil { + return coro.SSAFunctionPolicy{}, certificateErr + } + if certified { + external := coro.ExternalUnknownForeign + exec := coro.BlockForeign | coro.IRQUnsafe | coro.CallableContractExecConstraints(certificate.Contract) + if certificate.Contract.Progress == coro.ProgressExecutorSafe { + external = coro.ExternalKnown + exec &^= coro.BlockForeign + } + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: external, OverrideExternal: true, + Exec: exec, CallableContractCertificate: certificate, + }, nil + } + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: coro.ExternalUnknownForeign, OverrideExternal: true, + Exec: coro.BlockForeign | coro.IRQUnsafe, + }, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + if err != nil { + t.Fatal(err) + } + return plan +} + +func coroFrameRetentionHeapAllocs(fn *ssa.Function) []*ssa.Alloc { + var result []*ssa.Alloc + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + if alloc, ok := instruction.(*ssa.Alloc); ok && alloc.Heap { + result = append(result, alloc) + } + } + } + return result +} diff --git a/cl/coro_frame_roots_test.go b/cl/coro_frame_roots_test.go new file mode 100644 index 0000000000..47011ed1e1 --- /dev/null +++ b/cl/coro_frame_roots_test.go @@ -0,0 +1,917 @@ +//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 ( + "bytes" + "go/token" + "go/types" + "sort" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroFrameExactRootsFixture = `package foo + +import "unsafe" + +type Box struct { value byte } + +func Child(receiver *Box, bytes []byte, pointer *byte) {} + +//go:linkname raw llgo.syscall +func raw(fn, a0 uintptr) (uintptr, uintptr, uintptr) + +//go:linkname funcPCABI0 llgo.funcPCABI0 +func funcPCABI0(fn any) uintptr + +//llgo:coro workeraddr 1 +func libc_frame_root_v1_trampoline() + +func (receiver *Box) Method(pointer *byte, bytes []byte) uintptr { + if receiver != nil && pointer != nil && len(bytes) > 0 { + receiver.value = *pointer + Child(receiver, bytes, pointer) + word := uintptr(unsafe.Pointer(&bytes[0])) + result, _, _ := raw(funcPCABI0(libc_frame_root_v1_trampoline), word) + return result + } + return 0 +} +` + +func TestCoroFrameExactRootsAndUintptrKeepaliveAreFrozen(t *testing.T) { + digest := "" + for iteration := 0; iteration < 2; iteration++ { + prog, _, universe, method, audit, proof := prepareCoroFrameRootAudit( + t, coroFrameExactRootsFixture, "Method", EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if got := proof.exactRootCapabilityProfile(); got != coroFrameRetentionExactRootProfileV2 { + prog.Dispose() + t.Fatalf("exact-root capability profile = %q", got) + } + if got := proof.exactRootCapabilityDigest(); len(got) != 64 { + prog.Dispose() + t.Fatalf("exact-root digest = %q, want one SHA-256 identity", got) + } else if iteration == 0 { + digest = got + } else if got != digest { + prog.Dispose() + t.Fatalf("same immutable SSA rebuilt digest %q, want %q", got, digest) + } + + roots := make(map[string]coroFrameRetentionRootKind) + for _, value := range proof.exactRetainedRoots() { + roots[value.Name()] = proof.exactRoots[value].kind + } + for name, kind := range map[string]coroFrameRetentionRootKind{ + "receiver": coroFrameRetentionRootReceiver, + "pointer": coroFrameRetentionRootPointerParameter, + "bytes": coroFrameRetentionRootSliceParameter, + } { + if roots[name] != kind { + prog.Dispose() + t.Fatalf("exact root %q kind = %d, want %d; roots=%v", name, roots[name], kind, roots) + } + } + + var childCall, workerCall *ssa.Call + var sliceAddress *ssa.IndexAddr + var pointerWord *ssa.Convert + for _, block := range method.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + prog.Dispose() + t.Fatalf("certified instruction %T %q rejected: %s", instruction, instruction, reason) + } + switch instruction := instruction.(type) { + case *ssa.Call: + callee := instruction.Common().StaticCallee() + if callee != nil && callee.Name() == "Child" { + childCall = instruction + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(instruction) + if err == nil && intrinsic && semantics == CoroIntrinsicCallInlineSuspend { + workerCall = instruction + } + case *ssa.IndexAddr: + if _, slice := instruction.X.Type().Underlying().(*types.Slice); slice { + sliceAddress = instruction + } + case *ssa.Convert: + if coroFrameRetentionPointerToUintptr(instruction) { + pointerWord = instruction + } + } + } + } + if childCall == nil || workerCall == nil || sliceAddress == nil || pointerWord == nil { + prog.Dispose() + t.Fatalf("fixture facts child=%v worker=%v slice=%v uintptr=%v", childCall, workerCall, sliceAddress, pointerWord) + } + if !proof.provesDominatedStableAddress(sliceAddress, sliceAddress) || !proof.provesTraceableUintptr(pointerWord) { + prog.Dispose() + t.Fatal("dominated &bytes[0] or pointer->uintptr provenance was not frozen") + } + if got := rootNames(proof.exactCallKeepaliveRoots(childCall)); strings.Join(got, ",") != "bytes,pointer,receiver" { + prog.Dispose() + t.Fatalf("child keepalive roots = %v", got) + } + if got := rootNames(proof.exactCallKeepaliveRoots(workerCall)); strings.Join(got, ",") != "bytes" { + prog.Dispose() + t.Fatalf("worker keepalive roots = %v", got) + } + prog.Dispose() + } +} + +func TestCoroFrameExactRootsRemainFailClosed(t *testing.T) { + tests := []struct { + name string + source string + options EmissionUniverseOptions + want string + }{ + { + name: "unproved nil pointer", + source: `package foo +type Box struct { value byte } +func Root(box *Box) byte { return box.value } +`, + want: "no exact non-nil frame-retention proof", + }, + { + name: "unproved empty slice", + source: `package foo +import "unsafe" +func Child(uintptr) {} +func Root(bytes []byte) { Child(uintptr(unsafe.Pointer(&bytes[0]))) } +`, + want: "index base is not a fixed-array pointer", + }, + { + name: "non-positive dominance", + source: `package foo +import "unsafe" +func Child(uintptr) {} +func Root(bytes []byte) { if len(bytes) >= 0 { Child(uintptr(unsafe.Pointer(&bytes[0]))) } } +`, + want: "index base is not a fixed-array pointer", + }, + { + name: "index one only proves nonempty", + source: `package foo +import "unsafe" +func Child(uintptr) {} +func Root(bytes []byte) { if len(bytes) > 0 { Child(uintptr(unsafe.Pointer(&bytes[1]))) } } +`, + want: "index base is not a fixed-array pointer", + }, + { + name: "returned pointer word escapes bounded lifetime", + source: `package foo +import "unsafe" +func Root(pointer *byte) uintptr { return uintptr(unsafe.Pointer(pointer)) } +`, + want: "not bound to an exact managed-child/worker uintptrkeepalive source", + }, + { + name: "foreign pointer word escape", + source: `package foo +import "unsafe" +//go:linkname foreign C.foreign +func foreign(uintptr) +func Root(pointer *byte) { foreign(uintptr(unsafe.Pointer(pointer))) } +`, + want: "not bound to an exact managed-child/worker uintptrkeepalive source", + }, + { + name: "untraceable uintptr to pointer", + source: `package foo +import "unsafe" +func Root(word uintptr) unsafe.Pointer { return unsafe.Pointer(word) } +`, + want: "has no traceable exact pointer provenance", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + prog, _, _, root, audit, _ := prepareCoroFrameRootAudit(t, test.source, "Root", test.options) + defer prog.Dispose() + var got string + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + got = reason + break + } + } + if got != "" { + break + } + } + if !strings.Contains(got, test.want) { + t.Fatalf("first pure-SSA rejection = %q, want %q", got, test.want) + } + }) + } +} + +func TestCoroFrameExactRootsAcceptCanonicalSliceRangeIndex(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +func Root(bytes []byte) byte { + if len(bytes) == 0 { return 0 } + sum := bytes[len(bytes)-1] + for _, value := range bytes { + if value == 0 { continue } + sum ^= value + } + return sum +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var address *ssa.IndexAddr + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + t.Fatalf("canonical range instruction %T %q rejected: %s", instruction, instruction, reason) + } + if index, ok := instruction.(*ssa.IndexAddr); ok { + address = index + } + } + } + if address == nil || !proof.provesDominatedStableAddress(address, address) { + t.Fatal("canonical range IndexAddr has no exact dominating bounds/root proof") + } +} + +func TestCoroFrameExactRootsAcceptCanonicalFixedArrayRangeIndex(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +func Root() byte { + var values [16]byte + for index := 0; index < len(values); index++ { + values[index] = byte(index) + } + return values[15] +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var dynamicAddress *ssa.IndexAddr + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + t.Fatalf("canonical fixed-array range instruction %T %q rejected: %s", instruction, instruction, reason) + } + if index, ok := instruction.(*ssa.IndexAddr); ok { + if _, constant := index.Index.(*ssa.Const); !constant { + dynamicAddress = index + } + } + } + } + if dynamicAddress == nil || !proof.provesDominatedStableAddress(dynamicAddress, dynamicAddress) { + t.Fatal("canonical fixed-array range IndexAddr has no exact dominating bounds/root proof") + } +} + +func TestCoroFrameExactRootsAcceptBoundedFixedArrayStepIndex(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +func Root() byte { + var values [64]byte + for index := 0; index < len(values); index += 8 { + values[index] = byte(index) + } + return values[56] +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var dynamicAddress *ssa.IndexAddr + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + t.Fatalf("bounded step instruction %T %q rejected: %s", instruction, instruction, reason) + } + if index, ok := instruction.(*ssa.IndexAddr); ok { + if _, constant := index.Index.(*ssa.Const); !constant { + dynamicAddress = index + } + } + } + } + if dynamicAddress == nil || !proof.provesDominatedStableAddress(dynamicAddress, dynamicAddress) { + t.Fatal("bounded fixed-array step IndexAddr has no exact CFG/bounds proof") + } +} + +func TestCoroFrameExactRootsAcceptNestedSliceAndArrayIndexes(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +type bucket struct { values [16]uint16 } +func Root(buckets []bucket) []bucket { + for b := 0; b < len(buckets); b++ { + for s := 0; s < 16; s++ { + buckets[b].values[s] = uint16(b + s) + } + } + return buckets +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + proved := 0 + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + var dump bytes.Buffer + ssa.WriteFunction(&dump, root) + t.Fatalf("nested index instruction %T %q rejected in block %d: %s\n%s", instruction, instruction, block.Index, reason, dump.String()) + } + if index, ok := instruction.(*ssa.IndexAddr); ok && proof.provesDominatedStableAddress(index, index) { + proved++ + } + } + } + if proved < 2 { + t.Fatalf("nested slice/array fixture proved %d IndexAddr values, want both levels", proved) + } +} + +func TestCoroFrameExactRootsRejectSignedOverflowReentryIndex(t *testing.T) { + prog, _, _, root, audit, _ := prepareCoroFrameRootAudit(t, `package foo +func Root() byte { + var values [16]byte + var index int8 + for { + if index < 16 { + if index < 0 { + return values[index] + } + } + index++ + } +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + index, ok := instruction.(*ssa.IndexAddr) + if !ok { + continue + } + handled, reason := audit.validate(index) + if !handled || reason == "" { + t.Fatalf("signed-overflow reentry IndexAddr unexpectedly accepted: handled=%v reason=%q", handled, reason) + } + return + } + } + t.Fatal("signed-overflow fixture has no IndexAddr") +} + +func TestCoroFrameExactRootsAcceptGuardedUnsafeAddDereference(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +import "unsafe" +func Root(base *byte, offset uintptr) byte { + if base == nil { + return 0 + } + address := unsafe.Add(unsafe.Pointer(base), offset) + return *(*byte)(address) +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var dereference *ssa.UnOp + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + t.Fatalf("guarded unsafe.Add instruction %T %q rejected: %s", instruction, instruction, reason) + } + if load, ok := instruction.(*ssa.UnOp); ok && load.Op == token.MUL { + dereference = load + } + } + } + if dereference == nil || !proof.provesDominatedStableAddress(dereference.X, dereference) { + t.Fatal("guarded unsafe.Add dereference has no exact address-retention proof") + } +} + +func TestCoroFrameExactRootsAcceptGuardedMergedPointer(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +type Box struct { value byte } +func Root(first, second *Box, choose bool) byte { + selected := first + if choose { selected = second } + if selected == nil { return 0 } + return selected.value +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var field *ssa.FieldAddr + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + var dump bytes.Buffer + ssa.WriteFunction(&dump, root) + t.Fatalf("guarded merged-pointer instruction %T %q rejected: %s\n%s", instruction, instruction, reason, dump.String()) + } + if candidate, ok := instruction.(*ssa.FieldAddr); ok { + field = candidate + } + } + } + if field == nil || !proof.provesDominatedStableAddress(field, field) { + t.Fatal("guarded merged pointer field has no exact non-nil retention proof") + } +} + +func TestCoroFrameExactRootsAcceptGuardedLoopCarriedPointer(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +type Box struct { value byte } +func Root(boxes []*Box, match byte) byte { + var selected *Box + for index := 0; index < len(boxes); index++ { + candidate := boxes[index] + if candidate != nil && candidate.value == match { + if selected != nil { return 0 } + selected = candidate + } + } + if selected == nil { return 0 } + return selected.value +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var guardedField *ssa.FieldAddr + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + var dump bytes.Buffer + ssa.WriteFunction(&dump, root) + t.Fatalf("guarded loop-carried instruction %T %q rejected: %s\n%s", instruction, instruction, reason, dump.String()) + } + field, ok := instruction.(*ssa.FieldAddr) + if ok { + guardedField = field + } + } + } + if guardedField == nil || !proof.provesDominatedStableAddress(guardedField, guardedField) { + t.Fatal("guarded loop-carried pointer field has no exact non-nil retention proof") + } +} + +func TestCoroFrameExactRootsAcceptUnsafePointerPhiWithNilSeed(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +import "unsafe" +func Root(pointer unsafe.Pointer, choose bool) { + var address unsafe.Pointer + if choose { address = pointer } + *(*unsafe.Pointer)(address) = pointer +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + audit.allowImplicitNilFault = true + var store *ssa.Store + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + candidate, ok := instruction.(*ssa.Store) + if !ok { + continue + } + store = candidate + if reason := audit.validateStore(candidate); reason != "" { + var dump bytes.Buffer + ssa.WriteFunction(&dump, root) + t.Fatalf("unsafe.Pointer phi store rejected: %s\n%s", reason, dump.String()) + } + } + } + if store == nil || !proof.provesGuardableStableAddress(store.Addr, store) { + t.Fatal("unsafe.Pointer phi with a nil seed has no exact guardable address proof") + } +} + +func TestCoroFrameExactUintptrRoundtripWithInterveningChildIsFrozen(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +import "unsafe" +type Header struct { length uintptr } +type AddressWord uintptr +func Align(size int) int { return (size + 7) &^ 7 } +func Root(header *Header, offset uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(AddressWord(uintptr(unsafe.Pointer(header)))) + uintptr(Align(16)) + offset) +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + pointerWords := 0 + reconstruction := false + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + var dump bytes.Buffer + ssa.WriteFunction(&dump, root) + t.Fatalf("roundtrip instruction %T %q rejected: %s\n%s", instruction, instruction, reason, dump.String()) + } + conversion, ok := instruction.(*ssa.Convert) + if !ok { + continue + } + if coroFrameRetentionPointerToUintptr(conversion) { + pointerWords++ + if !proof.provesTraceableUintptr(conversion) { + t.Fatalf("pointer word %q has no exact roundtrip provenance", conversion) + } + } + if coroFrameRetentionUintptrLike(conversion.X.Type()) && coroFrameRetentionPointerLike(conversion.Type()) { + reconstruction = true + if !proof.provesTraceableUintptr(conversion.X) { + t.Fatalf("pointer reconstruction %q has no exact source provenance", conversion) + } + } + } + } + if pointerWords != 1 || !reconstruction { + t.Fatalf("roundtrip facts pointer words=%d reconstruction=%t, want 1/true", pointerWords, reconstruction) + } +} + +func TestCoroFramePointerDistanceIsAnExactScalarTerminal(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +import "unsafe" +func Root(start, end unsafe.Pointer) uintptr { + distance := uintptr(end) - uintptr(start) + if distance > 1<<20 { return 0 } + return distance +} +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + pointerWords := 0 + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if conversion, ok := instruction.(*ssa.Convert); ok && coroFrameRetentionPointerToUintptr(conversion) { + pointerWords++ + if proof.provesTraceableUintptr(conversion) { + t.Fatalf("scalar-only pointer word %q unexpectedly received reconstructable pointer provenance", conversion) + } + if !coroPointerUintptrScalarTerminal(conversion) { + t.Fatalf("pointer-distance word %q lacks the exact structural scalar terminal", conversion) + } + continue + } + if handled, reason := audit.validate(instruction); handled && reason != "" { + var dump bytes.Buffer + ssa.WriteFunction(&dump, root) + t.Fatalf("pointer-distance instruction %T %q rejected: %s\n%s", instruction, instruction, reason, dump.String()) + } + } + } + if pointerWords != 2 { + t.Fatalf("pointer-distance conversions = %d, want 2", pointerWords) + } +} + +func TestCoroFrameExactUintptrRoundtripChildAwaitNativeAndWasm(t *testing.T) { + llssa.Initialize(llssa.InitAll) + const source = `package foo +import "unsafe" +type Header struct { length uintptr } +func Align(size int) int { return (size + 7) &^ 7 } +func Root(header *Header, offset uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(unsafe.Pointer(header)) + uintptr(Align(16)) + offset) +} +` + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, source) + var prog llssa.Program + if test.target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, test.target) + } + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + root, align := ssaPkg.Func("Root"), ssaPkg.Func("Align") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: 1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == align { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.Primary != coro.PrimaryCoroutine || + !rootPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Root plan = %+v, present=%t; want structured child-await coroutine", rootPlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify uintptr roundtrip before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Root").String() + for _, required := range []string{"ptrtoint", "inttoptr", "foo.Align$coro", "call void @" + coroAwaitPrepareHookV1} { + if !strings.Contains(body, required) { + t.Fatalf("uintptr roundtrip child-await coroutine lacks %q:\n%s", required, body) + } + } + runCoroABITestPipeline(t, prog, module) + if resume := module.NamedFunction("foo.Root$coro.resume"); resume.IsNil() { + t.Fatalf("CoroSplit did not materialize Root resume entry:\n%s", module.String()) + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit uintptr roundtrip object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 { + t.Fatal("uintptr roundtrip emitted an empty object") + } + }) + } +} + +func TestCoroFrameExactUintptrRoundtripRemainsFailClosed(t *testing.T) { + tests := []struct { + name string + source string + }{ + { + name: "word return escape", + source: `package foo +import "unsafe" +func Root(pointer *byte, offset uintptr) uintptr { return uintptr(unsafe.Pointer(pointer)) + offset } +`, + }, + { + name: "converted integer return escape", + source: `package foo +import "unsafe" +func Root(pointer *byte) int64 { return int64(uintptr(unsafe.Pointer(pointer))) } +`, + }, + { + name: "word store escape", + source: `package foo +import "unsafe" +var escaped uintptr +func Root(pointer *byte) { escaped = uintptr(unsafe.Pointer(pointer)) } +`, + }, + { + name: "converted integer store escape", + source: `package foo +import "unsafe" +var escaped int64 +func Root(pointer *byte) { escaped = int64(uintptr(unsafe.Pointer(pointer))) } +`, + }, + { + name: "foreign word escape", + source: `package foo +import "unsafe" +//go:linkname foreign C.foreign +func foreign(uintptr) +func Root(pointer *byte) { foreign(uintptr(unsafe.Pointer(pointer))) } +`, + }, + { + name: "converted integer foreign escape", + source: `package foo +import "unsafe" +//go:linkname foreign C.foreign +func foreign(int64) +func Root(pointer *byte) { foreign(int64(uintptr(unsafe.Pointer(pointer)))) } +`, + }, + { + name: "converted integer arithmetic escape", + source: `package foo +import "unsafe" +func Child(int64) {} +func Root(pointer *byte) { Child(int64(uintptr(unsafe.Pointer(pointer))) + 1) } +`, + }, + { + name: "multiplication loses address provenance", + source: `package foo +import "unsafe" +func Root(pointer *byte, scale uintptr) unsafe.Pointer { + return unsafe.Pointer(uintptr(unsafe.Pointer(pointer)) * scale) +} +`, + }, + { + name: "two pointer words are ambiguous", + source: `package foo +import "unsafe" +func Root(left, right *byte) unsafe.Pointer { + return unsafe.Pointer(uintptr(unsafe.Pointer(left)) + uintptr(unsafe.Pointer(right))) +} +`, + }, + { + name: "partial control-flow reconstruction", + source: `package foo +import "unsafe" +func Root(pointer *byte, offset uintptr, reconstruct bool) unsafe.Pointer { + word := uintptr(unsafe.Pointer(pointer)) + if reconstruct { return unsafe.Pointer(word + offset) } + return nil +} +`, + }, + { + name: "phi address ambiguity", + source: `package foo +import "unsafe" +func Root(pointer *byte, offset uintptr, adjust bool) unsafe.Pointer { + word := uintptr(unsafe.Pointer(pointer)) + if adjust { word += offset } + return unsafe.Pointer(word) +} +`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, test.source, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + pointerWords := 0 + rejection := "" + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if conversion, ok := instruction.(*ssa.Convert); ok && coroFrameRetentionPointerToUintptr(conversion) { + pointerWords++ + if proof.provesTraceableUintptr(conversion) { + t.Fatalf("unsafe address word %q unexpectedly received exact provenance", conversion) + } + } + if handled, reason := audit.validate(instruction); handled && reason != "" && rejection == "" { + rejection = reason + } + } + } + if pointerWords == 0 { + t.Fatal("negative fixture has no pointer-to-uintptr conversion") + } + if rejection == "" || (!strings.Contains(rejection, "not bound to an exact managed-child/worker") && + !strings.Contains(rejection, "has no traceable exact pointer provenance")) { + var dump bytes.Buffer + ssa.WriteFunction(&dump, root) + t.Fatalf("first rejection = %q, want exact uintptr provenance failure\n%s", rejection, dump.String()) + } + }) + } +} + +func TestCoroPointerUintptrAlignmentObservationIsScalarTerminal(t *testing.T) { + for _, test := range []struct { + name string + expression string + wantSafe bool + }{ + {name: "comparison", expression: "return uintptr(unsafe.Pointer(pointer))%8 == 0", wantSafe: true}, + {name: "returned remainder", expression: "return uintptr(unsafe.Pointer(pointer)) % 8"}, + } { + t.Run(test.name, func(t *testing.T) { + result := "bool" + if !test.wantSafe { + result = "uintptr" + } + source := "package foo\nimport \"unsafe\"\nfunc Root(pointer *byte) " + result + " { " + test.expression + " }\n" + prog, _, _, root, audit, _ := prepareCoroFrameRootAudit(t, source, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + found := false + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + conversion, ok := instruction.(*ssa.Convert) + if !ok || !coroFrameRetentionPointerToUintptr(conversion) { + continue + } + found = true + reason := audit.validateConvert(conversion) + if test.wantSafe && !coroPointerUintptrScalarTerminal(conversion) { + t.Fatalf("alignment comparison lacks the structural scalar-terminal proof: %s", reason) + } + if !test.wantSafe && !strings.Contains(reason, "not bound to an exact managed-child/worker") { + t.Fatalf("returned remainder rejection = %q", reason) + } + } + } + if !found { + t.Fatal("fixture has no pointer-to-uintptr conversion") + } + }) + } +} + +func TestCoroFrameExactRootsRejectPreciseShadowProfile(t *testing.T) { + old := emitShadowStackInstrumentation + emitShadowStackInstrumentation = true + defer func() { emitShadowStackInstrumentation = old }() + prog, _, _, _, _, proof := prepareCoroFrameRootAudit( + t, coroFrameExactRootsFixture, "Method", EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + defer prog.Dispose() + if proof.exactRootCapabilityProfile() != "" || proof.exactRootCapabilityDigest() != "" || len(proof.exactRetainedRoots()) != 0 { + t.Fatalf("precise/shadow profile received exact-root capability: profile=%q digest=%q roots=%d", + proof.exactRootCapabilityProfile(), proof.exactRootCapabilityDigest(), len(proof.exactRetainedRoots())) + } +} + +func prepareCoroFrameRootAudit(t *testing.T, source, function string, options EmissionUniverseOptions) ( + llssa.Program, *ssa.Package, *EmissionUniverse, *ssa.Function, *coroPhysicalPureSSAAudit, *coroFrameRetentionProof, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, options) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + var target *ssa.Function + if direct := ssaPkg.Func(function); direct != nil { + target = direct + } else { + for _, candidate := range universe.Functions() { + if candidate != nil && candidate.Name() == function && candidate.Signature != nil && candidate.Signature.Recv() != nil { + target = candidate + break + } + } + } + if target == nil { + prog.Dispose() + t.Fatalf("function %q not found", function) + } + audit, err := newCoroPhysicalPureSSAAudit(universe, nil, target, "") + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, ssaPkg, universe, target, audit, audit.currentFrameRetentionProof() +} + +func rootNames(values []ssa.Value) []string { + names := make([]string, len(values)) + for index, value := range values { + names[index] = value.Name() + } + sort.Strings(names) + return names +} diff --git a/cl/coro_funcpc.go b/cl/coro_funcpc.go new file mode 100644 index 0000000000..01d5c65929 --- /dev/null +++ b/cl/coro_funcpc.go @@ -0,0 +1,356 @@ +/* + * 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 ( + "fmt" + "go/ast" + "go/types" + "sort" + "strings" + + "golang.org/x/tools/go/ssa" +) + +const ( + coroFuncPCABI0PackagePath = "internal/abi" + coroFuncPCABI0LocalName = "FuncPCABI0" + coroFuncPCABIInternalLocalName = "FuncPCABIInternal" + coroFuncPCABIInternalIntrinsic = "funcPCABIInternal" +) + +func coroFuncPCIntrinsicName(localName string) string { + if localName == coroFuncPCABIInternalLocalName { + return coroFuncPCABIInternalIntrinsic + } + return "funcPCABI0" +} + +// aliasPatchedFuncPCABI0Declarations records the one intentional cross-kind +// patch replacement used by Go's internal/abi package. The upstream package +// owns a bodyless Go declaration while LLGo's alternate package owns the +// compiler intrinsic that implements it. Their managed keys cannot collide: +// one is a Go symbol and the other is llgo.funcPCABI0. +// +// This bridge is deliberately narrower than managed-symbol canonicalization. +// It considers only the exact original and alternate packages already paired +// by one prepared Patch, requires the same package-scope source name and exact +// structural ABI signature, and consumes only frozen frontend classification. +// It never searches another package or guesses from Function.String/Name. +func (u *EmissionUniverse) aliasPatchedFuncPCABI0Declarations() error { + if u == nil { + return fmt.Errorf("prepare emission universe: cannot alias patched FuncPCABI0 in a nil universe") + } + packages := make([]*preparedEmissionPackage, 0, len(u.packages)) + for _, prepared := range u.packages { + if prepared != nil && prepared.hasPatch && !prepared.metadataOnly && prepared.pkgPath == coroFuncPCABI0PackagePath { + packages = append(packages, prepared) + } + } + sort.SliceStable(packages, func(i, j int) bool { + if packages[i].order != packages[j].order { + return packages[i].order < packages[j].order + } + return packages[i].identity < packages[j].identity + }) + + type operation struct { + owner *preparedEmissionPackage + original *ssa.Function + intrinsic *ssa.Function + originalKey string + } + operations := make([]operation, 0, len(packages)) + for _, prepared := range packages { + for _, localName := range []string{coroFuncPCABI0LocalName, coroFuncPCABIInternalLocalName} { + original, _ := prepared.ssa.Members[localName].(*ssa.Function) + if !coroFuncPCBodylessDeclaration(original, localName) { + continue + } + intrinsic, _ := prepared.patch.Alt.Members[localName].(*ssa.Function) + if intrinsic == nil || intrinsic.Parent() != nil || intrinsic.Signature == nil || intrinsic.Signature.Recv() != nil || + intrinsic.TypeParams() != nil || intrinsic.TypeArgs() != nil { + continue + } + + originalOwnerKey := emissionFunctionOwnerKey{function: original, owner: prepared} + originalKind, originalKindOK := u.functionKinds[originalOwnerKey] + originalKey, originalKeyOK := u.finalKeys[originalOwnerKey] + originalKeyKind, originalSymbol, originalSignature, originalKeyValid := splitManagedSymbolKey(originalKey) + if !originalKindOK || originalKind != goFunc || !originalKeyOK || !originalKeyValid || originalKeyKind != goFunc || + originalSymbol != coroFuncPCABI0PackagePath+"."+localName { + continue + } + + intrinsicOwnerKey := emissionFunctionOwnerKey{function: intrinsic, owner: prepared} + intrinsicKind, intrinsicKindOK := u.functionKinds[intrinsicOwnerKey] + intrinsicOpcode, intrinsicOpcodeOK := u.intrinsicOps[intrinsicOwnerKey] + if !intrinsicKindOK || intrinsicKind != llgoInstr || !intrinsicOpcodeOK || intrinsicOpcode != llgoFuncPCABI0 || + intrinsic.Signature == nil { + continue + } + intrinsicSignature := structuralEmissionABITypeKey(u.effectiveType(prepared, intrinsic, intrinsic.Signature)) + if originalSignature != intrinsicSignature { + return fmt.Errorf( + "prepare emission universe: patched internal/abi.%s declaration and alternate intrinsic have different structural ABI signatures", localName, + ) + } + + // selectFunction normally canonicalizes duplicate intrinsic declarations + // by managed key. That is correct for ordinary intrinsic calls, but using + // such a winner here would make the patch bridge depend on an unrelated + // alternate source name. Require one exact same-signature declaration. + matches := make([]*ssa.Function, 0, 2) + for _, member := range prepared.patch.Alt.Members { + candidate, ok := member.(*ssa.Function) + if !ok || candidate.Parent() != nil { + continue + } + if candidate.Name() != localName && + (candidate.Name() == coroFuncPCABI0LocalName || candidate.Name() == coroFuncPCABIInternalLocalName) { + // The two sanctioned source intrinsics intentionally share one + // opcode but own different frozen intrinsic symbols. + continue + } + candidateOwnerKey := emissionFunctionOwnerKey{function: candidate, owner: prepared} + candidateKind, kindOK := u.functionKinds[candidateOwnerKey] + candidateOpcode, opcodeOK := u.intrinsicOps[candidateOwnerKey] + candidateSignature := "" + if candidate.Signature != nil { + candidateSignature = structuralEmissionABITypeKey(u.effectiveType(prepared, candidate, candidate.Signature)) + } + if kindOK && candidateKind == llgoInstr && opcodeOK && candidateOpcode == llgoFuncPCABI0 && + candidateSignature == originalSignature { + matches = append(matches, candidate) + } + } + if len(matches) != 1 || matches[0] != intrinsic { + diagnostics := make([]string, len(matches)) + for index, candidate := range matches { + diagnostics[index] = emissionFunctionDiagnostic(candidate) + } + sort.Strings(diagnostics) + return fmt.Errorf( + "prepare emission universe: patched internal/abi.%s has ambiguous alternate intrinsic replacements: %s", + localName, strings.Join(diagnostics, ", "), + ) + } + if canonical := u.canonicalAlias(intrinsic); canonical == nil || canonical != intrinsic { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 alternate intrinsic is not canonical") + } + intrinsicKey, intrinsicKeyOK := u.finalKeys[intrinsicOwnerKey] + intrinsicKeyKind, intrinsicSymbol, frozenIntrinsicSignature, intrinsicKeyValid := splitManagedSymbolKey(intrinsicKey) + if !intrinsicKeyOK || !intrinsicKeyValid || intrinsicKeyKind != llgoInstr || intrinsicSymbol != coroFuncPCIntrinsicName(localName) || + frozenIntrinsicSignature != intrinsicSignature { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 alternate intrinsic has inconsistent frozen managed-symbol metadata") + } + if canonical := u.canonicalAlias(original); canonical == nil || canonical != original { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 original declaration is not canonical before patch aliasing") + } + if _, required := u.required[original]; !required { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 original declaration is not selected") + } + if _, required := u.required[intrinsic]; !required { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 alternate intrinsic is not selected") + } + if winner := prepared.winners[originalKey]; winner != original { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 original declaration is not its exact managed winner") + } + if !prepared.fromPatch[intrinsic] || prepared.fromPatch[original] { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 has inconsistent original/alternate provenance") + } + if err := u.validatePatchedFuncPCABI0AliasLifecycle(prepared, original, intrinsic); err != nil { + return err + } + operations = append(operations, operation{owner: prepared, original: original, intrinsic: intrinsic, originalKey: originalKey}) + } + } + + for _, operation := range operations { + prepared, original, intrinsic := operation.owner, operation.original, operation.intrinsic + u.aliases[original] = intrinsic + for alias, canonical := range u.aliases { + if canonical == original { + u.aliases[alias] = intrinsic + } + } + if prepared.winners[operation.originalKey] == original { + delete(prepared.winners, operation.originalKey) + } + delete(prepared.fromPatch, original) + for owner := range u.useOwners[original] { + ownerKey := emissionFunctionOwnerKey{function: original, owner: owner} + delete(u.functionKinds, ownerKey) + delete(u.intrinsicOps, ownerKey) + delete(u.finalKeys, ownerKey) + delete(u.physicalNames, ownerKey) + } + delete(u.required, original) + delete(u.useOwners, original) + delete(u.ownerStates, original) + delete(u.fnOwners, original) + delete(u.fnStates, original) + delete(u.excluded, original) + delete(u.foreignNoBlock, original) + delete(u.foreignSync, original) + delete(u.foreignSchedulerWait, original) + delete(u.foreignWorker, original) + delete(u.linkIdentities, original) + delete(u.linkOnceNames, original) + } + return nil +} + +func coroFuncPCBodylessDeclaration(function *ssa.Function, localName string) bool { + if function == nil || function.Pkg == nil || function.Parent() != nil || function.Signature == nil || function.Signature.Recv() != nil || + function.TypeParams() != nil || function.TypeArgs() != nil || functionNeedsLinkOnce(function) || len(function.Blocks) != 0 { + return false + } + declaration, _ := function.Syntax().(*ast.FuncDecl) + return declaration != nil && declaration.Body == nil && declaration.Recv == nil && declaration.Name != nil && + declaration.Name.Name == localName +} + +func (u *EmissionUniverse) validatePatchedFuncPCABI0AliasLifecycle(owner *preparedEmissionPackage, original, intrinsic *ssa.Function) error { + if _, materialized := u.materialized[original]; materialized || len(u.materializedOwners[original]) != 0 || + len(u.abiMethodReferences[original]) != 0 || len(u.abiSyncReferences[original]) != 0 || + len(u.loweredCalls[original]) != 0 || len(u.plainLoweredCalls[original]) != 0 || len(u.normalReturnBlocks[original]) != 0 { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 original declaration was materialized before exact aliasing") + } + owners := u.useOwners[original] + if len(owners) != 1 { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 original declaration has %d frozen use owners; want exact patch owner", len(owners)) + } + if _, ok := owners[owner]; !ok { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 original declaration is not owned by its exact patch") + } + state, stateOK := u.ownerStates[original][owner] + if !stateOK || state.fromPatch || state.state != pkgHasPatch || u.fnOwners[original] != owner { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 original declaration has incomplete frozen provenance") + } + intrinsicOwners := u.useOwners[intrinsic] + if len(intrinsicOwners) != 1 { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 alternate intrinsic has %d frozen use owners; want exact patch owner", len(intrinsicOwners)) + } + if _, ok := intrinsicOwners[owner]; !ok { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 alternate intrinsic is not owned by its exact patch") + } + intrinsicState, stateOK := u.ownerStates[intrinsic][owner] + if !stateOK || !intrinsicState.fromPatch || intrinsicState.state != pkgInPatch || u.fnOwners[intrinsic] != owner { + return fmt.Errorf("prepare emission universe: patched internal/abi.FuncPCABI0 alternate intrinsic has incomplete frozen provenance") + } + return nil +} + +// validateCoroFuncPCABI0CallSite freezes the same operand shapes consumed by +// funcPCABI0Value. The intrinsic emits only address selection/load operations; +// a structurally exposed Go function is still required to belong to the exact +// emission universe so its selected entry representation cannot drift later. +func (u *EmissionUniverse) validateCoroFuncPCABI0CallSite(direct *ssa.Call) error { + if direct == nil || direct.Common() == nil || direct.Common().IsInvoke() { + return fmt.Errorf("emission universe intrinsic call semantics: llgo.funcPCABI0 must be an exact direct call") + } + args := direct.Common().Args + signature := direct.Common().Signature() + if len(args) != 1 || signature == nil || signature.Recv() != nil || signature.Variadic() || + signature.Params() == nil || signature.Params().Len() != 1 || + signature.Results() == nil || signature.Results().Len() != 1 { + return fmt.Errorf( + "emission universe intrinsic call semantics: llgo.funcPCABI0 call %q requires the exact func(any) uintptr shape", direct.String(), + ) + } + parameter, ok := types.Unalias(signature.Params().At(0).Type()).Underlying().(*types.Interface) + if !ok || !parameter.Empty() { + return fmt.Errorf( + "emission universe intrinsic call semantics: llgo.funcPCABI0 call %q requires the exact func(any) uintptr shape", direct.String(), + ) + } + result, ok := types.Unalias(signature.Results().At(0).Type()).Underlying().(*types.Basic) + if !ok || result.Kind() != types.Uintptr { + return fmt.Errorf( + "emission universe intrinsic call semantics: llgo.funcPCABI0 call %q requires the exact func(any) uintptr shape", direct.String(), + ) + } + if err := u.validateCoroFuncPCABI0Value(args[0]); err != nil { + return fmt.Errorf("emission universe intrinsic call semantics: llgo.funcPCABI0 call %q: %w", direct.String(), err) + } + return nil +} + +func (u *EmissionUniverse) validateCoroFuncPCABI0Value(value ssa.Value) error { + switch value := value.(type) { + case *ssa.MakeInterface: + return u.validateCoroFuncPCABI0Value(value.X) + case *ssa.Function: + if extractTrampolineCName(value.Name()) != "" { + return nil + } + if canonical, resolved := u.Resolve(value); !resolved || canonical == nil { + return fmt.Errorf("target function %q is outside the frozen emission universe", value.Name()) + } + return nil + case *ssa.MakeClosure: + return u.validateCoroFuncPCABI0Value(value.Fn) + case *ssa.Const: + if value.IsNil() { + return fmt.Errorf("argument is statically nil") + } + return fmt.Errorf("argument has unsupported SSA type %T", value) + default: + if value != nil && value.Type() != nil { + if _, ok := types.Unalias(value.Type()).Underlying().(*types.Interface); ok { + return nil + } + } + return fmt.Errorf("argument has unsupported SSA type %T", value) + } +} + +// coroFuncPCABI0RawStaticOperand reports the structural function-address form +// whose transient MakeInterface must not by itself demand a dispatch wrapper. +// Dynamic interface values remain ordinary ABI roots and return false. +func coroFuncPCABI0RawStaticOperand(direct *ssa.Call) bool { + target, exact := coroFuncPCABI0ExactStaticOperand(direct) + if !exact { + return false + } + // funcPCABI0Value does not compile a Go function value for C trampolines; + // it synthesizes the foreign declaration and takes that address directly. + // Do not advertise such an operand as a managed raw-function singleton to + // the coroutine analyzer, whose raw-address proof intentionally requires a + // canonical target in the emission universe. + return extractTrampolineCName(target.Name()) == "" +} + +func coroFuncPCABI0ExactStaticOperand(direct *ssa.Call) (*ssa.Function, bool) { + if direct == nil || direct.Common() == nil || len(direct.Common().Args) != 1 { + return nil, false + } + boxed, ok := direct.Common().Args[0].(*ssa.MakeInterface) + if !ok { + return nil, false + } + refs := boxed.Referrers() + if refs == nil || len(*refs) != 1 || (*refs)[0] != direct { + return nil, false + } + target, ok := boxed.X.(*ssa.Function) + if !ok || target == nil || len(target.FreeVars) != 0 { + return nil, false + } + return target, true +} diff --git a/cl/coro_generic_closure_instance_test.go b/cl/coro_generic_closure_instance_test.go new file mode 100644 index 0000000000..15b70eca9c --- /dev/null +++ b/cl/coro_generic_closure_instance_test.go @@ -0,0 +1,182 @@ +//go:build !llgo + +/* + * Copyright (c) 2026 The XGo Authors. 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" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroMaterializedGenericClosureInstance(t *testing.T) { + llssa.Initialize(llssa.InitAll) + const source = `package foo +type Box[T any] struct { value T } +func (b *Box[T]) All() func(func(T) bool) { + return func(yield func(T) bool) { var zero T; yield(zero) } +} +func Yield(value int) bool { return value != 0 } +func Root(b *Box[int]) { b.All()(Yield) } +` + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + var instance *ssa.Function + var outerCall *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + if call.Common().StaticCallee() != nil { + instance = call.Common().StaticCallee() + } else { + outerCall = call + } + } + } + anonCount := 0 + if instance != nil { + anonCount = len(instance.AnonFuncs) + } + if instance == nil || anonCount != 1 || outerCall == nil { + t.Fatalf("generic receiver instance = %v, anonymous functions = %d, outer call = %v", + instance, anonCount, outerCall) + } + closure := instance.AnonFuncs[0] + if !coroMaterializedGenericInstance(instance) || !coroMaterializedGenericInstance(closure) { + t.Fatalf("materialized generic instance=%t closure=%t", coroMaterializedGenericInstance(instance), coroMaterializedGenericInstance(closure)) + } + if typeParamCount(closure.TypeParams()) == 0 || typeParamCount(closure.Signature.TypeParams()) != 0 || + closure.Parent() != instance || closure.Origin() == nil || len(closure.TypeArgs()) != 1 { + t.Fatalf("generic closure metadata is not the expected stale-declaration/concrete-signature shape: %+v", closure) + } + var innerCall *ssa.Call + for _, block := range closure.Blocks { + for _, instruction := range block.Instrs { + if call, ok := instruction.(*ssa.Call); ok && call.Common().StaticCallee() == nil { + innerCall = call + } + } + } + if innerCall == nil { + t.Fatal("materialized closure has no dynamic callback call") + } + + var prog llssa.Program + if test.target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, test.target) + } + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + yield := ssaPkg.Func("Yield") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == instance { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + if fn == closure || fn == yield { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, NeedsDispatch: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyUnknownCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.UnknownTarget, error) { + if call == outerCall || call == innerCall { + return coro.UnknownManagedDispatch, nil + } + return coro.UnknownManaged, nil + }, + }) + if err != nil { + t.Fatal(err) + } + instancePlan, ok := plan.FunctionPlan(instance) + if !ok || instancePlan.FuncRep != coro.DirectCoro || instancePlan.Emission != coro.EmitCoroutine { + t.Fatalf("generic receiver instance plan = %+v, present=%t; want direct coroutine", instancePlan, ok) + } + for name, fn := range map[string]*ssa.Function{"closure": closure, "yield": yield} { + functionPlan, ok := plan.FunctionPlan(fn) + if !ok || functionPlan.FuncRep != coro.Dispatch || functionPlan.Emission != coro.EmitCoroutine { + t.Fatalf("%s plan = %+v, present=%t; want coroutine Dispatch", name, functionPlan, ok) + } + } + callbackPlan, ok := plan.ValuePlan(closure.Params[0]) + if !ok || len(callbackPlan.Funcs) != 1 || callbackPlan.Funcs[0].Rep != coro.Dispatch || + len(callbackPlan.Funcs[0].Path) != 0 || !callbackPlan.Funcs[0].MayBeNil { + t.Fatalf("nested callback ValuePlan = %+v, present=%t; want nullable scalar Dispatch", callbackPlan, ok) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := compiled.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify recursive dispatch before CoroSplit: %v\n%s", err, module.String()) + } + ir := module.String() + if strings.Count(ir, coroPlainDispatchDescriptorPrefix) < 2 || + !strings.Contains(ir, "{ ptr, ptr }") || !strings.Contains(ir, "call void @"+coroAwaitPrepareHookV1) { + t.Fatalf("recursive descriptor transport is incomplete:\n%s", ir) + } + runCoroABITestPipeline(t, prog, module) + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify recursive dispatch after CoroSplit: %v\n%s", err, module.String()) + } + }) + } +} diff --git a/cl/coro_generic_receiver_instance_test.go b/cl/coro_generic_receiver_instance_test.go new file mode 100644 index 0000000000..bc8a0c6b2a --- /dev/null +++ b/cl/coro_generic_receiver_instance_test.go @@ -0,0 +1,186 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroMaterializedGenericReceiverInstanceNativeAndWasm(t *testing.T) { + llssa.Initialize(llssa.InitAll) + const source = `package foo +type Pointer[T any] struct { value *T } +func (p *Pointer[T]) Load() *T { return nil } +func Root(p *Pointer[int]) *int { return p.Load() } +` + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + var instance *ssa.Function + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Common().StaticCallee() != nil { + instance = call.Common().StaticCallee() + } + } + } + if instance == nil { + t.Fatal("generic receiver call has no static instance target") + } + if !coroMaterializedGenericInstance(instance) || typeParamCount(instance.Signature.RecvTypeParams()) != 1 { + t.Fatalf("generic receiver target = %v, materialized=%t recv-type-params=%d", + instance, coroMaterializedGenericInstance(instance), typeParamCount(instance.Signature.RecvTypeParams())) + } + + var prog llssa.Program + if test.target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, test.target) + } + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + sourceSig, err := universe.coroPhysicalSourceSignature(instance) + if err != nil { + t.Fatal(err) + } + if sourceSig.Recv() != nil || sourceSig.RecvTypeParams().Len() != 0 || + sourceSig.Params().Len() != 1 || !strings.Contains(sourceSig.Params().At(0).Type().String(), "Pointer[int]") { + t.Fatalf("normalized generic receiver signature = %v", sourceSig) + } + + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == instance { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + instancePlan, ok := plan.FunctionPlan(instance) + if !ok || instancePlan.Emission != coro.EmitCoroutine || instancePlan.Primary != coro.PrimaryCoroutine || + !instancePlan.Demand.Contains(coro.AsyncDemand) { + t.Fatalf("generic receiver instance plan = %+v, present=%t", instancePlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := compiled.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify generic receiver instance before CoroSplit: %v\n%s", err, module.String()) + } + rootIR := requireCoroPhysicalFunction(t, module, "foo.Root").String() + if !strings.Contains(rootIR, "$coro") || !strings.Contains(rootIR, "call void @"+coroAwaitPrepareHookV1) { + t.Fatalf("generic receiver call did not use child await:\n%s", rootIR) + } + runCoroABITestPipeline(t, prog, module) + if module.NamedFunction("foo.Root$coro.resume").IsNil() { + t.Fatalf("CoroSplit lost generic receiver caller resume:\n%s", module.String()) + } + }) + } +} + +func TestCoroMaterializedGenericPointerMethodWrapper(t *testing.T) { + const source = `package foo +type Pointer[T any] struct { value *T } +func (p Pointer[T]) Value() *T { return p.value } +func Root(p *Pointer[int]) *int { return p.Value() } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + selection := ssaPkg.Prog.MethodSets.MethodSet(root.Params[0].Type()).Lookup(ssaPkg.Pkg, "Value") + if selection == nil { + t.Fatal("generic pointer method selection is absent") + } + wrapper := ssaPkg.Prog.MethodValue(selection) + if wrapper == nil || !strings.HasPrefix(wrapper.Synthetic, "wrapper for ") || + wrapper.Pkg != nil || typeParamCount(wrapper.Signature.RecvTypeParams()) != 1 { + t.Fatalf("generic pointer method wrapper has unexpected shape: %v synthetic=%q", wrapper, func() string { + if wrapper == nil { + return "" + } + return wrapper.Synthetic + }()) + } + if !coroMaterializedGenericMethodWrapper(wrapper) || !coroMaterializedGenericCallable(wrapper) { + t.Fatalf("exact generated generic method wrapper was not recognized:\n%s", wrapper) + } + + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + sig, err := universe.coroPhysicalSourceSignature(wrapper) + if err != nil { + t.Fatal(err) + } + if sig.Recv() != nil || typeParamCount(sig.RecvTypeParams()) != 0 || sig.Params().Len() != 1 || + !strings.Contains(sig.Params().At(0).Type().String(), "*foo.Pointer[int]") { + t.Fatalf("generic pointer wrapper physical signature = %v", sig) + } + + originalSynthetic := wrapper.Synthetic + wrapper.Synthetic = "wrapper for forged generic method" + if coroMaterializedGenericMethodWrapper(wrapper) { + t.Fatal("forged generic method wrapper identity was accepted") + } + wrapper.Synthetic = originalSynthetic +} diff --git a/cl/coro_implicit_fault.go b/cl/coro_implicit_fault.go new file mode 100644 index 0000000000..36a8fc2e99 --- /dev/null +++ b/cl/coro_implicit_fault.go @@ -0,0 +1,404 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroFaultPrepareHookV1 = "__llgo_coro_fault_prepare_v1" + coroFaultPayloadHookV1 = "__llgo_coro_fault_payload_v1" +) + +const ( + coroFaultNilV1 uint32 = iota + 1 + coroFaultIndexBoundsV1 + coroFaultChannelSendClosedV1 + coroFaultUnsafeSliceLenV1 + coroFaultUnsafeSliceNilV1 + coroFaultChannelCloseNilV1 + coroFaultChannelCloseClosedV1 + coroFaultUnsafeStringLenV1 + coroFaultUnsafeStringNilV1 + coroFaultSliceConvertV1 + coroFaultLimitV1 +) + +func coroFaultPrepareSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + types.NewParam(token.NoPos, nil, "header", pointer), + types.NewParam(token.NoPos, nil, "kind", types.Typ[types.Uint32]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroFaultPayloadSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "kind", types.Typ[types.Uint32]), + types.NewParam(token.NoPos, nil, "typeOut", pointer), + types.NewParam(token.NoPos, nil, "dataOut", pointer), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +// compileCoroImplicitNilFieldAddrGuard splits the current source block before +// LLVM forms the GEP. A nullable Go pointer is retained as an exact coroutine- +// frame root, but its access semantics never rely on a native signal or wasm +// trap: nil takes the compiler-owned explicit-status terminal edge and only +// the non-nil block may construct the field address. +func (p *context) compileCoroImplicitNilFieldAddrGuard( + b llssa.Builder, + field *ssa.FieldAddr, + base llssa.Expr, +) llssa.Expr { + body := p.coroBody() + if body == nil || field == nil || field.X == nil || b == nil || b.Func != p.fn { + panic("implicit nil FieldAddr guard escaped its physical coroutine body") + } + if !p.coroEmissionExplicitStatus() || + body.abi.version < coroPhysicalABIVersionV1 { + panic("implicit nil FieldAddr guard requires the PhysicalABIV1 explicit-status panic ABI") + } + if _, ok := types.Unalias(field.X.Type()).Underlying().(*types.Pointer); !ok { + panic(fmt.Sprintf("implicit nil FieldAddr base %T is not pointer-shaped", field.X.Type())) + } + return p.compileCoroImplicitNilAccessGuard(b, base) +} + +// compileCoroImplicitNilDerefGuard gives an ordinary typed load the same +// platform-independent explicit-status nil semantics as FieldAddr. Lifetime +// was certified separately by the exact frame-retention proof; this guard does +// not infer non-nil from a pointer type or from closure capture. +func (p *context) compileCoroImplicitNilDerefGuard( + b llssa.Builder, + deref *ssa.UnOp, + base llssa.Expr, +) llssa.Expr { + if p == nil || p.coroBody() == nil || deref == nil || deref.Op != token.MUL || deref.X == nil || + b == nil || b.Func != p.fn { + panic("implicit nil typed-load guard escaped its physical coroutine body") + } + if _, ok := types.Unalias(deref.X.Type()).Underlying().(*types.Pointer); !ok { + panic(fmt.Sprintf("implicit nil typed-load base %T is not pointer-shaped", deref.X.Type())) + } + return p.compileCoroImplicitNilAccessGuard(b, base) +} + +func (p *context) compileCoroImplicitNilAccessGuard(b llssa.Builder, base llssa.Expr) llssa.Expr { + body := p.coroBody() + if body == nil || b == nil || b.Func != p.fn { + panic("implicit nil access guard escaped its physical coroutine body") + } + if !p.coroEmissionExplicitStatus() || + body.abi.version < coroPhysicalABIVersionV1 { + panic("implicit nil access guard requires the PhysicalABIV1 explicit-status panic ABI") + } + + isNil := b.BinOp(token.EQL, base, b.Prog.Nil(base.Type)) + p.compileCoroFaultConditionGuard(b, isNil, coroFaultNilV1) + return base +} + +// compileCoroIndexBoundsGuard routes an out-of-range predicate through the +// target-neutral explicit-status fault ABI. The caller emits an unchecked GEP +// or load only in the normal continuation block. +func (p *context) compileCoroIndexBoundsGuard(b llssa.Builder, outOfRange llssa.Expr) { + if outOfRange.IsNil() { + return + } + p.compileCoroFaultConditionGuard(b, outOfRange, coroFaultIndexBoundsV1) +} + +func (p *context) compileCoroIndexAddrPlanned( + b llssa.Builder, + operation *ssa.IndexAddr, + base, index llssa.Expr, + plan coroPhysicalInstructionPlan, +) llssa.Expr { + body := p.coroBody() + if body == nil || operation == nil || operation.X == nil || + b == nil || b.Func != p.fn { + panic("structured coroutine IndexAddr escaped its physical body") + } + if plan.recipe != coroPhysicalInstructionIndexAddr { + panic("structured coroutine IndexAddr has the wrong physical recipe") + } + var limit llssa.Expr + switch plan.container { + case coroPhysicalContainerSlice: + if plan.boundsGuard { + limit = b.SliceLen(base) + } + case coroPhysicalContainerArrayPointer: + if plan.boundsGuard { + limit = b.Prog.IntVal(uint64(plan.bound), b.Prog.Int()) + } + default: + panic("structured coroutine IndexAddr has an invalid frozen container") + } + // Indexing a pointer-to-array first implicitly dereferences the pointer. + // Preserve Go's fault order: nil pointer before index bounds. A nil slice, + // by contrast, is a valid length-zero slice and therefore takes only the + // ordinary bounds fault for any element access. + if plan.nilGuard { + p.observeCoroPhysicalNilGuard(operation) + base = p.compileCoroImplicitNilAccessGuard(b, base) + } + normalized := index + if plan.boundsGuard { + p.observeCoroPhysicalBoundsGuard(operation) + var outOfRange llssa.Expr + normalized, outOfRange = b.IndexBounds(index, limit) + p.compileCoroIndexBoundsGuard(b, outOfRange) + } + return b.IndexAddrUnchecked(base, normalized) +} + +// compileCoroIndexGuarded implements every concrete x/tools Index container +// shape without calling the native-stack CheckIndexRange helper. String and +// array values use LLSSA's unchecked value load; slice and *array values use +// the corresponding unchecked address followed by a typed load. In all cases +// the address/load is emitted only in the continuation dominated by the Go +// bounds check. A nullable *array gets the same structured nil-fault edge as +// IndexAddr after its bounds check. +func (p *context) compileCoroIndexPlanned( + b llssa.Builder, + operation *ssa.Index, + base, index llssa.Expr, + takeArrayAddr func() (addr llssa.Expr, zero bool), + plan coroPhysicalInstructionPlan, +) llssa.Expr { + if p == nil || p.coroBody() == nil || operation == nil || operation.X == nil || + operation.Index == nil || b == nil || b.Func != p.fn { + panic("structured coroutine Index escaped its physical body") + } + + if plan.recipe != coroPhysicalInstructionIndex { + panic("structured coroutine Index has the wrong physical recipe") + } + var limit llssa.Expr + if plan.boundsGuard { + switch plan.container { + case coroPhysicalContainerString: + limit = b.StringLen(base) + case coroPhysicalContainerArray, coroPhysicalContainerArrayPointer: + limit = b.Prog.IntVal(uint64(plan.bound), b.Prog.Int()) + case coroPhysicalContainerSlice: + limit = b.SliceLen(base) + default: + panic("structured coroutine Index has an invalid frozen container") + } + } else if plan.container != coroPhysicalContainerArray && plan.container != coroPhysicalContainerArrayPointer { + panic("unchecked coroutine Index is not a fixed-array recipe") + } + if plan.nilGuard { + p.observeCoroPhysicalNilGuard(operation) + base = p.compileCoroImplicitNilAccessGuard(b, base) + } + + normalized := index + if plan.boundsGuard { + p.observeCoroPhysicalBoundsGuard(operation) + var outOfRange llssa.Expr + normalized, outOfRange = b.IndexBounds(index, limit) + p.compileCoroIndexBoundsGuard(b, outOfRange) + } + + switch plan.container { + case coroPhysicalContainerString, coroPhysicalContainerArray: + return b.IndexUnchecked(base, normalized, takeArrayAddr) + case coroPhysicalContainerSlice, coroPhysicalContainerArrayPointer: + return b.Load(b.IndexAddrUnchecked(base, normalized)) + default: + panic("structured coroutine Index lost its validated container shape") + } +} + +// compileCoroSliceGuarded implements two- and three-index Go slicing without +// calling the native-stack StringSlice2/NewSlice2/NewSlice3Bounds helpers. +// Operand evaluation has already happened in source order. A nullable *array +// then takes the structured nil edge, followed by the exact inclusive slice +// bounds predicate; only the dominated continuation constructs the aggregate. +func (p *context) compileCoroSlicePlanned( + b llssa.Builder, + operation *ssa.Slice, + base, low, high, max llssa.Expr, + plan coroPhysicalInstructionPlan, +) llssa.Expr { + body := p.coroBody() + if body == nil || operation == nil || operation.X == nil || + b == nil || b.Func != p.fn { + panic("structured coroutine Slice escaped its physical body") + } + if !p.coroEmissionExplicitStatus() || + body.abi.version < coroPhysicalABIVersionV1 { + panic("structured coroutine Slice requires the PhysicalABIV1 explicit-status panic ABI") + } + if plan.recipe != coroPhysicalInstructionSlice || !plan.boundsGuard { + panic("structured coroutine Slice has the wrong physical recipe") + } + + zero := b.Prog.IntVal(0, b.Prog.Int()) + if low.IsNil() { + low = zero + } + var limit llssa.Expr + switch plan.container { + case coroPhysicalContainerString: + if !max.IsNil() { + panic("structured coroutine Slice basic base is not a two-index string") + } + limit = b.StringLen(base) + if high.IsNil() { + high = limit + } + case coroPhysicalContainerSlice: + limit = b.SliceCap(base) + if high.IsNil() { + high = b.SliceLen(base) + } + case coroPhysicalContainerArrayPointer: + if plan.nilGuard { + p.observeCoroPhysicalNilGuard(operation) + base = p.compileCoroImplicitNilAccessGuard(b, base) + } + limit = b.Prog.IntVal(uint64(plan.bound), b.Prog.Int()) + if high.IsNil() { + high = limit + } + default: + panic(fmt.Sprintf("structured coroutine Slice has invalid frozen container %d", plan.container)) + } + + p.observeCoroPhysicalBoundsGuard(operation) + low, high, max, outOfRange := b.SliceBounds(low, high, max, limit) + p.compileCoroIndexBoundsGuard(b, outOfRange) + return b.SliceUnchecked(base, low, high, max) +} + +func (p *context) compileCoroFaultConditionGuard(b llssa.Builder, condition llssa.Expr, kind uint32) { + if p == nil || p.coroBody() == nil || b == nil || b.Func != p.fn || condition.IsNil() { + panic("structured coroutine fault guard escaped its physical body") + } + fault := b.Func.MakeBlock() + normal := b.Func.MakeBlock() + b.If(condition, fault, normal) + + b.SetBlockEx(fault, llssa.AtEnd, false) + p.compileCoroTerminalFault(b, kind) + + // The fault path is terminal (possibly after the static drainer). Continue + // source lowering only in the block dominated by base != nil. + b.SetBlockContinuation(normal) +} + +// compileCoroTerminalFault enters the one target-neutral explicit-status +// fault path shared by implicit language faults and typed runtime outcomes +// such as send-on-closed-channel. Static defers drain before publication; a +// body without cleanup publishes immediately. The call never returns to the +// source continuation. +func (p *context) compileCoroTerminalFault(b llssa.Builder, kind uint32) { + body := p.coroBody() + if body == nil || b == nil || b.Func != p.fn { + panic("coroutine terminal fault escaped its physical body") + } + if cleanup := body.cleanup; cleanup != nil { + cleanup.enterFault(p, b, kind) + } else { + body.implicitFault(p, b, kind) + } +} + +func (c *coroBodyContext) implicitFault(p *context, b llssa.Builder, kind uint32) { + if c == nil || p == nil || b == nil || c.abi.version < coroPhysicalABIVersionV1 || c.finalSuspend == nil { + panic("implicit nil fault requires a PhysicalABIV1 body and shared final suspend") + } + c.publishState(b, coroSuspendPanic, coroLifecycleFinalSuspended, c.terminalStateID()) + prepare := p.pkg.NewFunc(coroFaultPrepareHookV1, coroFaultPrepareSignature(), llssa.InC) + b.Call( + prepare.Expr, + c.task, + c.coro.Handle(), + b.Convert(b.Prog.VoidPtr(), c.header), + b.Prog.IntVal(uint64(kind), b.Prog.Uint32()), + ) + b.Jump(c.finalSuspend) +} + +// materializeCoroFaultPayload loads the stable Go panic pair for one structured +// language fault without publishing a terminal scheduler outcome. A cleanup +// drainer must expose that pair to each direct deferred child before deciding +// whether the panic remains terminal, so the older fault_prepare hook is too +// late for this path. The output cells live in the LLVM coroutine ramp: a +// source fault may be emitted in a resume-only block where a local alloca would +// not dominate CoroSplit's generated resume function. +func (p *context) materializeCoroFaultPayload( + b llssa.Builder, kind uint32, +) (typeWord, dataWord llssa.Expr) { + body := p.coroBody() + if body == nil || b == nil || b.Func != p.fn || + !p.coroEmissionExplicitStatus() || + body.abi.version < coroPhysicalABIVersionV1 { + panic("coroutine fault payload materialization requires an explicit-status PhysicalABIV1 body") + } + typeSlot := p.coroFrameAlloca(p.prog.VoidPtr()) + dataSlot := p.coroFrameAlloca(p.prog.VoidPtr()) + b.Store(typeSlot, p.prog.Nil(p.prog.VoidPtr())) + b.Store(dataSlot, p.prog.Nil(p.prog.VoidPtr())) + payload := p.pkg.NewFunc(coroFaultPayloadHookV1, coroFaultPayloadSignature(), llssa.InC) + b.Call( + payload.Expr, + p.prog.IntVal(uint64(kind), p.prog.Uint32()), + b.Convert(p.prog.VoidPtr(), typeSlot), + b.Convert(p.prog.VoidPtr(), dataSlot), + ) + return b.Load(typeSlot), b.Load(dataSlot) +} + +// enterFault turns a source-body implicit fault into the same recoverable +// panic overlay as an explicit panic. The canonical Recover continuation is +// retained as the base; if no direct deferred child recovers the payload, the +// shared cleanup panic block publishes it through panic_prepare_v1. +func (s *coroStaticCleanupState) enterFault(p *context, b llssa.Builder, kind uint32) { + if s == nil || p == nil || p.coroBody() == nil || b == nil { + panic("implicit nil fault cleanup has no active coroutine state") + } + typeWord, dataWord := p.materializeCoroFaultPayload(b, kind) + s.enterPanic(b, typeWord, dataWord) +} + +// replaceFault is the cleanup-internal counterpart used by operations such as +// invoking a nil deferred function descriptor. The popped record has already +// become at-most-once; preserve its existing normal/RunDefers/cancel base while +// replacing any older panic with the newer implicit fault. +func (s *coroStaticCleanupState) replaceFault(p *context, b llssa.Builder, kind uint32) { + if s == nil || p == nil || p.coroBody() == nil || b == nil { + panic("implicit cleanup fault has no active coroutine state") + } + typeWord, dataWord := p.materializeCoroFaultPayload(b, kind) + s.replacePanic(b, typeWord, dataWord) +} diff --git a/cl/coro_implicit_fault_lane_test.go b/cl/coro_implicit_fault_lane_test.go new file mode 100644 index 0000000000..6b529786a9 --- /dev/null +++ b/cl/coro_implicit_fault_lane_test.go @@ -0,0 +1,198 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +func TestCoroCompilerElidedImplicitFaultHelperInventoryFailsClosed(t *testing.T) { + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, `package runtime +import "unsafe" +func AllocZ(size uintptr) unsafe.Pointer { return nil } +`) + callerPkg := testProg.addPackage(t, "example.com/emission/implicitinventory", `package implicitinventory +func Root() *byte { return new(byte) } +`) + testProg.ssa.Build() + + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: callerPkg.ssa, Files: []*ast.File{callerPkg.file}}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + t.Fatal(err) + } + + root := callerPkg.ssa.Func("Root") + var allocation *ssa.Alloc + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.Alloc); ok && candidate.Heap { + allocation = candidate + } + } + } + if allocation == nil { + t.Fatal("implicit helper inventory fixture has no heap allocation") + } + audit, err := newCoroPhysicalPureSSAAudit(universe, nil, root, "") + if err != nil { + t.Fatal(err) + } + if helpers := strings.Join(universe.loweredRuntimeHelpers(audit.ctx, allocation), ","); helpers != "AllocZ" { + t.Fatalf("heap allocation helper inventory = %q, want AllocZ", helpers) + } + if reason := audit.requireOnlyCompilerElidedRuntimeHelpers( + allocation, "CheckIndexRange", "AssertNilDeref", + ); !strings.Contains(reason, "non-elided runtime helper(s) AllocZ") { + t.Fatalf("unexpected implicit-fault helper inventory rejection = %q", reason) + } +} + +func TestCoroImplicitIndexAddrRequiresExplicitStatus(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func Root(values []byte, index int) byte { return values[index] } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + }) + if err != nil { + t.Fatal(err) + } + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "") + if err != nil { + t.Fatal(err) + } + proof := audit.currentFrameRetentionProof() + + var indexAddr *ssa.IndexAddr + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.IndexAddr); ok { + indexAddr = candidate + } + } + } + if indexAddr == nil { + t.Fatal("explicit-status gate fixture has no IndexAddr") + } + if proof == nil || !proof.provesGuardableStableAddress(indexAddr, indexAddr) { + t.Fatal("dynamic slice IndexAddr lacks its guardable frame-retention proof") + } + if helpers := strings.Join(universe.loweredRuntimeHelpers(audit.ctx, indexAddr), ","); helpers != "CheckIndexRange" { + t.Fatalf("dynamic slice IndexAddr helpers = %q, want CheckIndexRange", helpers) + } + if reason := audit.validateIndexAddr(indexAddr); !strings.Contains(reason, "index base is not a fixed-array pointer") { + t.Fatalf("IndexAddr without ExplicitStatus rejection = %q", reason) + } + audit.allowImplicitNilFault = true + if reason := audit.validateIndexAddr(indexAddr); reason != "" { + t.Fatalf("IndexAddr with ExplicitStatus rejected: %s", reason) + } +} + +func TestEmissionUniverseImplicitIndexPlainHelperRetainsRawDemand(t *testing.T) { + testProg := newEmissionTestProgram() + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, `package runtime +func CheckIndexRange(ok bool, index int64, signed bool, length int) {} +`) + callerPkg := testProg.addPackage(t, "example.com/emission/implicitplain", `package implicitplain +func Root(values []byte, index int) byte { return values[index] } +`) + testProg.ssa.Build() + + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: callerPkg.ssa, Files: []*ast.File{callerPkg.file}}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + t.Fatal(err) + } + root := callerPkg.ssa.Func("Root") + helper := runtimePkg.ssa.Func("CheckIndexRange") + if target, ok, err := universe.ResolveCoroPlainLoweredCall(root, "CheckIndexRange"); err != nil || !ok || target != helper { + t.Fatalf("plain CheckIndexRange = %v, %t, %v; want exact runtime helper", target, ok, err) + } + if calls, err := universe.CoroLoweredCalls(root); err != nil { + t.Fatal(err) + } else if len(calls) != 0 { + t.Fatalf("physical Index lowered calls = %+v, want compiler-owned fault guard", calls) + } + + ssaUniverse, err := coro.NewSSAEmissionUniverse(testProg.ssa, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(testProg.ssa, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + ClassifyRawPlainDemandReferences: universe.CoroSyncDemandReferences, + }) + if err != nil { + t.Fatal(err) + } + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("physical Index owner plan = %+v, present=%t; want coroutine without helper await", rootPlan, ok) + } + helperPlan, ok := plan.FunctionPlan(helper) + if !ok || helperPlan.ManagedDemand != coro.NoDemand || !helperPlan.RawPlainDemand || + !helperPlan.RawPlainOnly || helperPlan.Emission != coro.EmitRawPlain || !plan.HasRawPlainVariant(helper) { + t.Fatalf("plain CheckIndexRange plan = %+v, present=%t, raw-variant=%t", helperPlan, ok, plan.HasRawPlainVariant(helper)) + } +} diff --git a/cl/coro_implicit_fault_test.go b/cl/coro_implicit_fault_test.go new file mode 100644 index 0000000000..3290b1b6c2 --- /dev/null +++ b/cl/coro_implicit_fault_test.go @@ -0,0 +1,383 @@ +//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 ( + "bytes" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroImplicitNilFaultFixture = `package foo + +var Sink uint32 + +type Box struct { Value uint32 } +type Empty struct{} + +func Cleanup() { Sink++ } +func RecoverFault() { recover() } + +func Nullable(box *Box) uint32 { return box.Value } +func EmptyLoad(value *Empty) Empty { return *value } + +func Guarded(box *Box) uint32 { + if box == nil { return 0 } + return box.Value +} + +func WithCleanup(box *Box) { + defer Cleanup() + Sink = box.Value +} + +func WithRecover(box *Box) { + defer RecoverFault() + Sink = box.Value +} + +func StringAt(value string, index int) byte { return value[index] } + +func ConstantStringAt(index int) byte { return "0123456789abcdef"[index] } + +type Array4 [4]uint32 + +func ArrayAt(values Array4, index int) uint32 { return [4]uint32(values)[index] } + +func SliceAt(values []uint32, index int) uint32 { return values[index] } + +func PointerEqual(first, second *Box) bool { return first == second } + +type ValueReceiver struct { Value uint32 } +func (value ValueReceiver) Touch() { Sink += value.Value } +func ValueReceiverCall(value *ValueReceiver) { value.Touch() } +` + +func TestCoroImplicitNilFieldAddrNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroImplicitNilFaultFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify implicit nil fault before CoroSplit: %v\n%s", err, module.String()) + } + for _, name := range []string{"Nullable", "EmptyLoad", "WithCleanup", "ValueReceiverCall"} { + function := functions[name] + functionPlan, ok := plan.FunctionPlan(function) + if !ok || functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want may-unwind coroutine", name, functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + wantPrepare, wantPayload := 1, 0 + if name == "WithCleanup" { + wantPrepare, wantPayload = 0, 1 + } + if got := strings.Count(body, "call void @"+coroFaultPrepareHookV1); got != wantPrepare { + t.Fatalf("%s nil-fault prepare calls = %d, want %d:\n%s", name, got, wantPrepare, body) + } + if got := strings.Count(body, "call void @"+coroFaultPayloadHookV1); got != wantPayload { + t.Fatalf("%s nil-fault payload calls = %d, want %d:\n%s", name, got, wantPayload, body) + } + if !strings.Contains(body, "icmp eq ptr") || strings.Contains(body, "AssertNilDeref") { + t.Fatalf("%s did not use an inline pointer guard exclusively:\n%s", name, body) + } + if name != "WithCleanup" { + if hook := strings.Index(body, "call void @"+coroFaultPrepareHookV1); hook < 0 || + !strings.Contains(body[:hook], "store i16 5") || !strings.Contains(body[:hook], "store i16 4") { + t.Fatalf("%s did not publish Panic/FinalSuspended before its hook:\n%s", name, body) + } + } + } + + guarded := requireCoroPhysicalFunction(t, module, "foo.Guarded").String() + if strings.Contains(guarded, coroFaultPrepareHookV1) || strings.Contains(guarded, "AssertNilDeref") { + t.Fatalf("dominated non-nil FieldAddr retained a runtime/terminal guard:\n%s", guarded) + } + cleanup := requireCoroPhysicalFunction(t, module, "foo.WithCleanup").String() + payload := strings.Index(cleanup, "call void @"+coroFaultPayloadHookV1) + if !strings.Contains(cleanup, "switch i32") || payload < 0 || !strings.Contains(cleanup, "foo.Cleanup") || + !strings.Contains(cleanup, "call void @"+coroPanicPrepareHookV1) || + strings.Contains(cleanup, "call void @"+coroFaultPrepareHookV1) { + t.Fatalf("implicit nil fault bypassed the static cleanup dispatcher:\n%s", cleanup) + } + recovering := requireCoroPhysicalFunction(t, module, "foo.WithRecover").String() + if strings.Count(recovering, "call void @"+coroFaultPayloadHookV1) != 1 || + strings.Contains(recovering, "call void @"+coroFaultPrepareHookV1) || + countCoroIRDirectCalls(requireCoroPhysicalFunction(t, module, "foo.WithRecover"), coroAwaitPrepareHookV1) != 1 || + countCoroIRDirectCalls(requireCoroPhysicalFunction(t, module, "foo.RecoverFault"), coroRecoverTakeHookV1) != 1 { + t.Fatalf("recoverable implicit fault does not use the shared panic/child transaction:\nWithRecover:\n%s\nRecoverFault:\n%s", + recovering, requireCoroPhysicalFunction(t, module, "foo.RecoverFault").String()) + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{"Nullable", "EmptyLoad", "WithCleanup", "ValueReceiverCall"} { + resume := module.NamedFunction("foo." + name + "$coro.resume") + wantPrepare, wantPayload := 1, 0 + if name == "WithCleanup" { + wantPrepare, wantPayload = 0, 1 + } + if resume.IsNil() || strings.Count(resume.String(), "call void @"+coroFaultPrepareHookV1) != wantPrepare || + strings.Count(resume.String(), "call void @"+coroFaultPayloadHookV1) != wantPayload { + t.Fatalf("post-split %s resume lost its nil-fault edge:\n%s", name, module.String()) + } + } + withRecover := module.NamedFunction("foo.WithRecover$coro.resume") + recoverFault := module.NamedFunction("foo.RecoverFault$coro.resume") + if withRecover.IsNil() || recoverFault.IsNil() || + strings.Count(withRecover.String(), "call void @"+coroFaultPayloadHookV1) != 1 || + countCoroIRDirectCalls(withRecover, coroAwaitPrepareHookV1) != 1 || + countCoroIRDirectCalls(recoverFault, coroRecoverTakeHookV1) != 1 { + t.Fatalf("post-split recoverable implicit fault lost its payload/recover transaction:\n%s", module.String()) + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit implicit nil-fault object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroFaultPrepareHookV1)) || + !bytes.Contains(object.Bytes(), []byte(coroFaultPayloadHookV1)) { + t.Fatal("post-CoroSplit object lost a nil-fault hook") + } + }) + } +} + +func TestCoroImplicitIndexAddrBoundsNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroImplicitNilFaultFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + function := functions["SliceAt"] + functionPlan, ok := plan.FunctionPlan(function) + if !ok || functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("SliceAt plan = %+v, present=%t; want may-unwind coroutine", functionPlan, ok) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify structured IndexAddr before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.SliceAt").String() + if got := strings.Count(body, "call void @"+coroFaultPrepareHookV1); got != 1 { + t.Fatalf("SliceAt fault prepare calls = %d, want one:\n%s", got, body) + } + if strings.Contains(body, "CheckIndexRange") || strings.Contains(body, "AssertIndexRange") { + t.Fatalf("SliceAt retained a native-stack bounds helper:\n%s", body) + } + hook := strings.Index(body, "call void @"+coroFaultPrepareHookV1) + if hook < 0 || !strings.Contains(body[hook:], "i32 2") { + t.Fatalf("SliceAt did not select the index-bounds fault kind:\n%s", body) + } + gep := strings.Index(body, "getelementptr inbounds i32") + if gep < 0 || hook > gep { + t.Fatalf("SliceAt formed its element address before the terminal bounds edge:\n%s", body) + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.SliceAt$coro.resume") + if resume.IsNil() || strings.Count(resume.String(), "call void @"+coroFaultPrepareHookV1) != 1 { + t.Fatalf("post-split SliceAt resume lost its bounds-fault edge:\n%s", module.String()) + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit structured IndexAddr object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroFaultPrepareHookV1)) { + t.Fatal("post-CoroSplit object lost the bounds-fault hook") + } + }) + } +} + +func TestCoroPurePointerEqualityNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroImplicitNilFaultFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + function := functions["PointerEqual"] + functionPlan, ok := plan.FunctionPlan(function) + if !ok || functionPlan.Emission != coro.EmitCoroutine { + t.Fatalf("PointerEqual plan = %+v, present=%t; want coroutine", functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo.PointerEqual").String() + if !strings.Contains(body, "icmp eq ptr") || strings.Contains(body, coroFaultPrepareHookV1) { + t.Fatalf("pointer equality did not remain one direct non-faulting comparison:\n%s", body) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify pointer equality before CoroSplit: %v\n%s", err, module.String()) + } + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.PointerEqual$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), "icmp eq ptr") { + t.Fatalf("post-split pointer equality lost its direct comparison:\n%s", module.String()) + } + }) + } +} + +func TestCoroImplicitNilFieldAddrProofSeparatesRootFromAccess(t *testing.T) { + prog, _, _, root, audit, proof := prepareCoroFrameRootAudit(t, `package foo +type Box struct { Value uint32 } +func (box *Box) Root() uint32 { return box.Value } +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + + var field *ssa.FieldAddr + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.FieldAddr); ok { + field = candidate + } + } + } + if field == nil { + t.Fatal("fixture has no FieldAddr") + } + if !proof.provesGuardableStableAddress(field, field) || proof.provesDominatedStableAddress(field, field) { + t.Fatal("nullable FieldAddr did not retain separate transport/nonnull facts") + } + if roots := rootNames(proof.exactRetainedRoots()); len(roots) != 1 || roots[0] != "box" { + t.Fatalf("nullable receiver is not the sole exact retained root: %v", roots) + } + if len(root.Params) != 1 || proof.exactRoots[root.Params[0]].kind != coroFrameRetentionRootReceiver { + t.Fatalf("nullable method parameter was not classified as the receiver root: %+v", proof.exactRoots) + } + if reason := audit.validateFieldAddr(field); !strings.Contains(reason, "non-nil") { + t.Fatalf("legacy audit accepted nullable FieldAddr or changed fail-closed reason: %q", reason) + } + audit.allowImplicitNilFault = true + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + t.Fatalf("explicit-status instruction %T %q rejected: %s", instruction, instruction, reason) + } + } + } +} + +func compileCoroImplicitNilFaultFixture( + t *testing.T, + target *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, map[string]*ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroImplicitNilFaultFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := map[string]*ssa.Function{ + "Nullable": ssaPkg.Func("Nullable"), + "EmptyLoad": ssaPkg.Func("EmptyLoad"), + "Guarded": ssaPkg.Func("Guarded"), + "WithCleanup": ssaPkg.Func("WithCleanup"), + "RecoverFault": ssaPkg.Func("RecoverFault"), + "WithRecover": ssaPkg.Func("WithRecover"), + "StringAt": ssaPkg.Func("StringAt"), + "ConstantStringAt": ssaPkg.Func("ConstantStringAt"), + "ArrayAt": ssaPkg.Func("ArrayAt"), + "SliceAt": ssaPkg.Func("SliceAt"), + "PointerEqual": ssaPkg.Func("PointerEqual"), + "ValueReceiverCall": ssaPkg.Func("ValueReceiverCall"), + } + roots := make(coro.Roots, 0, len(functions)) + for _, function := range functions { + roots = append(roots, coro.Root{Function: function, Demand: coro.AsyncDemand}) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + for _, root := range functions { + if function == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, functions +} diff --git a/cl/coro_index_fault_test.go b/cl/coro_index_fault_test.go new file mode 100644 index 0000000000..af74df7d1c --- /dev/null +++ b/cl/coro_index_fault_test.go @@ -0,0 +1,117 @@ +//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 ( + "bytes" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroImplicitIndexBoundsNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, target := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(target.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroImplicitNilFaultFixture(t, target.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify structured Index before CoroSplit: %v\n%s", err, module.String()) + } + for _, operation := range []struct { + name string + elementGEP string + }{ + {name: "StringAt", elementGEP: "getelementptr inbounds i8"}, + {name: "ConstantStringAt", elementGEP: "getelementptr inbounds i8"}, + {name: "ArrayAt", elementGEP: "getelementptr inbounds i32"}, + } { + function := functions[operation.name] + if !coroFunctionHasSSAIndex(function) { + t.Fatalf("%s fixture no longer exercises ssa.Index", operation.name) + } + functionPlan, ok := plan.FunctionPlan(function) + if !ok || functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want may-unwind coroutine", operation.name, functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo."+operation.name).String() + if got := strings.Count(body, "call void @"+coroFaultPrepareHookV1); got != 1 { + t.Fatalf("%s fault prepare calls = %d, want one:\n%s", operation.name, got, body) + } + if strings.Contains(body, "CheckIndexRange") || strings.Contains(body, "AssertNilDeref") { + t.Fatalf("%s retained a native-stack index helper:\n%s", operation.name, body) + } + hook := strings.Index(body, "call void @"+coroFaultPrepareHookV1) + hookLine := body[hook:] + if end := strings.IndexByte(hookLine, '\n'); end >= 0 { + hookLine = hookLine[:end] + } + if !strings.Contains(hookLine, "i32 2") { + t.Fatalf("%s did not select the index-bounds fault kind:\n%s", operation.name, body) + } + if !strings.Contains(body[hook:], operation.elementGEP) { + t.Fatalf("%s formed no element address after its terminal bounds edge:\n%s", operation.name, body) + } + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{"StringAt", "ConstantStringAt", "ArrayAt"} { + resume := module.NamedFunction("foo." + name + "$coro.resume") + if resume.IsNil() || strings.Count(resume.String(), "call void @"+coroFaultPrepareHookV1) != 1 { + t.Fatalf("post-split %s resume lost its bounds-fault edge:\n%s", name, module.String()) + } + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit structured Index object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroFaultPrepareHookV1)) { + t.Fatal("post-CoroSplit object lost the bounds-fault hook") + } + }) + } +} + +func coroFunctionHasSSAIndex(function *ssa.Function) bool { + if function == nil { + return false + } + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + if _, ok := instruction.(*ssa.Index); ok { + return true + } + } + } + return false +} diff --git a/cl/coro_interface_await.go b/cl/coro_interface_await.go new file mode 100644 index 0000000000..409624e3bc --- /dev/null +++ b/cl/coro_interface_await.go @@ -0,0 +1,153 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +func coroInterfaceDispatchNeedsAwait(dispatch *coroInterfaceDispatchPlan) bool { + if dispatch == nil { + return false + } + for _, candidate := range dispatch.candidates { + if candidate.plan.Emission == coro.EmitCoroutine { + return true + } + } + return false +} + +// compileCoroInterfaceDispatchAwait lowers a closed interface invoke into +// one receiver-aware dispatch chain. The ordinary itab method word is used +// only as the exact target discriminator: an async itab slot currently names +// a $coro root whose physical signature cannot be called as a legacy method. +// Each selected target is therefore invoked through its planned primary entry; +// coroutine candidates use the same structured child-await transaction as a +// static synchronous-style Go call, while plain candidates remain direct. +// +// This is the closed-world bridge to the canonical {descriptor,env} ABI. Once +// itab emission stores that descriptor directly, the candidate chain reduces +// to one validated descriptor entry load without changing scheduler semantics. +func (p *context) compileCoroInterfaceDispatchAwait( + b llssa.Builder, call *ssa.Call, instructionPlan coroPhysicalInstructionPlan, +) llssa.Expr { + if !p.hasCoroPhysicalBody() || call == nil || call.Common() == nil || !call.Common().IsInvoke() || + instructionPlan.control != coroPhysicalControlClosedInterfaceAwait { + panic("coroutine interface dispatch escaped its frozen physical control recipe") + } + dispatch := instructionPlan.controlInterface + if dispatch == nil || !coroInterfaceDispatchNeedsAwait(dispatch) { + panic("coroutine interface dispatch has an incomplete frozen physical control recipe") + } + p.recordCallerLocationForCall(b, &call.Call) + p.emitPCLineLabel(b, call.Pos()) + // Preserve source evaluation order and the existing nil-interface check. + intf := p.compileValue(b, dispatch.receiver) + methodValue := b.Imethod(intf, dispatch.method) + methodWord := b.Convert(p.prog.VoidPtr(), b.Field(methodValue, 0)) + env := b.Field(methodValue, 1) + args := p.compileValues(b, call.Call.Args, fnNormal) + keepaliveSlots := p.compileCoroCallKeepaliveSlots(b, call) + + resultCount := dispatch.sourceCallSignature.Results().Len() + var resultSlot llssa.Expr + if resultCount != 0 { + resultSlot = p.coroFrameAlloca(p.type_(call.Type(), llssa.InGo)) + } + join := p.fn.MakeBlock() + next := p.fn.MakeBlock() + b.Jump(next) + for _, candidate := range dispatch.candidates { + b.SetBlockEx(next, llssa.AtEnd, false) + selected := p.fn.MakeBlock() + next = p.fn.MakeBlock() + methodEntry, _, methodKind := p.compileFunction(candidate.methodEntry) + if methodKind != goFunc || methodEntry == nil { + panic(fmt.Sprintf("coroutine interface dispatch: target %q has no exact itab method entry", candidate.id)) + } + entry, _, kind := p.compileFunction(candidate.function) + if kind != goFunc || entry == nil { + panic(fmt.Sprintf("coroutine interface dispatch: target %q has no Go primary entry", candidate.id)) + } + entryWord := b.Convert(p.prog.VoidPtr(), methodEntry.Expr) + b.If(b.BinOp(token.EQL, methodWord, entryWord), selected, next) + + b.SetBlockEx(selected, llssa.AtEnd, false) + receiverType := p.type_(candidate.receiver, llssa.InGo) + var dynamicReceiver llssa.Expr + if _, pointer := types.Unalias(candidate.receiver).Underlying().(*types.Pointer); pointer { + dynamicReceiver = b.Convert(receiverType, env) + } else { + receiverAddress := b.Convert(p.prog.Pointer(receiverType), env) + p.compileCoroImplicitNilAccessGuard(b, receiverAddress) + dynamicReceiver = b.LoadKnownNonNil(receiverAddress) + } + receiver := dynamicReceiver + if !types.Identical(candidate.receiver, candidate.targetReceiver) { + pointer, ok := types.Unalias(candidate.receiver).Underlying().(*types.Pointer) + if !ok || !types.Identical(pointer.Elem(), candidate.targetReceiver) { + panic(fmt.Sprintf( + "coroutine interface dispatch: target %q cannot adapt dynamic receiver %s to declared receiver %s", + candidate.id, candidate.receiver, candidate.targetReceiver, + )) + } + p.compileCoroImplicitNilAccessGuard(b, dynamicReceiver) + receiver = b.LoadKnownNonNil(dynamicReceiver) + } + physical := make([]llssa.Expr, 0, len(args)+1) + physical = append(physical, receiver) + physical = append(physical, args...) + var result llssa.Expr + switch candidate.plan.Emission { + case coro.EmitCoroutine: + result = p.compileCoroTargetAwaitWithKeepalive(b, candidate.function, physical, keepaliveSlots) + case coro.EmitPlain: + result = b.Call(entry.Expr, physical...) + default: + panic(fmt.Sprintf("coroutine interface dispatch: target %q has emission %s", candidate.id, candidate.plan.Emission)) + } + if resultCount != 0 { + b.Store(resultSlot, result) + } + b.Jump(join) + } + + // A closed plan and the frozen itab method table must agree exactly. Nil + // interfaces already took the ordinary panic edge in Imethod; any non-nil + // unmatched word is corrupted representation state, not an open fallback. + b.SetBlockEx(next, llssa.AtEnd, false) + trap := p.pkg.NewFunc( + "llvm.trap", + types.NewSignatureType(nil, nil, nil, nil, nil, false), + llssa.InC, + ) + b.Call(trap.Expr) + b.Unreachable() + + b.SetBlockContinuation(join) + if resultCount == 0 { + return llssa.Nil + } + return b.LoadKnownNonNil(resultSlot) +} diff --git a/cl/coro_interface_dispatch.go b/cl/coro_interface_dispatch.go new file mode 100644 index 0000000000..9f1c9cbf8d --- /dev/null +++ b/cl/coro_interface_dispatch.go @@ -0,0 +1,389 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + "sort" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// coroInterfaceDispatchPlan is the immutable source-level proof required by +// receiver-aware interface dispatch. It deliberately contains no LLVM or +// scheduler state. A frontend consumer patches sourceCallSignature exactly +// once, then selects the ordinary or coroutine physical entry recorded by each +// candidate's FunctionPlan. +// +// mayBeNil preserves the ordinary Go nil-interface panic check. It is not an +// unresolved-target marker: every accepted candidate set is closed and +// nonempty. +type coroInterfaceDispatchPlan struct { + call *ssa.Call + receiver ssa.Value + iface *types.Interface + method *types.Func + sourceCallSignature *types.Signature + mayBeNil bool + candidates []coroInterfaceDispatchCandidate +} + +type coroInterfaceDispatchCandidate struct { + id coro.FunctionID + function *ssa.Function + plan coro.FunctionPlan + receiver types.Type + targetReceiver types.Type + methodEntry *ssa.Function +} + +// resolveCoroInterfaceDispatchPlan freezes one ordinary interface invoke for +// frontend code generation. The returned candidates are sorted by FunctionID, +// independent of SSA or map enumeration order. The source call signature is +// receiver-free and shared by every candidate, so target-specific codegen must +// not reconstruct it from a selected method body. +func resolveCoroInterfaceDispatchPlan(plan *coro.SSAPlan, universe *EmissionUniverse, call *ssa.Call) (*coroInterfaceDispatchPlan, error) { + if plan == nil || call == nil || call.Common() == nil { + return nil, fmt.Errorf("coroutine interface dispatch requires an exact call and compilation plan") + } + common := call.Common() + if !common.IsInvoke() || common.StaticCallee() != nil || common.Method == nil { + return nil, fmt.Errorf("coroutine interface dispatch requires an ordinary interface invoke") + } + if call.Parent() == nil { + return nil, fmt.Errorf("coroutine interface dispatch requires an invoke owned by an SSA function") + } + if universe != nil && universe.ownerOf(call.Parent()) == nil { + return nil, fmt.Errorf("coroutine interface dispatch invoke owner is absent from the emission universe") + } + iface, ok := types.Unalias(common.Value.Type()).Underlying().(*types.Interface) + if !ok { + return nil, fmt.Errorf("coroutine interface dispatch receiver type %s is not an interface", common.Value.Type()) + } + iface.Complete() + + callPlan, ok := plan.CallPlan(call) + if !ok || callPlan.Call != call { + return nil, fmt.Errorf("coroutine interface dispatch invoke has no exact compilation CallPlan") + } + if callPlan.Kind != coro.CallDirect || callPlan.Rep != coro.Dispatch || callPlan.Open || len(callPlan.Targets) == 0 { + return nil, fmt.Errorf( + "coroutine interface dispatch requires a closed nonempty Dispatch CallPlan, got kind=%v representation=%s open=%t may-be-nil=%t targets=%d", + callPlan.Kind, callPlan.Rep, callPlan.Open, callPlan.MayBeNil, len(callPlan.Targets), + ) + } + + sourceSignature, err := coroInterfaceDispatchSourceSignature(common) + if err != nil { + return nil, err + } + result := &coroInterfaceDispatchPlan{ + call: call, + receiver: common.Value, + iface: iface, + method: common.Method, + sourceCallSignature: sourceSignature, + mayBeNil: callPlan.MayBeNil, + candidates: make([]coroInterfaceDispatchCandidate, 0, len(callPlan.Targets)), + } + + ids := append([]coro.FunctionID(nil), callPlan.Targets...) + sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] }) + for index, id := range ids { + if index != 0 && ids[index-1] == id { + return nil, fmt.Errorf("coroutine interface dispatch repeats target ID %q", id) + } + target, found := plan.Function(id) + if !found || target == nil { + return nil, fmt.Errorf("coroutine interface dispatch target %q is absent from the compilation plan", id) + } + targetPlan, found := plan.FunctionPlan(target) + if !found || targetPlan.ID != id { + return nil, fmt.Errorf("coroutine interface dispatch target %q has no exact function plan", id) + } + receiver, targetReceiver, methodEntry, err := validateCoroInterfaceDispatchCandidate( + common, iface, sourceSignature, universe, call.Parent(), id, target, targetPlan, + ) + if err != nil { + return nil, err + } + result.candidates = append(result.candidates, coroInterfaceDispatchCandidate{ + id: id, + function: target, + plan: targetPlan, + receiver: receiver, + targetReceiver: targetReceiver, + methodEntry: methodEntry, + }) + } + return result, nil +} + +func coroInterfaceDispatchSourceSignature(common *ssa.CallCommon) (*types.Signature, error) { + if common == nil || common.Method == nil { + return nil, fmt.Errorf("coroutine interface dispatch requires an exact invoke method") + } + signature := coroInterfaceDispatchCallableSignature(common.Signature()) + methodSignature, _ := common.Method.Type().(*types.Signature) + methodSignature = coroInterfaceDispatchCallableSignature(methodSignature) + if signature == nil || methodSignature == nil || !types.Identical(signature, methodSignature) { + return nil, fmt.Errorf("coroutine interface dispatch call signature %v does not match method signature %v", signature, methodSignature) + } + if signature.Variadic() { + return nil, fmt.Errorf("coroutine interface dispatch variadic method %q is not implemented", common.Method.Id()) + } + if list := signature.TypeParams(); list != nil && list.Len() != 0 { + return nil, fmt.Errorf("coroutine interface dispatch generic call signature is not materialized") + } + if len(common.Args) != signature.Params().Len() { + return nil, fmt.Errorf("coroutine interface dispatch has %d arguments for %d source parameters", len(common.Args), signature.Params().Len()) + } + for index, argument := range common.Args { + if argument == nil || !types.Identical(argument.Type(), signature.Params().At(index).Type()) { + return nil, fmt.Errorf("coroutine interface dispatch argument %d does not match source parameter type %s", index, signature.Params().At(index).Type()) + } + } + return coroInterfaceDispatchCanonicalSignature(signature), nil +} + +func validateCoroInterfaceDispatchCandidate( + common *ssa.CallCommon, + iface *types.Interface, + sourceSignature *types.Signature, + universe *EmissionUniverse, + caller *ssa.Function, + id coro.FunctionID, + target *ssa.Function, + plan coro.FunctionPlan, +) (types.Type, types.Type, *ssa.Function, error) { + fail := func(format string, args ...any) (types.Type, types.Type, *ssa.Function, error) { + return nil, nil, nil, fmt.Errorf("coroutine interface dispatch target %q: %s", id, fmt.Sprintf(format, args...)) + } + if common == nil || common.Method == nil || iface == nil || sourceSignature == nil || target == nil || target.Signature == nil { + return fail("missing method, receiver interface, source signature, or target signature") + } + if plan.ID != id { + return fail("function plan ID is %q", plan.ID) + } + if plan.External != coro.Defined || plan.FuncRep != coro.Dispatch { + return fail("requires a defined Dispatch body, got external=%s representation=%s", plan.External, plan.FuncRep) + } + switch { + case plan.Emission == coro.EmitPlain && plan.Primary == coro.PrimaryPlain: + if plan.Effect != coro.NoSuspend || plan.Effect.IsOpaque() { + return fail("plain candidate effect %s is not exact no-suspend", plan.Effect) + } + if plan.Demand == coro.NoDemand { + return fail("plain candidate is not demanded") + } + if plan.Exec.Contains(coro.NeedsPreempt) || plan.Exec.IsOpaque() { + return fail("plain candidate execution constraints %s require coroutine or open lowering", plan.Exec) + } + case plan.Emission == coro.EmitCoroutine && plan.Primary == coro.PrimaryCoroutine: + // A RawPlainEntry is an alternate physical entry for exact raw ABI + // consumers. BothDemand therefore still has a managed coroutine + // primary, which is the only entry an ordinary interface invoke may + // select. + if !plan.Demand.Contains(coro.AsyncDemand) { + return fail("coroutine candidate demand is %s, want managed async", plan.Demand) + } + if !plan.Effect.MaySuspend() || plan.Effect.IsOpaque() { + return fail("coroutine candidate effect %s is not an exact suspend effect", plan.Effect) + } + if plan.Exec.IsOpaque() { + return fail("coroutine candidate execution constraints %s are opaque", plan.Exec) + } + default: + return fail( + "requires either a plain/no-suspend or coroutine/async body, got emission=%s primary=%s demand=%s effect=%s", + plan.Emission, plan.Primary, plan.Demand, plan.Effect, + ) + } + if len(target.Blocks) == 0 { + return fail("requires one defined SSA body") + } + if target.Parent() != nil || len(target.FreeVars) != 0 { + return fail("captured or nested methods require an environment adapter") + } + if target.Signature.Variadic() { + return fail("variadic methods are not implemented") + } + directive, err := coroRawABIDirective(target, universe) + if err != nil { + return fail("classify ABI directive: %v", err) + } + if directive != "" { + return fail("ABI directive %q requires an explicit boundary adapter", directive) + } + if params := target.TypeParams(); params != nil && params.Len() != 0 { + return fail("generic declarations are not materialized method bodies") + } + if params := target.Signature.TypeParams(); params != nil && params.Len() != 0 { + return fail("generic method signatures are not materialized") + } + if params := target.Signature.RecvTypeParams(); params != nil && params.Len() != 0 { + return fail("generic receiver methods are not materialized") + } + if len(target.TypeArgs()) != 0 || target.Origin() != nil { + return fail("generic instances require a frozen instantiated interface ABI") + } + + recv := target.Signature.Recv() + if recv == nil { + return fail("candidate is not a declared method") + } + method, ok := target.Object().(*types.Func) + if !ok || method == nil { + return fail("candidate has no exact method object") + } + if method.Name() != common.Method.Name() || (universe == nil && method.Id() != common.Method.Id()) { + return fail("method ID %q does not match invoke method ID %q", method.Id(), common.Method.Id()) + } + targetReceiver := recv.Type() + dynamicReceiver := targetReceiver + implements, implementsErr := coroInterfaceDispatchCandidateImplements(universe, dynamicReceiver, iface) + if implementsErr != nil { + return fail("prove receiver %s implements invoke interface %s: %v", dynamicReceiver, iface, implementsErr) + } + if !implements { + if _, pointer := types.Unalias(dynamicReceiver).Underlying().(*types.Pointer); pointer { + return fail("receiver %s does not implement invoke interface %s", dynamicReceiver, iface) + } + promoted := types.NewPointer(dynamicReceiver) + promotedImplements, promotedErr := coroInterfaceDispatchCandidateImplements(universe, promoted, iface) + if promotedErr != nil { + return fail("prove promoted receiver %s implements invoke interface %s: %v", promoted, iface, promotedErr) + } + if !promotedImplements { + return fail("receiver %s and promoted receiver %s do not implement invoke interface %s", dynamicReceiver, promoted, iface) + } + dynamicReceiver = promoted + } + selection := types.NewMethodSet(dynamicReceiver).Lookup(method.Pkg(), method.Name()) + if selection == nil { + return fail("dynamic receiver method set has no method %q", common.Method.Id()) + } + selectedMethod, ok := selection.Obj().(*types.Func) + if !ok || selectedMethod == nil || selectedMethod.Id() != method.Id() || + (universe == nil && selectedMethod.Id() != common.Method.Id()) { + return fail("receiver method selection does not resolve exact method ID %q", method.Id()) + } + methodEntry := target.Prog.MethodValue(selection) + if methodEntry == nil || methodEntry.Prog != target.Prog || methodEntry.Signature == nil || len(methodEntry.FreeVars) != 0 { + return fail("dynamic receiver method selection has no exact non-capturing SSA entry") + } + entryReceiver := methodEntry.Signature.Recv() + if entryReceiver == nil || !types.Identical(entryReceiver.Type(), dynamicReceiver) { + return fail("method entry receiver %v does not match dynamic receiver %s", entryReceiver, dynamicReceiver) + } + entrySignature, err := coroInterfaceDispatchEffectiveCallableSignature(universe, methodEntry, methodEntry.Signature) + if err != nil { + return fail("derive effective method-entry signature: %v", err) + } + effectiveSourceSignature, err := coroInterfaceDispatchEffectiveCallableSignature(universe, caller, sourceSignature) + if err != nil { + return fail("derive effective source call signature: %v", err) + } + if entrySignature == nil || !coroInterfaceDispatchSignaturesIdentical(effectiveSourceSignature, entrySignature) { + return fail("effective method entry signature %v does not match source call signature %v", entrySignature, effectiveSourceSignature) + } + + targetSignature, err := coroInterfaceDispatchEffectiveCallableSignature(universe, target, target.Signature) + if err != nil { + return fail("derive effective target signature: %v", err) + } + if targetSignature == nil || !coroInterfaceDispatchSignaturesIdentical(effectiveSourceSignature, coroInterfaceDispatchCanonicalSignature(targetSignature)) { + return fail("effective source call signature %v does not match receiver-free target signature %v", effectiveSourceSignature, targetSignature) + } + if len(target.Params) != target.Signature.Params().Len()+1 || target.Params[0] == nil || !types.Identical(target.Params[0].Type(), recv.Type()) { + return fail("SSA parameters do not contain the exact declared receiver") + } + for index := 0; index < target.Signature.Params().Len(); index++ { + parameter := target.Params[index+1] + if parameter == nil || !types.Identical(parameter.Type(), target.Signature.Params().At(index).Type()) { + return fail("SSA parameter %d does not match declared method parameter %d", index+1, index) + } + } + return dynamicReceiver, targetReceiver, methodEntry, nil +} + +func coroInterfaceDispatchCandidateImplements( + universe *EmissionUniverse, + candidate types.Type, + iface *types.Interface, +) (bool, error) { + if universe != nil { + return universe.CoroDynamicImplements(candidate, iface) + } + return types.Implements(candidate, iface), nil +} + +func coroInterfaceDispatchEffectiveCallableSignature( + universe *EmissionUniverse, + caller *ssa.Function, + typ types.Type, +) (*types.Signature, error) { + if typ == nil { + return nil, nil + } + if universe != nil { + if caller == nil { + return nil, fmt.Errorf("effective interface signature requires an SSA owner") + } + owner := universe.ownerOf(caller) + if owner == nil { + return nil, fmt.Errorf("function %q is absent from the emission universe", caller.Name()) + } + typ = universe.effectiveType(owner, caller, typ) + } + signature, _ := types.Unalias(typ).(*types.Signature) + return coroInterfaceDispatchCanonicalSignature(coroInterfaceDispatchCallableSignature(signature)), nil +} + +func coroInterfaceDispatchSignaturesIdentical(left, right *types.Signature) bool { + if left == nil || right == nil { + return left == right + } + return structuralEmissionABITypeKey(left) == structuralEmissionABITypeKey(right) +} + +func coroInterfaceDispatchCallableSignature(signature *types.Signature) *types.Signature { + if signature == nil { + return nil + } + return types.NewSignatureType(nil, nil, nil, signature.Params(), signature.Results(), signature.Variadic()) +} + +// coroInterfaceDispatchCanonicalSignature removes source variable names while +// retaining the exact source types that the frontend must patch. This makes a +// single signature safe to share across candidates from different packages. +func coroInterfaceDispatchCanonicalSignature(signature *types.Signature) *types.Signature { + if signature == nil { + return nil + } + canonicalTuple := func(tuple *types.Tuple) *types.Tuple { + variables := make([]*types.Var, tuple.Len()) + for index := range variables { + variables[index] = types.NewVar(token.NoPos, nil, "", tuple.At(index).Type()) + } + return types.NewTuple(variables...) + } + return types.NewSignatureType(nil, nil, nil, canonicalTuple(signature.Params()), canonicalTuple(signature.Results()), signature.Variadic()) +} diff --git a/cl/coro_interface_dispatch_test.go b/cl/coro_interface_dispatch_test.go new file mode 100644 index 0000000000..10377c4ae4 --- /dev/null +++ b/cl/coro_interface_dispatch_test.go @@ -0,0 +1,711 @@ +//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" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroUniqueAsyncWriterSource = `package foo + +var gate chan struct{} + +type Writer interface { Write([]byte) (int, error) } +type AsyncWriter struct{} + +func (*AsyncWriter) Write(buffer []byte) (int, error) { + <-gate + return len(buffer), nil +} + +func Root(writer Writer) (int, error) { + return writer.Write([]byte("payload")) +} +` + +func TestResolveCoroInterfaceDispatchPlanUniqueAsyncWriter(t *testing.T) { + fixture := buildCoroInterfaceDispatchFixture(t, coroUniqueAsyncWriterSource, coro.DynamicCHAClosed) + defer fixture.program.Dispose() + + resolved, err := resolveCoroInterfaceDispatchPlan(fixture.plan, nil, fixture.invoke) + if err != nil { + t.Fatal(err) + } + if resolved.call != fixture.invoke || resolved.receiver != fixture.invoke.Common().Value || resolved.method.Id() != "Write" { + t.Fatalf("resolved call facts do not preserve the exact invoke: %+v", resolved) + } + if !resolved.mayBeNil { + t.Fatal("interface invoke lost its required nil-interface panic check") + } + if resolved.sourceCallSignature == nil || resolved.sourceCallSignature.Recv() != nil || resolved.sourceCallSignature.Variadic() || + resolved.sourceCallSignature.Params().Len() != 1 || resolved.sourceCallSignature.Results().Len() != 2 { + t.Fatalf("source call signature = %v", resolved.sourceCallSignature) + } + if len(resolved.candidates) != 1 { + t.Fatalf("candidates = %d, want one: %+v", len(resolved.candidates), resolved.candidates) + } + candidate := resolved.candidates[0] + if candidate.function == nil || candidate.function.Name() != "Write" || candidate.plan.ID != candidate.id || + candidate.plan.External != coro.Defined || candidate.plan.Emission != coro.EmitCoroutine || + candidate.plan.Primary != coro.PrimaryCoroutine || candidate.plan.Demand != coro.AsyncDemand || + candidate.plan.FuncRep != coro.Dispatch || !candidate.plan.Effect.MaySuspend() { + t.Fatalf("async Writer.Write candidate = %+v", candidate) + } + + again, err := resolveCoroInterfaceDispatchPlan(fixture.plan, nil, fixture.invoke) + if err != nil { + t.Fatal(err) + } + if len(again.candidates) != 1 || again.candidates[0].id != candidate.id || again.candidates[0].function != candidate.function || + !types.Identical(again.sourceCallSignature, resolved.sourceCallSignature) { + t.Fatalf("repeated resolution is not stable: first=%+v again=%+v", resolved, again) + } +} + +func TestCoroManagedOpenAnonymousInterfaceUsesUniversalMethodDescriptor(t *testing.T) { + const source = `package foo +var gate chan struct{} +type plainMatcher struct{} +type asyncMatcher struct{} +type promotedBase struct{} +type deadPromotedMatcher struct{ promotedBase } +func (plainMatcher) As(any) bool { return true } +func (*asyncMatcher) As(any) bool { <-gate; return true } +func (promotedBase) As(any) bool { return true } +func keep(flag bool) interface{ As(any) bool } { + if flag { return plainMatcher{} } + return &asyncMatcher{} +} +func Root(value interface{ As(any) bool }, target any, flag bool) bool { + if flag { + _, _ = target.(*plainMatcher) + _, _ = target.(*asyncMatcher) + } + return value.As(target) +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + program := newLLSSAProg(t) + defer program.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + program, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + invoke := coroInterfaceDispatchFindInvoke(t, root) + methodTargets := make(map[*ssa.Function]struct{}) + for _, function := range universe.Functions() { + if function != nil && function.Name() == "As" && function.Signature != nil && function.Signature.Recv() != nil { + methodTargets[function] = struct{}{} + } + } + if len(methodTargets) < 2 { + t.Fatalf("managed interface fixture has %d As method entries, want at least two", len(methodTargets)) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + DynamicResolution: coro.DynamicCHAOpen, + MaxPlainInstructions: -1, + ClassifyUnknownCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.UnknownTarget, error) { + if call == invoke { + return coro.UnknownManagedInterfaceDispatch, nil + } + return coro.UnknownManaged, nil + }, + }) + if err != nil { + t.Fatal(err) + } + callPlan, ok := plan.CallPlan(invoke) + if !ok || callPlan.Rep != coro.Dispatch || !callPlan.Open || + callPlan.Unresolved != coro.UnknownManagedInterfaceDispatch || len(callPlan.Targets) == 0 { + t.Fatalf("anonymous As invoke CallPlan = %+v, present=%t", callPlan, ok) + } + var deadPromoted *ssa.Function + for target := range methodTargets { + if strings.Contains(target.Synthetic, "wrapper") && strings.Contains(target.String(), "deadPromotedMatcher") { + deadPromoted = target + break + } + } + if deadPromoted == nil { + t.Fatal("managed interface fixture has no dead promoted method wrapper") + } + deadPlan, ok := plan.FunctionPlan(deadPromoted) + if !ok || !coroInterfaceTargetContains(callPlan.Targets, deadPlan.ID) { + t.Fatalf("dead promoted target plan = %+v, present=%t; open targets=%v", deadPlan, ok, callPlan.Targets) + } + materializedByTypeData := false + for _, owner := range plan.Functions() { + if owner.Function == nil || owner.Plan.Emission == coro.EmitNone { + continue + } + references, err := universe.CoroDemandReferences(owner.Function) + if err != nil { + t.Fatal(err) + } + for _, target := range references { + materializedByTypeData = materializedByTypeData || target == deadPromoted + } + } + if materializedByTypeData { + t.Fatal("dead promoted wrapper unexpectedly has an ABI type-data owner") + } + managedMethods, err := analyzeCoroManagedInterfaceDispatchPlan(plan, universe, true) + if err != nil { + t.Fatal(err) + } + if !managedMethods.acceptsTarget(deadPromoted, deadPlan) { + t.Fatalf("managed method plan did not freeze exact dead promoted target %q", deadPlan.ID) + } + closedMethods, err := analyzeCoroClosedInterfacePlainPlan(plan, universe, false, true) + if err != nil { + t.Fatal(err) + } + if closedMethods.acceptsTarget(deadPromoted, deadPlan) { + t.Fatal("dead promoted target acquired an unrelated closed/raw method-token capability") + } + if err := validateCoroDynamicDispatchTarget(deadPromoted, deadPlan); err == nil || + !strings.Contains(err.Error(), "methods require receiver-aware dispatch lowering") { + t.Fatalf("receiver-free function-value validator accepted managed method target: %v", err) + } + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.Effect.IsOpaque() || + !rootPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Root plan = %+v, present=%t", rootPlan, ok) + } + + compilation := coroClosedInterfacePlainCompilation(plan, universe) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + program, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile managed anonymous interface invoke: %v", err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed anonymous interface invoke: %v\n%s", err, module.String()) + } + ir := module.String() + if !strings.Contains(ir, coroPlainDispatchDescriptorPrefix+"method.") || + !strings.Contains(ir, coroPlainDispatchThunkPrefix+"method.") || + !strings.Contains(ir, coroCoroDispatchThunkPrefix+"method.") { + t.Fatalf("plain/coroutine method capabilities were not materialized:\n%s", ir) + } + rootIR := requireCoroPhysicalFunction(t, module, "foo.Root").String() + if !strings.Contains(rootIR, "coro.dispatch.version.invalid") || + !strings.Contains(rootIR, "coro.dispatch.flags.unknown") || + !strings.Contains(rootIR, "call void @"+coroAwaitPrepareHookV1) { + t.Fatalf("open interface invoke did not enter validated descriptor child-await lowering:\n%s", rootIR) + } + if strings.Contains(rootIR, "call i1 %") && !strings.Contains(rootIR, "coro.dispatch") { + t.Fatalf("open interface invoke fell back to an unvalidated raw itab call:\n%s", rootIR) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed anonymous interface invoke before split: %v\n%s", err, ir) + } +} + +func TestCoroRawPlainTypeDataPublishesManagedMethodPrimary(t *testing.T) { + const source = `package foo +var gate chan struct{} +type Writer interface { Write() int } +type writer struct{} +func (writer) Write() int { <-gate; return 1 } +func ManagedRoot(value Writer) int { return value.Write() } +func RawRoot() any { return writer{} } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + program := newLLSSAProg(t) + defer program.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + program, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + managedRoot, rawRoot := ssaPkg.Func("ManagedRoot"), ssaPkg.Func("RawRoot") + invoke := coroInterfaceDispatchFindInvoke(t, managedRoot) + var method *ssa.Function + for _, function := range universe.Functions() { + if function != nil && function.Name() == "Write" && function.Signature != nil && function.Signature.Recv() != nil { + method = function + break + } + } + if method == nil { + t.Fatal("writer.Write is absent from the emission universe") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: managedRoot, Demand: coro.AsyncDemand}, + {Function: rawRoot, RawPlainDemand: true}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + DynamicResolution: coro.DynamicCHAClosed, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == rawRoot { + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + callPlan, ok := plan.CallPlan(invoke) + if !ok || callPlan.Rep != coro.Dispatch || len(callPlan.Targets) == 0 { + t.Fatalf("managed invoke plan = %+v, present=%t", callPlan, ok) + } + methodPlan, ok := plan.FunctionPlan(method) + if !ok || methodPlan.Emission != coro.EmitCoroutine || plan.HasRawPlainVariant(method) { + t.Fatalf("writer.Write plan = %+v, present=%t raw-variant=%t", methodPlan, ok, plan.HasRawPlainVariant(method)) + } + + compilation := coroClosedInterfacePlainCompilation(plan, universe) + pkg, _, err := NewPackageExWithEmbedOptions( + program, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile raw type-data producer: %v", err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify raw type-data producer: %v\n%s", err, module.String()) + } + ir := module.String() + if !strings.Contains(ir, coroPlainDispatchDescriptorPrefix+"method.") || + !strings.Contains(ir, "foo.writer.Write$coro") { + t.Fatalf("raw type data did not publish the managed method primary:\n%s", ir) + } + if !strings.Contains(ir, "define linkonce ptr @"+coroCoroDispatchThunkPrefix+"method.") || + !strings.Contains(ir, "define linkonce i64 @"+coroManagedInterfaceRawTrapPrefix) { + t.Fatalf("cross-package method capability helpers are not coalescible:\n%s", ir) + } +} + +func TestValidateCoroManagedInterfaceDescriptorTargetSelectsCoroutinePrimaryWithRawAlternate(t *testing.T) { + fixture := buildCoroInterfaceDispatchFixture(t, coroUniqueAsyncWriterSource, coro.DynamicCHAClosed) + defer fixture.program.Dispose() + resolved, err := resolveCoroInterfaceDispatchPlan(fixture.plan, nil, fixture.invoke) + if err != nil { + t.Fatal(err) + } + candidate := resolved.candidates[0] + candidate.plan.Demand = coro.BothDemand + candidate.plan.RawPlainEntry = true + for _, rep := range []coro.FuncRep{coro.Dispatch, coro.DirectCoro} { + plan := candidate.plan + plan.FuncRep = rep + if err := validateCoroManagedInterfaceDescriptorTarget( + candidate.function, plan, nil, resolved.sourceCallSignature, + ); err == nil || !strings.Contains(err.Error(), "prepared emission universe") { + // The nil universe must remain fail-closed after accepting the managed + // coroutine primary shape. A receiver-aware ABI method descriptor may + // wrap DirectCoro without creating a receiver-free function descriptor + // or a second body. + t.Fatalf("%s BothDemand/raw-alternate descriptor validation stopped at %v", rep, err) + } + } +} + +func TestValidateCoroInterfaceDispatchCandidateAcceptsManagedPrimaryWithRawAlternate(t *testing.T) { + fixture := buildCoroInterfaceDispatchFixture(t, coroUniqueAsyncWriterSource, coro.DynamicCHAClosed) + defer fixture.program.Dispose() + + resolved, err := resolveCoroInterfaceDispatchPlan(fixture.plan, nil, fixture.invoke) + if err != nil { + t.Fatal(err) + } + if len(resolved.candidates) != 1 { + t.Fatalf("candidates = %d, want one", len(resolved.candidates)) + } + candidate := resolved.candidates[0] + candidate.plan.Demand = coro.BothDemand + candidate.plan.RawPlainEntry = true + receiver, targetReceiver, methodEntry, err := validateCoroInterfaceDispatchCandidate( + fixture.invoke.Common(), resolved.iface, resolved.sourceCallSignature, nil, + fixture.invoke.Parent(), candidate.id, candidate.function, candidate.plan, + ) + if err != nil { + t.Fatalf("BothDemand managed interface candidate rejected: %v", err) + } + if !types.Identical(receiver, candidate.receiver) || !types.Identical(targetReceiver, candidate.targetReceiver) || methodEntry != candidate.methodEntry { + t.Fatalf("validated candidate changed: receiver=%s target=%s entry=%v", receiver, targetReceiver, methodEntry) + } +} + +func TestResolveCoroInterfaceDispatchPlanMixedPlainAndCoroutine(t *testing.T) { + const source = `package foo +var gate chan struct{} +type Writer interface { Write([]byte) (int, error) } +type AsyncWriter struct{} +type PlainWriter struct{} +func (*AsyncWriter) Write(buffer []byte) (int, error) { <-gate; return len(buffer), nil } +func (*PlainWriter) Write(buffer []byte) (int, error) { return len(buffer), nil } +func KeepBoth(flag bool) Writer { + if flag { return &AsyncWriter{} } + return &PlainWriter{} +} +func Root(writer Writer) (int, error) { return writer.Write([]byte("payload")) } +` + fixture := buildCoroInterfaceDispatchFixture(t, source, coro.DynamicCHAClosed) + defer fixture.program.Dispose() + + resolved, err := resolveCoroInterfaceDispatchPlan(fixture.plan, nil, fixture.invoke) + if err != nil { + t.Fatal(err) + } + if len(resolved.candidates) != 2 { + t.Fatalf("candidates = %d, want mixed pair: %+v", len(resolved.candidates), resolved.candidates) + } + plain, asynchronous := 0, 0 + for index, candidate := range resolved.candidates { + if index != 0 && resolved.candidates[index-1].id >= candidate.id { + t.Fatalf("candidates are not in strict FunctionID order: %+v", resolved.candidates) + } + switch candidate.plan.Emission { + case coro.EmitPlain: + plain++ + if candidate.plan.Primary != coro.PrimaryPlain || candidate.plan.Effect != coro.NoSuspend { + t.Fatalf("plain candidate = %+v", candidate) + } + case coro.EmitCoroutine: + asynchronous++ + if candidate.plan.Primary != coro.PrimaryCoroutine || candidate.plan.Demand != coro.AsyncDemand || !candidate.plan.Effect.MaySuspend() { + t.Fatalf("coroutine candidate = %+v", candidate) + } + default: + t.Fatalf("unexpected candidate emission: %+v", candidate) + } + } + if plain != 1 || asynchronous != 1 { + t.Fatalf("candidate classes: plain=%d coroutine=%d", plain, asynchronous) + } +} + +func TestResolveCoroInterfaceDispatchPlanPointerPromotedMethodEntry(t *testing.T) { + const source = `package foo +var gate chan struct{} +type Writer interface { + Write([]byte) (int, error) + Close() error +} +type PointerOnlyWriter struct{} +func (PointerOnlyWriter) Write(buffer []byte) (int, error) { <-gate; return len(buffer), nil } +func (*PointerOnlyWriter) Close() error { return nil } +func Keep() Writer { return &PointerOnlyWriter{} } +func Root(writer Writer) (int, error) { return writer.Write([]byte("payload")) } +` + fixture := buildCoroPointerPromotedInterfaceDispatchFixture(t, source) + defer fixture.program.Dispose() + + resolved, err := resolveCoroInterfaceDispatchPlan(fixture.plan, nil, fixture.invoke) + if err != nil { + t.Fatal(err) + } + if len(resolved.candidates) != 1 { + t.Fatalf("candidates = %d, want one pointer-promoted method: %+v", len(resolved.candidates), resolved.candidates) + } + candidate := resolved.candidates[0] + dynamicPointer, dynamicIsPointer := types.Unalias(candidate.receiver).Underlying().(*types.Pointer) + if !dynamicIsPointer || !types.Identical(dynamicPointer.Elem(), candidate.targetReceiver) { + t.Fatalf("dynamic receiver %s does not promote declared receiver %s", candidate.receiver, candidate.targetReceiver) + } + if candidate.methodEntry == nil || candidate.methodEntry == candidate.function || candidate.methodEntry.Signature == nil || + candidate.methodEntry.Signature.Recv() == nil || !types.Identical(candidate.methodEntry.Signature.Recv().Type(), candidate.receiver) { + t.Fatalf("pointer-promoted method entry = %v; target=%v dynamic receiver=%s", candidate.methodEntry, candidate.function, candidate.receiver) + } + if !strings.Contains(candidate.methodEntry.Synthetic, "wrapper") { + t.Fatalf("method entry %s is not the exact pointer method-set wrapper: synthetic=%q", candidate.methodEntry, candidate.methodEntry.Synthetic) + } +} + +func TestResolveCoroInterfaceDispatchPlanFailsClosed(t *testing.T) { + closed := buildCoroInterfaceDispatchFixture(t, coroUniqueAsyncWriterSource, coro.DynamicCHAClosed) + defer closed.program.Dispose() + open := buildCoroInterfaceDispatchFixture(t, coroUniqueAsyncWriterSource, coro.DynamicCHAOpen) + defer open.program.Dispose() + other := buildCoroInterfaceDispatchFixture(t, coroUniqueAsyncWriterSource, coro.DynamicCHAClosed) + defer other.program.Dispose() + + tests := []struct { + name string + plan *coro.SSAPlan + call *ssa.Call + want string + }{ + {name: "nil plan", call: closed.invoke, want: "exact call and compilation plan"}, + {name: "nil call", plan: closed.plan, want: "exact call and compilation plan"}, + {name: "open", plan: open.plan, call: open.invoke, want: "closed nonempty Dispatch CallPlan"}, + {name: "missing exact call plan", plan: closed.plan, call: other.invoke, want: "no exact compilation CallPlan"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + _, err := resolveCoroInterfaceDispatchPlan(test.plan, nil, test.call) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("error = %v, want substring %q", err, test.want) + } + }) + } + + resolved, err := resolveCoroInterfaceDispatchPlan(closed.plan, nil, closed.invoke) + if err != nil { + t.Fatal(err) + } + target := resolved.candidates[0].function + original := target.Signature + recv := original.Recv() + badParam := types.NewVar(0, target.Pkg.Pkg, "buffer", types.Typ[types.Int]) + target.Signature = types.NewSignatureType(recv, nil, nil, types.NewTuple(badParam), original.Results(), false) + _, err = resolveCoroInterfaceDispatchPlan(closed.plan, nil, closed.invoke) + target.Signature = original + if err == nil || !strings.Contains(err.Error(), "does not match") { + t.Fatalf("signature conflict error = %v", err) + } + + originalFreeVars := target.FreeVars + target.FreeVars = []*ssa.FreeVar{nil} + _, err = resolveCoroInterfaceDispatchPlan(closed.plan, nil, closed.invoke) + target.FreeVars = originalFreeVars + if err == nil || !strings.Contains(err.Error(), "captured or nested methods") { + t.Fatalf("free-variable error = %v", err) + } + + constraint := types.NewInterfaceType(nil, nil) + constraint.Complete() + typeParam := types.NewTypeParam(types.NewTypeName(0, target.Pkg.Pkg, "T", nil), constraint) + named := types.NewNamed(types.NewTypeName(0, target.Pkg.Pkg, "GenericReceiver", nil), types.NewStruct(nil, nil), nil) + named.SetTypeParams([]*types.TypeParam{typeParam}) + receiverTypeParam := types.NewTypeParam(types.NewTypeName(0, target.Pkg.Pkg, "T", nil), constraint) + instantiated, instantiateErr := types.Instantiate(nil, named, []types.Type{receiverTypeParam}, false) + if instantiateErr != nil { + t.Fatal(instantiateErr) + } + genericRecv := types.NewVar(0, target.Pkg.Pkg, "writer", types.NewPointer(instantiated)) + target.Signature = types.NewSignatureType(genericRecv, []*types.TypeParam{receiverTypeParam}, nil, original.Params(), original.Results(), false) + _, err = resolveCoroInterfaceDispatchPlan(closed.plan, nil, closed.invoke) + target.Signature = original + if err == nil || !strings.Contains(err.Error(), "generic") { + t.Fatalf("generic receiver error = %v", err) + } + + badRecv := types.NewVar(0, target.Pkg.Pkg, "writer", types.Typ[types.Int]) + target.Signature = types.NewSignatureType(badRecv, nil, nil, original.Params(), original.Results(), false) + _, err = resolveCoroInterfaceDispatchPlan(closed.plan, nil, closed.invoke) + target.Signature = original + if err == nil || !strings.Contains(err.Error(), "implement invoke interface") { + t.Fatalf("receiver conflict error = %v", err) + } +} + +func TestResolveCoroInterfaceDispatchPlanRejectsVariadicAndABIDirective(t *testing.T) { + tests := []struct { + name string + source string + want string + }{ + { + name: "variadic", + source: `package foo +type Writer interface { Write(...byte) int } +type Concrete struct{} +func (Concrete) Write(buffer ...byte) int { return len(buffer) } +func Root(writer Writer) int { return writer.Write(1, 2) } +`, + want: "variadic method", + }, + { + name: "ABI directive", + source: `package foo +import _ "unsafe" +type Writer interface { Write([]byte) int } +type Concrete struct{} +//go:linkname redirectedWrite example.com/redirectedWrite +func (Concrete) Write(buffer []byte) int { return len(buffer) } +func Root(writer Writer) int { return writer.Write(nil) } +`, + want: "ABI directive", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fixture := buildCoroInterfaceDispatchFixture(t, test.source, coro.DynamicCHAClosed) + defer fixture.program.Dispose() + _, err := resolveCoroInterfaceDispatchPlan(fixture.plan, nil, fixture.invoke) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("error = %v, want substring %q", err, test.want) + } + }) + } +} + +type coroInterfaceDispatchFixture struct { + program llssa.Program + plan *coro.SSAPlan + invoke *ssa.Call +} + +func buildCoroInterfaceDispatchFixture(t *testing.T, source string, resolution coro.DynamicResolution) coroInterfaceDispatchFixture { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + program := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverseWithOptions( + program, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + program.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + program.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + invoke := coroInterfaceDispatchFindInvoke(t, root) + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + DynamicResolution: resolution, + MaxPlainInstructions: -1, + }) + if err != nil { + program.Dispose() + t.Fatal(err) + } + return coroInterfaceDispatchFixture{program: program, plan: plan, invoke: invoke} +} + +func buildCoroPointerPromotedInterfaceDispatchFixture(t *testing.T, source string) coroInterfaceDispatchFixture { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + program := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverseWithOptions( + program, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + program.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + invoke := coroInterfaceDispatchFindInvoke(t, root) + var declared, wrapper *ssa.Function + for _, function := range universe.Functions() { + if function == nil || function.Name() != "Write" || function.Signature == nil || function.Signature.Recv() == nil { + continue + } + _, pointer := types.Unalias(function.Signature.Recv().Type()).Underlying().(*types.Pointer) + switch { + case !pointer && function.Synthetic == "": + declared = function + case pointer && strings.Contains(function.Synthetic, "wrapper"): + wrapper = function + } + } + if declared == nil || wrapper == nil { + program.Dispose() + t.Fatalf("pointer promotion fixture methods: declared=%v wrapper=%v", declared, wrapper) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + FunctionIDs: functionIDs, + DynamicResolution: coro.DynamicCHAClosed, + MaxPlainInstructions: -1, + ResolveFunction: func(function *ssa.Function) (*ssa.Function, bool, error) { + if function == wrapper { + return declared, true, nil + } + return function, true, nil + }, + }) + if err != nil { + program.Dispose() + t.Fatal(err) + } + return coroInterfaceDispatchFixture{program: program, plan: plan, invoke: invoke} +} + +func coroInterfaceDispatchFindInvoke(t *testing.T, function *ssa.Function) *ssa.Call { + t.Helper() + if function == nil { + t.Fatal("missing Root function") + } + var result *ssa.Call + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || !call.Common().IsInvoke() { + continue + } + if result != nil { + t.Fatal("Root has more than one interface invoke") + } + result = call + } + } + if result == nil { + t.Fatal("Root has no interface invoke") + } + return result +} diff --git a/cl/coro_interface_plain.go b/cl/coro_interface_plain.go new file mode 100644 index 0000000000..0cc2c278cd --- /dev/null +++ b/cl/coro_interface_plain.go @@ -0,0 +1,618 @@ +/* + * 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 ( + "fmt" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// coroClosedInterfacePlainPlan is a compilation-scoped proof that selected +// ordinary Go interface invokes and runtime ABI method-table references keep +// LLGo's existing receiver-aware raw method ABI. These uses are not first-class +// Go function values, so the certificate neither creates a function-value +// descriptor nor adds another scheduler/event path. +// +// A CHA candidate receives FuncRep=Dispatch because it is dynamically +// reachable. That does not mean the concrete method body is ever materialized +// as a first-class function value. In particular, an invoke in an EmitNone +// body can select Dispatch globally even though it has no physical ABI +// consumer. targets records exactly the methods for which every emitted +// consumer preserves that distinction. +type coroClosedInterfacePlainPlan struct { + calls map[ssa.CallInstruction]struct{} + targets map[coro.FunctionID]*ssa.Function +} + +func (p *coroClosedInterfacePlainPlan) acceptsCall(call ssa.CallInstruction) bool { + if p == nil || call == nil { + return false + } + _, ok := p.calls[call] + return ok +} + +func (p *coroClosedInterfacePlainPlan) acceptsTarget(fn *ssa.Function, plan coro.FunctionPlan) bool { + if p == nil || fn == nil { + return false + } + target, ok := p.targets[plan.ID] + return ok && target == fn +} + +// resolveMethodToken keeps a closed async method's itab discriminator on the +// exact physical entry. The word is compared but never called through the +// legacy method ABI, so wrapping it with closureWrapDecl would manufacture an +// invalid source-signature call to a (g,out,receiver,args...) coroutine entry. +func (p *context) resolveMethodToken( + resolvedName string, method *types.Func, signature *types.Signature, +) (llssa.Expr, bool) { + if p == nil || p.compilation == nil || p.compilation.coroClosedInterfacePlain == nil || + method == nil || signature == nil { + return llssa.Nil, false + } + target := p.resolveInterfaceMethodSSA(method, signature) + entry := p.mustFunctionSymbol(target) + if entry.plan.Emission != coro.EmitCoroutine || resolvedName != entry.name || + !p.compilation.coroClosedInterfacePlain.acceptsTarget(entry.function, entry.plan) { + return llssa.Nil, false + } + fn, _, kind := p.funcOfEntry(entry) + if fn == nil || kind != goFunc { + panic(fmt.Errorf("coroutine method token target %q did not resolve to one physical Go entry", entry.plan.ID)) + } + return fn.Expr, true +} + +// analyzeCoroClosedInterfacePlainPlan freezes the code-generation proof once, +// before any package can materialize a body. It deliberately derives every +// fact from exact SSA objects and immutable CallPlan/ValuePlan records. +func analyzeCoroClosedInterfacePlainPlan( + plan *coro.SSAPlan, + universe *EmissionUniverse, + explicitStatusPanic, interfaceAwait bool, + managedPlans ...*coroManagedInterfaceDispatchPlan, +) (*coroClosedInterfacePlainPlan, error) { + if plan == nil { + return nil, fmt.Errorf("closed interface plain island requires a compilation plan") + } + result := &coroClosedInterfacePlainPlan{ + calls: make(map[ssa.CallInstruction]struct{}), + targets: make(map[coro.FunctionID]*ssa.Function), + } + var managed *coroManagedInterfaceDispatchPlan + if len(managedPlans) != 0 { + managed = managedPlans[0] + } + // Restricted CHA may mark a receiver method Dispatch because of an + // unreachable interface consumer. A live type descriptor can independently + // demand that same method's raw ifn/tfn address. Freeze those exact live raw + // references before scanning SSA consumers so they are not mistaken for + // descriptor-backed Go function values. + for _, owner := range plan.Functions() { + if owner.Function == nil || (owner.Plan.Emission != coro.EmitPlain && owner.Plan.Emission != coro.EmitCoroutine) { + continue + } + references, err := universe.CoroDemandReferences(owner.Function) + if err != nil { + return nil, err + } + synchronous, err := universe.CoroSyncDemandReferences(owner.Function) + if err != nil { + return nil, err + } + syncTargets := make(map[*ssa.Function]struct{}, len(synchronous)) + for _, target := range synchronous { + syncTargets[target] = struct{}{} + } + for _, target := range references { + targetPlan, ok := plan.FunctionPlan(target) + if !ok { + return nil, fmt.Errorf("raw ABI method target %q has no compilation plan", target) + } + _, rawSyncTarget := syncTargets[target] + asyncMethodToken := !rawSyncTarget && target.Signature != nil && target.Signature.Recv() != nil && targetPlan.Emission == coro.EmitCoroutine + if rawSyncTarget { + if err := validateCoroRawABIEntryTarget(target, targetPlan); err != nil { + return nil, err + } + } else if asyncMethodToken { + if err := validateCoroRawABIMethodTokenTarget(target, targetPlan); err != nil { + return nil, err + } + } else if err := validateCoroRawABIPlainTarget(target, targetPlan); err != nil { + return nil, err + } + if asyncMethodToken || targetPlan.FuncRep == coro.Dispatch { + result.targets[targetPlan.ID] = target + } + } + } + // Function representation is selected before graph demand has removed dead + // bodies. Consequently a dormant interface invoke can be the sole reason a + // live, statically-called receiver method has FuncRep=Dispatch. Freeze only + // the exact demanded method candidates of EmitNone invokes here. This grants + // no invoke or descriptor capability: the scan below still rejects any live + // first-class value or non-interface dynamic consumer of the same method. + if err := freezeCoroDormantInterfaceDispatchTargets(plan, universe, result); err != nil { + return nil, err + } + firstClassUse := make(map[coro.FunctionID]string) + dynamicUse := make(map[coro.FunctionID]string) + seenValues := make(map[ssa.Value]struct{}) + + recordValue := func(owner *ssa.Function, value ssa.Value) { + if value == nil { + return + } + if _, seen := seenValues[value]; seen { + return + } + seenValues[value] = struct{}{} + valuePlan, ok := plan.ValuePlan(value) + if !ok { + return + } + for _, leaf := range valuePlan.Funcs { + for _, id := range leaf.Targets { + if _, exists := firstClassUse[id]; !exists { + firstClassUse[id] = fmt.Sprintf("function %q materializes target through first-class value %q", owner.Name(), value.Name()) + } + } + } + } + + for _, owner := range plan.Functions() { + if owner.Function == nil || (owner.Plan.Emission != coro.EmitPlain && owner.Plan.Emission != coro.EmitCoroutine) { + continue + } + fn := owner.Function + for _, param := range fn.Params { + recordValue(fn, param) + } + for _, free := range fn.FreeVars { + recordValue(fn, free) + } + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + var exactStaticCallee ssa.Value + if call, ok := instruction.(ssa.CallInstruction); ok && call.Common() != nil && call.Common().StaticCallee() != nil { + exactStaticCallee = call.Common().Value + } + if value, ok := instruction.(ssa.Value); ok { + recordValue(fn, value) + } + for _, operand := range instruction.Operands(nil) { + if operand != nil && *operand != exactStaticCallee { + recordValue(fn, *operand) + } + } + + call, isCall := instruction.(ssa.CallInstruction) + if !isCall || plan.ElidesCall(call) || call.Common() == nil { + continue + } + common := call.Common() + if _, builtin := common.Value.(*ssa.Builtin); builtin { + continue + } + callPlan, found := plan.CallPlan(call) + if !found { + if owner.Plan.Emission == coro.EmitCoroutine && common.IsInvoke() { + return nil, coroLeafInstructionError(fn, owner.Plan, instruction, "interface invoke has no compilation CallPlan") + } + continue + } + + if common.IsInvoke() { + if managed.acceptsCall(call) { + continue + } + if callPlan.Open && callPlan.Unresolved == coro.UnknownManagedInterfaceDispatch { + if !interfaceAwait || owner.Plan.Emission != coro.EmitCoroutine { + return nil, coroLeafInstructionError(fn, owner.Plan, instruction, + "managed interface descriptor requires coroutine child-await lowering") + } + if err := validateCoroManagedInterfaceDispatchCall(plan, universe, fn, call, callPlan); err != nil { + return nil, err + } + continue + } + targets, err := resolveCoroClosedInterfacePlainCall(plan, call) + if err == nil { + if explicitStatusPanic { + return nil, coroLeafInstructionError(fn, owner.Plan, instruction, "closed interface plain invoke requires the legacy panic ABI") + } + result.calls[call] = struct{}{} + for _, target := range targets { + result.targets[target.plan.ID] = target.function + } + continue + } + if interfaceAwait && owner.Plan.Emission == coro.EmitCoroutine { + if direct, ok := call.(*ssa.Call); ok { + if dispatch, awaitErr := resolveCoroInterfaceDispatchPlan(plan, universe, direct); awaitErr == nil && coroInterfaceDispatchNeedsAwait(dispatch) { + for _, candidate := range dispatch.candidates { + result.targets[candidate.id] = candidate.function + } + continue + } else { + err = fmt.Errorf("plain island: %v; coroutine dispatch: %v", err, awaitErr) + } + } + } + if owner.Plan.Emission == coro.EmitCoroutine { + return nil, coroLeafInstructionError(fn, owner.Plan, instruction, "unsupported interface invoke: "+err.Error()) + } + for _, id := range callPlan.Targets { + if _, exists := dynamicUse[id]; !exists { + dynamicUse[id] = fmt.Sprintf("function %q has another unverified interface invoke", fn.Name()) + } + } + continue + } + + // Exact static calls consume the method body entry directly and do + // not require a receiver-aware function-value descriptor. Every + // other CallPlan target is a second dynamic consumer. + if common.StaticCallee() == nil { + for _, id := range callPlan.Targets { + if _, exists := dynamicUse[id]; !exists { + dynamicUse[id] = fmt.Sprintf("function %q has another dynamic call consumer", fn.Name()) + } + } + } + } + } + } + + for _, function := range plan.Functions() { + id := function.Plan.ID + target, accepted := result.targets[id] + if !accepted { + continue + } + if reason := firstClassUse[id]; reason != "" { + return nil, fmt.Errorf("raw/interface plain target %q also has a function-value consumer: %s", id, reason) + } + if reason := dynamicUse[id]; reason != "" { + return nil, fmt.Errorf("raw/interface plain target %q also has a dynamic consumer: %s", id, reason) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.ID != id { + return nil, fmt.Errorf("closed interface plain target %q lost its exact function plan", id) + } + } + return result, nil +} + +func freezeCoroDormantInterfaceDispatchTargets( + plan *coro.SSAPlan, + universe *EmissionUniverse, + result *coroClosedInterfacePlainPlan, +) error { + if plan == nil || universe == nil || result == nil { + return fmt.Errorf("dormant interface dispatch requires an exact plan, emission universe, and receiver plan") + } + for _, owner := range plan.Functions() { + if owner.Function == nil || owner.Plan.Emission != coro.EmitNone { + continue + } + for _, block := range owner.Function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if !ok || plan.ElidesCall(call) || call.Common() == nil || !call.Common().IsInvoke() { + continue + } + callPlan, found := plan.CallPlan(call) + if !found || callPlan.Call != call || callPlan.Kind != coro.CallDirect || callPlan.Rep != coro.Dispatch { + continue + } + common := call.Common() + if _, ok := types.Unalias(common.Value.Type()).Underlying().(*types.Interface); !ok { + return coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("dormant interface receiver %s is not an interface", common.Value.Type())) + } + for _, targetID := range callPlan.Targets { + target, found := plan.Function(targetID) + if !found || target == nil { + return coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("dormant interface target %q is absent from the compilation plan", targetID)) + } + targetPlan, found := plan.FunctionPlan(target) + if !found || targetPlan.ID != targetID { + return coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("dormant interface target %q has no exact function plan", targetID)) + } + // An undemanded target has no physical entry to validate or + // certify. Its dormant CallPlan remains useful only as analysis + // metadata and cannot affect code generation. + if targetPlan.Emission == coro.EmitNone { + continue + } + // The dormant invoke has no physical call ABI, so its patched + // source signature need not match another package's effective + // method signature. It certifies only why representation analysis + // selected Dispatch. The actual emitted uses are proven below to + // be static/raw/receiver-aware, and the ordinary entry validator + // remains authoritative for the selected body. + if targetPlan.External != coro.Defined || targetPlan.FuncRep != coro.Dispatch || + target.Signature == nil || target.Signature.Recv() == nil || len(target.Blocks) == 0 || + target.Parent() != nil || len(target.FreeVars) != 0 { + return coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("dormant interface target %q is not one exact emitted receiver-only Dispatch body", targetID)) + } + if previous := result.targets[targetID]; previous != nil && previous != target { + return coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("dormant interface target %q resolves to both %q and %q", targetID, previous.Name(), target.Name())) + } + result.targets[targetID] = target + } + } + } + } + return nil +} + +// validateCoroRawABIEntryTarget validates the physical entry selected by one +// exact CoroSyncDemandReferences use. The historical strict single-plain-body +// validator remains unchanged. A coroutine managed primary is accepted only +// through the separately planned RawPlainEntry capability and its independent +// legacy symbol/body validation. +func validateCoroRawABIEntryTarget(target *ssa.Function, plan coro.FunctionPlan) error { + switch plan.Emission { + case coro.EmitPlain, coro.EmitExternal: + return validateCoroRawABIPlainTarget(target, plan) + case coro.EmitCoroutine, coro.EmitRawPlain: + return validatePlannedRawPlainEntry(target, plan) + default: + return fmt.Errorf("raw ABI function target %q (%s): unsupported emission %s", target, plan.ID, plan.Emission) + } +} + +func validateCoroRawABIPlainTarget(target *ssa.Function, plan coro.FunctionPlan) error { + fail := func(format string, args ...any) error { + name := "" + if target != nil { + name = target.String() + } + return fmt.Errorf("raw ABI function target %q (%s): %s", name, plan.ID, fmt.Sprintf(format, args...)) + } + if target == nil || target.Signature == nil || len(target.FreeVars) != 0 { + return fail("requires one non-capturing raw ABI function") + } + receiver := target.Signature.Recv() + externalMethodEntry := receiver != nil && plan.External != coro.Defined + if externalMethodEntry { + // Runtime type data embeds a receiver method's raw symbol address but does + // not call it while constructing the descriptor. C/assembly method + // entries therefore need no synthetic Go body here. A real interface + // invoke is validated separately by validateCoroClosedInterfacePlainCandidate + // (or the coroutine interface dispatcher), so this does not authorize a + // blocking foreign call on a scheduler thread. Receiver-less equality and + // hash callbacks remain on the strict owned-body path below. + if plan.Emission != coro.EmitExternal || plan.Primary != coro.PrimaryExternal || + plan.Demand == coro.NoDemand || plan.Effect != coro.NoSuspend || plan.Effect.IsOpaque() { + return fail( + "requires a demanded external no-suspend method entry, got external=%s emission=%s primary=%s demand=%s effect=%s", + plan.External, plan.Emission, plan.Primary, plan.Demand, plan.Effect, + ) + } + } else { + if len(target.Blocks) == 0 || plan.External != coro.Defined || plan.Emission != coro.EmitPlain || plan.Primary != coro.PrimaryPlain || + plan.Demand == coro.NoDemand || plan.Effect != coro.NoSuspend || plan.Effect.IsOpaque() { + return fail( + "requires a demanded defined no-suspend plain body, got external=%s emission=%s primary=%s demand=%s effect=%s", + plan.External, plan.Emission, plan.Primary, plan.Demand, plan.Effect, + ) + } + if plan.Exec&(coro.ThreadAffine|coro.NeedsPreempt) != 0 || plan.Exec.IsOpaque() { + return fail("execution constraints %s require a coroutine adapter", plan.Exec) + } + } + parameterBase := 0 + if receiver != nil { + parameterBase = 1 + } + if len(target.Params) != target.Signature.Params().Len()+parameterBase || + (receiver != nil && !types.Identical(target.Params[0].Type(), receiver.Type())) { + return fail("SSA body has no exact raw ABI parameter shape (receiver=%v, SSA params=%d, declared params=%d)", + receiver, len(target.Params), target.Signature.Params().Len()) + } + for index := 0; index < target.Signature.Params().Len(); index++ { + if !types.Identical(target.Params[index+parameterBase].Type(), target.Signature.Params().At(index).Type()) { + return fail("SSA parameter %d does not match declared parameter %d", index+parameterBase, index) + } + } + if plan.FuncRep != coro.DirectPlain && plan.FuncRep != coro.Dispatch { + return fail("representation %s has no raw plain method entry", plan.FuncRep) + } + return nil +} + +// validateCoroRawABIMethodTokenTarget accepts the one non-callable use of an +// async receiver method's ordinary itab word. In a closed coroutine invoke the +// word is only a stable discriminator: codegen compares it with the exact +// method symbol, then invokes the planned coroutine primary through structured +// child-await. It must never be called with the legacy raw method signature. +// +// Receiver-less equality/hash callbacks are deliberately excluded because the +// runtime calls those words directly. A first-class or otherwise unverified +// consumer is rejected later by analyzeCoroClosedInterfacePlainPlan. +func validateCoroRawABIMethodTokenTarget(target *ssa.Function, plan coro.FunctionPlan) error { + fail := func(format string, args ...any) error { + name := "" + if target != nil { + name = target.String() + } + return fmt.Errorf("raw ABI coroutine method token %q (%s): %s", name, plan.ID, fmt.Sprintf(format, args...)) + } + if target == nil || target.Signature == nil || target.Signature.Recv() == nil || + len(target.Blocks) == 0 || len(target.FreeVars) != 0 { + return fail("requires one defined non-capturing receiver body") + } + if plan.External != coro.Defined || plan.Emission != coro.EmitCoroutine || plan.Primary != coro.PrimaryCoroutine || + plan.Demand == coro.NoDemand || !plan.Effect.MaySuspend() || plan.Effect.IsOpaque() || + (plan.FuncRep != coro.DirectCoro && plan.FuncRep != coro.Dispatch) { + return fail( + "requires a demanded defined non-opaque coroutine body, got external=%s emission=%s primary=%s demand=%s representation=%s effect=%s", + plan.External, plan.Emission, plan.Primary, plan.Demand, plan.FuncRep, plan.Effect, + ) + } + if plan.Exec&(coro.BlockForeign|coro.ThreadAffine) != 0 || plan.Exec.IsOpaque() { + return fail("execution constraints %s have no closed coroutine method adapter", plan.Exec) + } + receiver := target.Signature.Recv() + if len(target.Params) != target.Signature.Params().Len()+1 || + !types.Identical(target.Params[0].Type(), receiver.Type()) { + return fail("SSA body has no exact raw ABI receiver shape (receiver=%v, SSA params=%d, declared params=%d)", + receiver, len(target.Params), target.Signature.Params().Len()) + } + for index := 0; index < target.Signature.Params().Len(); index++ { + if !types.Identical(target.Params[index+1].Type(), target.Signature.Params().At(index).Type()) { + return fail("SSA parameter %d does not match declared parameter %d", index+1, index) + } + } + return nil +} + +type coroClosedInterfacePlainTarget struct { + function *ssa.Function + plan coro.FunctionPlan +} + +// resolveCoroClosedInterfacePlainCall proves one exact ordinary itab invoke. +// Multiple concrete methods are allowed because the existing Go interface ABI +// performs that dispatch; all candidates must nevertheless be bounded plain +// bodies so the current physical frame cannot suspend through the call. +func resolveCoroClosedInterfacePlainCall(plan *coro.SSAPlan, call ssa.CallInstruction) ([]coroClosedInterfacePlainTarget, error) { + if plan == nil || call == nil || call.Common() == nil { + return nil, fmt.Errorf("requires an exact call and compilation CallPlan") + } + direct, ordinary := call.(*ssa.Call) + common := call.Common() + if !ordinary || direct == nil || !common.IsInvoke() || common.StaticCallee() != nil || common.Method == nil { + return nil, fmt.Errorf("requires an ordinary interface invoke") + } + iface, ok := types.Unalias(common.Value.Type()).Underlying().(*types.Interface) + if !ok { + return nil, fmt.Errorf("invoke receiver type %s is not an interface", common.Value.Type()) + } + iface.Complete() + callPlan, ok := plan.CallPlan(call) + if !ok || callPlan.Call != call { + return nil, fmt.Errorf("invoke has no exact compilation CallPlan") + } + if callPlan.Kind != coro.CallDirect || callPlan.Rep != coro.Dispatch || callPlan.Open || len(callPlan.Targets) == 0 { + return nil, fmt.Errorf( + "requires a closed nonempty Dispatch CallPlan, got kind=%v representation=%s open=%t may-be-nil=%t targets=%d", + callPlan.Kind, callPlan.Rep, callPlan.Open, callPlan.MayBeNil, len(callPlan.Targets), + ) + } + targets := make([]coroClosedInterfacePlainTarget, 0, len(callPlan.Targets)) + seen := make(map[coro.FunctionID]struct{}, len(callPlan.Targets)) + for _, id := range callPlan.Targets { + if _, duplicate := seen[id]; duplicate { + return nil, fmt.Errorf("invoke repeats target ID %q", id) + } + seen[id] = struct{}{} + target, found := plan.Function(id) + if !found || target == nil { + return nil, fmt.Errorf("invoke target %q is absent from the compilation plan", id) + } + targetPlan, found := plan.FunctionPlan(target) + if !found || targetPlan.ID != id { + return nil, fmt.Errorf("invoke target %q has no exact function plan", id) + } + if err := validateCoroClosedInterfacePlainCandidate(common, iface, id, target, targetPlan); err != nil { + return nil, err + } + targets = append(targets, coroClosedInterfacePlainTarget{function: target, plan: targetPlan}) + } + return targets, nil +} + +func validateCoroClosedInterfacePlainCandidate(common *ssa.CallCommon, iface *types.Interface, id coro.FunctionID, target *ssa.Function, plan coro.FunctionPlan) error { + fail := func(format string, args ...any) error { + return fmt.Errorf("invoke target %q: %s", id, fmt.Sprintf(format, args...)) + } + if common == nil || iface == nil || common.Method == nil || target == nil || target.Signature == nil { + return fail("missing method, receiver interface, or target signature") + } + if plan.ID != id { + return fail("function plan ID is %q", plan.ID) + } + if plan.External != coro.Defined || plan.Emission != coro.EmitPlain || plan.Primary != coro.PrimaryPlain || plan.FuncRep != coro.Dispatch || plan.Demand == coro.NoDemand { + return fail( + "requires a demanded defined plain Dispatch body, got external=%s emission=%s primary=%s representation=%s demand=%s", + plan.External, plan.Emission, plan.Primary, plan.FuncRep, plan.Demand, + ) + } + if plan.Effect != coro.NoSuspend || plan.Effect.IsOpaque() { + return fail("effect %s is not exact no-suspend", plan.Effect) + } + if plan.Exec&(coro.ThreadAffine|coro.NeedsPreempt) != 0 || plan.Exec.IsOpaque() { + return fail("execution constraints %s require preemption or open lowering", plan.Exec) + } + if len(target.Blocks) == 0 || len(target.FreeVars) != 0 { + return fail("requires one owned non-capturing SSA body") + } + recv := target.Signature.Recv() + if recv == nil { + return fail("candidate is not a declared method") + } + method, ok := target.Object().(*types.Func) + if !ok || method == nil { + return fail("candidate has no exact method object") + } + if method.Id() != common.Method.Id() { + return fail("method ID %q does not match invoke method ID %q", method.Id(), common.Method.Id()) + } + if !types.Implements(recv.Type(), iface) { + return fail("receiver %s does not implement invoke interface %s", recv.Type(), iface) + } + selected, _, _ := types.LookupFieldOrMethod(recv.Type(), false, common.Method.Pkg(), common.Method.Name()) + selectedMethod, ok := selected.(*types.Func) + if !ok || selectedMethod == nil || selectedMethod.Id() != method.Id() { + return fail("receiver method selection does not resolve exact method ID %q", method.Id()) + } + callSignature := coroClosedInterfacePlainCallableSignature(common.Signature()) + targetSignature := coroClosedInterfacePlainCallableSignature(target.Signature) + if callSignature == nil || targetSignature == nil || !types.Identical(callSignature, targetSignature) { + return fail("call signature %v does not match receiver-free target signature %v", callSignature, targetSignature) + } + if len(target.Params) != target.Signature.Params().Len()+1 || !types.Identical(target.Params[0].Type(), recv.Type()) { + return fail("SSA parameters do not contain the exact declared receiver") + } + for index := 0; index < target.Signature.Params().Len(); index++ { + if !types.Identical(target.Params[index+1].Type(), target.Signature.Params().At(index).Type()) { + return fail("SSA parameter %d does not match declared method parameter %d", index+1, index) + } + } + return nil +} + +func coroClosedInterfacePlainCallableSignature(sig *types.Signature) *types.Signature { + if sig == nil { + return nil + } + return types.NewSignatureType(nil, nil, nil, sig.Params(), sig.Results(), sig.Variadic()) +} diff --git a/cl/coro_interface_plain_test.go b/cl/coro_interface_plain_test.go new file mode 100644 index 0000000000..bd5ecf6715 --- /dev/null +++ b/cl/coro_interface_plain_test.go @@ -0,0 +1,538 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroClosedInterfacePlainSource = `package foo + +var gate chan uint32 + +type Value interface { Value() uint32 } +type concrete uint32 + +func (value concrete) Value() uint32 { return uint32(value) + 1 } + +func Root(value Value) uint32 { + <-gate + return value.Value() +} +` + +func TestCoroManagedClosedInterfaceInvokeSurvivesCoroSplit(t *testing.T) { + prog, pkg, plan, root, method, invoke := compileCoroClosedInterfacePlainFixture(t, coroClosedInterfacePlainSource, coro.DynamicCHAClosed) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.FuncRep != coro.DirectCoro || + !rootPlan.Effect.Contains(coro.MayPark|coro.AwaitStructured) { + t.Fatalf("Root plan = %+v, present=%t; want a parking managed-interface-await coroutine", rootPlan, ok) + } + methodPlan, ok := plan.FunctionPlan(method) + if !ok || methodPlan.Emission != coro.EmitPlain || methodPlan.Primary != coro.PrimaryPlain || + methodPlan.FuncRep != coro.Dispatch || methodPlan.Effect != coro.NoSuspend || methodPlan.Exec != 0 { + t.Fatalf("concrete.Value plan = %+v, present=%t; want an exactly no-unwind plain descriptor target", methodPlan, ok) + } + callPlan, ok := plan.CallPlan(invoke) + if !ok || callPlan.Open || callPlan.Rep != coro.Dispatch || len(callPlan.Targets) == 0 || !coroInterfaceTargetContains(callPlan.Targets, methodPlan.ID) { + t.Fatalf("interface CallPlan = %+v, present=%t; want a nonempty closed Dispatch target set containing the declared method", callPlan, ok) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify closed interface coroutine before CoroSplit: %v\n%s", err, module.String()) + } + rootIR := requireCoroPhysicalFunction(t, module, "foo.Root").String() + assertCoroManagedClosedInterfaceIR(t, rootIR) + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() { + t.Fatalf("CoroSplit did not create Root resume entry:\n%s", module.String()) + } + assertCoroManagedClosedInterfaceIR(t, resume.String()) + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify closed interface coroutine after CoroSplit: %v\n%s", err, module.String()) + } +} + +func TestCoroClosedInterfacePlainTargetMayAlsoHaveStaticCalls(t *testing.T) { + const source = `package foo +var gate chan uint32 +type Value interface { Value() uint32 } +type concrete uint32 +func (value concrete) Value() uint32 { return uint32(value) + 1 } +func Root(value Value, direct concrete) uint32 { + <-gate + observed := direct.Value() + return observed + value.Value() +} +` + prog, pkg, _, _, _, _ := compileCoroClosedInterfacePlainFixture(t, source, coro.DynamicCHAClosed) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify interface target with static call: %v\n%s", err, module.String()) + } + assertCoroManagedClosedInterfaceIR(t, requireCoroPhysicalFunction(t, module, "foo.Root").String()) +} + +func TestCoroDormantInterfaceInvokeDoesNotTurnStaticMethodIntoFunctionValue(t *testing.T) { + const source = `package foo +var gate chan struct{} +type text interface { String() string } +type concrete string +func (value concrete) String() string { return string(value) } +func dormant(value text) string { return value.String() } +func Root(value concrete) string { + <-gate + return value.String() +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + program := newLLSSAProg(t) + defer program.Dispose() + universe, plan, root, dormant, method, invoke := prepareCoroDormantInterfaceFixture(t, program, ssaPkg, files) + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine { + t.Fatalf("Root plan = %+v, present=%t; want one emitted coroutine", rootPlan, ok) + } + dormantPlan, ok := plan.FunctionPlan(dormant) + if !ok || dormantPlan.Emission != coro.EmitNone { + t.Fatalf("dormant plan = %+v, present=%t; want EmitNone", dormantPlan, ok) + } + methodPlan, ok := plan.FunctionPlan(method) + if !ok || methodPlan.Emission != coro.EmitCoroutine || methodPlan.Primary != coro.PrimaryCoroutine || + methodPlan.FuncRep != coro.Dispatch || !methodPlan.Effect.Contains(coro.OutcomeStructured) { + t.Fatalf("concrete.String plan = %+v, present=%t; want one explicit-outcome coroutine selected by the live static call", methodPlan, ok) + } + callPlan, ok := plan.CallPlan(invoke) + if !ok || callPlan.Open || callPlan.Rep != coro.Dispatch || !coroInterfaceTargetContains(callPlan.Targets, methodPlan.ID) { + t.Fatalf("dormant invoke CallPlan = %+v, present=%t; want closed Dispatch target %q", callPlan, ok, methodPlan.ID) + } + + receivers, err := analyzeCoroClosedInterfacePlainPlan(plan, universe, false, true) + if err != nil { + t.Fatal(err) + } + if !receivers.acceptsTarget(method, methodPlan) { + t.Fatalf("dormant receiver proof did not freeze exact static method target %q", methodPlan.ID) + } + if err := validateCoroDynamicDispatchTarget(method, methodPlan); err == nil || + !strings.Contains(err.Error(), "methods require receiver-aware dispatch lowering") { + t.Fatalf("receiver-free function-value validator accepted method target: %v", err) + } + + pkg, _, err := NewPackageExWithEmbedOptions( + program, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: coroClosedInterfacePlainCompilation(plan, universe)}, + ) + if err != nil { + t.Fatalf("compile static method with dormant interface CHA source: %v", err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify static method with dormant interface CHA source: %v\n%s", err, module.String()) + } + if ir := module.String(); strings.Contains(ir, coroPlainDispatchDescriptorPrefix) || strings.Contains(ir, coroPlainDispatchThunkPrefix) { + t.Fatalf("dormant invoke incorrectly materialized a function-value descriptor:\n%s", ir) + } +} + +func TestCoroDormantInterfaceTargetSupportsLiveFirstClassMethodExpression(t *testing.T) { + const source = `package foo +var gate chan struct{} +type text interface { String() string } +type concrete string +func (value concrete) String() string { return string(value) } +func dormant(value text) string { return value.String() } +func consume(func(concrete) string) {} +func Root(value concrete) string { + <-gate + consume(concrete.String) + return value.String() +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + program := newLLSSAProg(t) + defer program.Dispose() + universe, plan, _, _, _, _ := prepareCoroDormantInterfaceFixture(t, program, ssaPkg, files) + + if _, err := analyzeCoroClosedInterfacePlainPlan(plan, universe, false, true); err != nil { + t.Fatalf("declared receiver body was confused with its first-class method-expression wrapper: %v", err) + } + pkg, _, err := NewPackageExWithEmbedOptions( + program, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: coroClosedInterfacePlainCompilation(plan, universe)}, + ) + if err != nil { + t.Fatalf("compile live first-class method expression: %v", err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify live first-class method expression: %v\n%s", err, module.String()) + } + if ir := module.String(); !strings.Contains(ir, coroPlainDispatchDescriptorPrefix) || !strings.Contains(ir, coroCoroDispatchThunkPrefix) { + t.Fatalf("live first-class method expression did not materialize its descriptor and entry thunk:\n%s", ir) + } +} + +func prepareCoroDormantInterfaceFixture( + t *testing.T, + program llssa.Program, + ssaPkg *ssa.Package, + files []*ast.File, +) (*EmissionUniverse, *coro.SSAPlan, *ssa.Function, *ssa.Function, *ssa.Function, *ssa.Call) { + t.Helper() + universe, err := prepareStacklessEmissionUniverseWithOptions( + program, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + dormant := ssaPkg.Func("dormant") + var method *ssa.Function + for _, fn := range universe.Functions() { + if fn != nil && fn.Name() == "String" && fn.Signature != nil && fn.Signature.Recv() != nil { + method = fn + break + } + } + if root == nil || dormant == nil || method == nil { + t.Fatalf("fixture functions root=%v dormant=%v method=%v", root, dormant, method) + } + var invoke *ssa.Call + for _, block := range dormant.Blocks { + for _, instruction := range block.Instrs { + if call, ok := instruction.(*ssa.Call); ok && call.Common().IsInvoke() { + invoke = call + } + } + } + if invoke == nil { + t.Fatal("dormant interface invoke not found") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + DynamicResolution: coro.DynamicCHAClosed, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + }) + if err != nil { + t.Fatal(err) + } + return universe, plan, root, dormant, method, invoke +} + +func TestCoroClosedInterfacePlainInvokeCompatibility(t *testing.T) { + tests := []struct { + name string + source string + resolution coro.DynamicResolution + want string + }{ + { + name: "open world", + source: coroClosedInterfacePlainSource, + resolution: coro.DynamicCHAOpen, + want: "closed nonempty Dispatch CallPlan", + }, + { + name: "suspending target with method-expression consumer", + source: `package foo +var gate chan uint32 +type Value interface { Value() uint32 } +type concrete struct{} +func (value *concrete) Value() uint32 { return <-gate } +func consume(func(*concrete) uint32) {} +func Root(value Value) uint32 { + <-gate + consume((*concrete).Value) + return value.Value() +} +`, + resolution: coro.DynamicCHAClosed, + want: "", + }, + { + name: "plain method-expression consumer", + source: `package foo +var gate chan uint32 +type Value interface { Value() uint32 } +type concrete uint32 +func (value concrete) Value() uint32 { return uint32(value) + 1 } +func consume(func(concrete) uint32) {} +func Root(value Value) uint32 { + <-gate + consume(concrete.Value) + return value.Value() +} +`, + resolution: coro.DynamicCHAClosed, + want: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, test.source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, plan, _, _, _ := prepareCoroClosedInterfacePlainPlan(t, prog, ssaPkg, files, test.resolution) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: coroClosedInterfacePlainCompilation(plan, universe)}, + ) + if test.want == "" { + if err != nil { + t.Fatalf("compile exact method-expression consumer: %v", err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify exact method-expression consumer: %v\n%s", err, module.String()) + } + if ir := module.String(); !strings.Contains(ir, coroPlainDispatchDescriptorPrefix) { + t.Fatalf("exact method-expression consumer did not materialize a descriptor:\n%s", ir) + } + return + } + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("compile error = %v, want substring %q", err, test.want) + } + }) + } +} + +func TestCoroRawABIPlainTargetRejectsThreadAffineMethod(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, coroClosedInterfacePlainSource) + prog := newLLSSAProg(t) + defer prog.Dispose() + _, plan, _, method, _ := prepareCoroClosedInterfacePlainPlan(t, prog, ssaPkg, files, coro.DynamicCHAClosed) + methodPlan, ok := plan.FunctionPlan(method) + if !ok { + t.Fatal("concrete.Value has no function plan") + } + methodPlan.Exec |= coro.ThreadAffine + if err := validateCoroRawABIPlainTarget(method, methodPlan); err == nil || !strings.Contains(err.Error(), "thread-affine") { + t.Fatalf("thread-affine raw method error = %v; want fail-closed execution constraint", err) + } +} + +func TestCoroRawABIPlainTargetAcceptsExternalMethodAddressOnly(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, coroClosedInterfacePlainSource) + prog := newLLSSAProg(t) + defer prog.Dispose() + _, plan, _, method, _ := prepareCoroClosedInterfacePlainPlan(t, prog, ssaPkg, files, coro.DynamicCHAClosed) + methodPlan, ok := plan.FunctionPlan(method) + if !ok { + t.Fatal("concrete.Value has no function plan") + } + methodPlan.External = coro.ExternalUnknownForeign + methodPlan.Emission = coro.EmitExternal + methodPlan.Primary = coro.PrimaryExternal + methodPlan.FuncRep = coro.DirectPlain + methodPlan.Effect = coro.NoSuspend + methodPlan.Exec = coro.BlockForeign | coro.IRQUnsafe + if err := validateCoroRawABIPlainTarget(method, methodPlan); err != nil { + t.Fatalf("external raw method address rejected: %v", err) + } +} + +func TestCoroClosedInterfacePlainCandidateRejectsMethodMismatch(t *testing.T) { + const source = `package foo +var gate chan uint32 +type Value interface { Value() uint32 } +type concrete uint32 +func (value concrete) Value() uint32 { return uint32(value) + 1 } +func (value concrete) Other() uint32 { return uint32(value) + 2 } +func Root(value Value) uint32 { <-gate; return value.Value() } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, plan, _, method, invoke := prepareCoroClosedInterfacePlainPlan(t, prog, ssaPkg, files, coro.DynamicCHAClosed) + methodPlan, ok := plan.FunctionPlan(method) + if !ok { + t.Fatal("Value method has no plan") + } + var other *ssa.Function + for _, fn := range universe.Functions() { + if fn != nil && fn.Name() == "Other" && fn.Signature != nil && fn.Signature.Recv() != nil { + other = fn + break + } + } + if other == nil { + t.Fatal("Other method not found in emission universe") + } + iface := invoke.Common().Value.Type().Underlying().(*types.Interface) + err := validateCoroClosedInterfacePlainCandidate(invoke.Common(), iface, methodPlan.ID, other, methodPlan) + if err == nil || !strings.Contains(err.Error(), "method ID") { + t.Fatalf("method mismatch error = %v", err) + } +} + +func TestCoroClosedInterfaceInvokeUsesExplicitStatusABI(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, coroClosedInterfacePlainSource) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, plan, _, _, _ := prepareCoroClosedInterfacePlainPlan(t, prog, ssaPkg, files, coro.DynamicCHAClosed) + compilation := coroClosedInterfacePlainCompilation(plan, universe) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile explicit-status closed interface: %v", err) + } + ir := requireCoroPhysicalFunction(t, pkg.Module(), "foo.Root").String() + assertCoroManagedClosedInterfaceIR(t, ir) + if !strings.Contains(ir, coroFaultPrepareHookV1) { + t.Fatalf("explicit-status closed interface lacks nil-interface fault lowering:\n%s", ir) + } +} + +func compileCoroClosedInterfacePlainFixture(t *testing.T, source string, resolution coro.DynamicResolution) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, *ssa.Function, *ssa.Call, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + universe, plan, root, method, invoke := prepareCoroClosedInterfacePlainPlan(t, prog, ssaPkg, files, resolution) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: coroClosedInterfacePlainCompilation(plan, universe)}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root, method, invoke +} + +func prepareCoroClosedInterfacePlainPlan(t *testing.T, prog llssa.Program, ssaPkg *ssa.Package, files []*ast.File, resolution coro.DynamicResolution) ( + *EmissionUniverse, *coro.SSAPlan, *ssa.Function, *ssa.Function, *ssa.Call, +) { + t.Helper() + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + var method *ssa.Function + for _, fn := range universe.Functions() { + if fn != nil && fn.Name() == "Value" && fn.Signature != nil && fn.Signature.Recv() != nil { + method = fn + break + } + } + if method == nil { + t.Fatal("concrete Value method not found in emission universe") + } + var invoke *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if call, ok := instruction.(*ssa.Call); ok && call.Common().IsInvoke() { + invoke = call + } + } + } + if invoke == nil { + t.Fatal("Root interface invoke not found") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + DynamicResolution: resolution, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + }) + if err != nil { + t.Fatal(err) + } + return universe, plan, root, method, invoke +} + +func coroClosedInterfacePlainCompilation(plan *coro.SSAPlan, universe *EmissionUniverse) *Compilation { + return &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + } +} + +func assertCoroManagedClosedInterfaceIR(t *testing.T, ir string) { + t.Helper() + if !strings.Contains(ir, "llvm.coro.suspend") && !strings.Contains(ir, ".resume") { + t.Fatalf("coroutine body has no suspension/resume marker:\n%s", ir) + } + if !strings.Contains(ir, "coro.dispatch") { + t.Fatalf("coroutine body does not use the managed descriptor protocol:\n%s", ir) + } + if !strings.Contains(ir, coroAwaitPrepareHookV1) { + t.Fatalf("coroutine body does not await the managed interface child:\n%s", ir) + } +} + +func coroInterfaceTargetContains(targets []coro.FunctionID, want coro.FunctionID) bool { + for _, target := range targets { + if target == want { + return true + } + } + return false +} diff --git a/cl/coro_interface_zero_receiver_test.go b/cl/coro_interface_zero_receiver_test.go new file mode 100644 index 0000000000..2c7df09d79 --- /dev/null +++ b/cl/coro_interface_zero_receiver_test.go @@ -0,0 +1,186 @@ +//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/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroClosedInterfaceAwaitAdaptsZeroSizedPointerReceiver(t *testing.T) { + const source = `package foo + +var gate chan byte + +type Runner interface { + Run() int + Close() +} + +type Zero struct{} + +func (Zero) Run() int { + <-gate + return 7 +} + +func (*Zero) Close() {} + +func Keep() Runner { return &Zero{} } + +func Root(runner Runner) int { return runner.Run() } +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + program := newLLSSAProg(t) + defer program.Dispose() + universe, err := prepareStacklessEmissionUniverseWithOptions( + program, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + invoke := coroInterfaceDispatchFindInvoke(t, root) + var declared, wrapper *ssa.Function + for _, function := range universe.Functions() { + if function == nil || function.Name() != "Run" || function.Signature == nil || function.Signature.Recv() == nil { + continue + } + _, pointer := types.Unalias(function.Signature.Recv().Type()).Underlying().(*types.Pointer) + switch { + case !pointer && function.Synthetic == "": + declared = function + case pointer && strings.Contains(function.Synthetic, "wrapper"): + wrapper = function + } + } + if declared == nil || wrapper == nil { + t.Fatalf("zero-size pointer-promotion methods: declared=%v wrapper=%v", declared, wrapper) + } + + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + DynamicResolution: coro.DynamicCHAClosed, + MaxPlainInstructions: -1, + }) + if err != nil { + t.Fatal(err) + } + dispatch, err := resolveCoroInterfaceDispatchPlan(plan, universe, invoke) + if err != nil { + t.Fatal(err) + } + if len(dispatch.candidates) != 1 { + t.Fatalf("zero-size interface candidates = %d, want one: %+v", len(dispatch.candidates), dispatch.candidates) + } + candidate := dispatch.candidates[0] + dynamicPointer, pointer := types.Unalias(candidate.receiver).Underlying().(*types.Pointer) + declaredReceiver := declared.Signature.Recv().Type() + if !pointer || !types.Identical(dynamicPointer.Elem(), declaredReceiver) || candidate.function != wrapper || + candidate.methodEntry != wrapper || !types.Identical(candidate.targetReceiver, candidate.receiver) { + t.Fatalf( + "zero-size receiver adaptation = dynamic:%s target:%s function:%v entry:%v; want the exact *Zero method-set wrapper", + candidate.receiver, candidate.targetReceiver, candidate.function, candidate.methodEntry, + ) + } + if size := program.SizeOf(program.Type(declaredReceiver, llssa.InGo)); size != 0 { + t.Fatalf("declared receiver %s has size %d, want zero", declaredReceiver, size) + } + adaptation := false + for _, block := range wrapper.Blocks { + for _, instruction := range block.Instrs { + load, ok := instruction.(*ssa.UnOp) + if ok && load.Op == token.MUL && types.Identical(load.Type(), declaredReceiver) { + adaptation = true + } + } + } + if !adaptation { + t.Fatalf("pointer method-set wrapper %v has no exact *Zero -> Zero SSA adaptation", wrapper) + } + + compilation := coroClosedInterfacePlainCompilation(plan, universe) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + compiled, _, err := NewPackageExWithEmbedOptions( + program, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile zero-size interface await: %v", err) + } + module := compiled.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify zero-size interface await before CoroSplit: %v\n%s", err, module.String()) + } + if ir := module.String(); strings.Contains(ir, "AssertNilDeref") { + t.Fatalf("zero-size interface module retained a native-stack nil assertion:\n%s", ir) + } + rootIR := requireCoroPhysicalFunction(t, module, "foo.Root").String() + if !strings.Contains(rootIR, "call void @"+coroAwaitPrepareHookV1) || + !strings.Contains(rootIR, "call i8 @llvm.coro.suspend") { + t.Fatalf("zero-size interface dispatch did not use structured child-await lowering:\n%s", rootIR) + } + var wrapperCoro llvm.Value + for function := module.FirstFunction(); !function.IsNil(); function = llvm.NextFunction(function) { + if strings.Contains(function.Name(), "$llgo$promoted$") && strings.HasSuffix(function.Name(), "$coro") { + if !wrapperCoro.IsNil() { + t.Fatalf("multiple promoted coroutine wrappers: %q and %q", wrapperCoro.Name(), function.Name()) + } + wrapperCoro = function + } + } + if wrapperCoro.IsNil() { + t.Fatalf("zero-size value receiver has no promoted coroutine wrapper:\n%s", module.String()) + } + wrapperIR := wrapperCoro.String() + if strings.Contains(wrapperIR, "AssertNilDeref") || + !strings.Contains(wrapperIR, "call void @"+coroFaultPrepareHookV1) || + !strings.Contains(wrapperIR, "call void @"+coroAwaitPrepareHookV1) { + t.Fatalf("promoted wrapper did not lower pointer adaptation through structured fault/await edges:\n%s", wrapperIR) + } + + runCoroABITestPipeline(t, program, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() || strings.Contains(module.String(), "AssertNilDeref") { + t.Fatalf("post-split zero-size receiver resume is absent or retained AssertNilDeref:\n%s", module.String()) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify zero-size interface await after CoroSplit: %v\n%s", err, module.String()) + } +} diff --git a/cl/coro_len_builtin_test.go b/cl/coro_len_builtin_test.go new file mode 100644 index 0000000000..7c038864e8 --- /dev/null +++ b/cl/coro_len_builtin_test.go @@ -0,0 +1,162 @@ +//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/token" + "go/types" + "strings" + "testing" + + "golang.org/x/tools/go/ssa" +) + +func TestCoroLenBuiltinGenericMapRequiresExactMapLenHelper(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func MakeLen[K comparable, V any](values map[K]V) func() int { + return func() int { return len(values) } +} +func Root(values map[int]string) int { return MakeLen(values)() } +`) + origin := ssaPkg.Func("MakeLen") + if origin == nil || len(origin.AnonFuncs) != 1 { + t.Fatalf("generic MakeLen anonymous functions = %d, want one", len(origin.AnonFuncs)) + } + closure := origin.AnonFuncs[0] + call := coroLenBuiltinCall(t, closure) + operand := call.Common().Args[0].Type() + if _, ok := types.Unalias(operand).Underlying().(*types.Map); !ok { + t.Fatalf("generic len operand = %T %v, want map[K]V", types.Unalias(operand).Underlying(), operand) + } + if got := coroPhysicalLenKind(operand); got != coroPhysicalLenMap { + t.Fatalf("generic map len kind = %d, want exact MapLen lowering", got) + } + audit, err := newCoroPhysicalPureSSAAudit(nil, nil, closure, "") + if err != nil { + t.Fatal(err) + } + if reason := audit.validateLenBuiltin(call); reason != "runtime helper capability validation requires a frozen emission universe" { + t.Fatalf("generic map len validation = %q, want exact MapLen helper-plan gate", reason) + } +} + +func TestCoroLenBuiltinDoesNotInferLoweringFromUnknownTypeParameter(t *testing.T) { + constraint := types.NewInterfaceType(nil, nil).Complete() + parameter := types.NewTypeParam(types.NewTypeName(token.NoPos, nil, "T", nil), constraint) + if got := coroPhysicalLenKind(parameter); got != coroPhysicalLenUnsupported { + t.Fatalf("bare type parameter len kind = %d, want fail-closed", got) + } + if got := coroPhysicalLenKind(types.NewMap(parameter, types.Typ[types.Int])); got != coroPhysicalLenMap { + t.Fatalf("map[T]int len kind = %d, want exact map-header lowering", got) + } + if got := coroPhysicalLenKind(types.NewSlice(parameter)); got != coroPhysicalLenInline { + t.Fatalf("[]T len kind = %d, want exact inline slice-header lowering", got) + } + if got := coroPhysicalLenKind(constraint); got != coroPhysicalLenUnsupported { + t.Fatalf("interface len kind = %d, want fail-closed", got) + } +} + +func TestCoroLenAndCapBuiltinChannelDirectionsRequireExactHelpers(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func Ops[T any](recv <-chan T, send chan<- T, both chan T, values []T) int { + return len(recv) + len(send) + len(both) + cap(recv) + cap(send) + cap(both) + cap(values) +} +`) + function := ssaPkg.Func("Ops") + if function == nil { + t.Fatal("missing generic Ops function") + } + audit, err := newCoroPhysicalPureSSAAudit(nil, nil, function, "") + if err != nil { + t.Fatal(err) + } + counts := map[string]int{} + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Common() == nil { + continue + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || (builtin.Name() != "len" && builtin.Name() != "cap") { + continue + } + counts[builtin.Name()]++ + _, channel := types.Unalias(call.Common().Args[0].Type()).Underlying().(*types.Chan) + var reason string + if builtin.Name() == "len" { + reason = audit.validateLenBuiltin(call) + } else { + reason = audit.validateCapBuiltin(call) + } + if channel { + if !strings.Contains(reason, "runtime helper capability validation requires a frozen emission universe") { + t.Errorf("%s(%s) validation = %q, want exact channel-helper gate", builtin.Name(), call.Common().Args[0].Type(), reason) + } + } else if builtin.Name() != "cap" || reason != "" { + t.Errorf("non-channel builtin %s(%s) validation = %q, want inline cap(slice)", builtin.Name(), call.Common().Args[0].Type(), reason) + } + } + } + if counts["len"] != 3 || counts["cap"] != 4 { + t.Fatalf("builtin counts = %+v, want three channel len and three channel plus one slice cap", counts) + } + + parameter := types.NewTypeParam( + types.NewTypeName(token.NoPos, nil, "T", nil), + types.NewInterfaceType(nil, nil).Complete(), + ) + for _, direction := range []types.ChanDir{types.SendRecv, types.RecvOnly, types.SendOnly} { + channel := types.NewChan(direction, parameter) + if got := coroPhysicalLenKind(channel); got != coroPhysicalLenChan { + t.Errorf("%s len kind = %d, want exact ChanLen lowering", channel, got) + } + if got := coroPhysicalCapKind(channel); got != coroPhysicalCapChan { + t.Errorf("%s cap kind = %d, want exact ChanCap lowering", channel, got) + } + } + if got := coroPhysicalCapKind(types.NewSlice(parameter)); got != coroPhysicalCapInline { + t.Fatalf("[]T cap kind = %d, want exact inline slice-header lowering", got) + } + if got := coroPhysicalCapKind(parameter); got != coroPhysicalCapUnsupported { + t.Fatalf("bare type parameter cap kind = %d, want fail-closed", got) + } + if got := coroPhysicalCapKind(parameter.Constraint()); got != coroPhysicalCapUnsupported { + t.Fatalf("interface cap kind = %d, want fail-closed", got) + } +} + +func coroLenBuiltinCall(t *testing.T, fn *ssa.Function) *ssa.Call { + t.Helper() + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Common() == nil { + continue + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if ok && builtin.Name() == "len" { + return call + } + } + } + t.Fatalf("%s has no len builtin", fn.Name()) + return nil +} diff --git a/cl/coro_linkname_visibility.go b/cl/coro_linkname_visibility.go new file mode 100644 index 0000000000..a480bf1e9e --- /dev/null +++ b/cl/coro_linkname_visibility.go @@ -0,0 +1,183 @@ +/* + * 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 ( + "fmt" + "go/ast" + "strings" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// CoroGoLinknameVisibilityCertificate proves that one exact bodyful, +// one-argument `//go:linkname local` directive (two lexical fields including +// the directive token) changes only linker visibility. It +// does not redirect the function's default managed Go symbol and therefore is +// not, by itself, a raw synchronous caller or a request for a second body. +// Actual bodyless Go consumers are still joined by final symbol + structural +// signature before this certificate is frozen. +type CoroGoLinknameVisibilityCertificate struct { + ID string + PhysicalSymbol string + ABISignature string +} + +// attachedGoLinknameVisibilityDirective accepts only the exact visibility-only +// source shape. Redirecting linknames, malformed/duplicate directives, and any +// additional export, cgo, wasm, or custom physical ABI directive remain raw +// boundaries and deliberately receive no certificate. +func attachedGoLinknameVisibilityDirective(fn *ssa.Function) (string, bool) { + if fn == nil || fn.Parent() != nil || len(fn.FreeVars) != 0 { + return "", false + } + decl, _ := fn.Syntax().(*ast.FuncDecl) + if decl == nil || decl.Body == nil || decl.Doc == nil || decl.Name == nil || decl.Recv != nil { + return "", false + } + _, localName := astFuncName("", decl) + var found string + for _, comment := range decl.Doc.List { + if comment == nil { + continue + } + text := strings.TrimSpace(comment.Text) + fields := strings.Fields(text) + if len(fields) != 0 && fields[0] == "//go:linkname" { + if found != "" || len(fields) != 2 || fields[1] != localName { + return "", false + } + found = text + continue + } + for _, prefix := range []string{ + "//llgo:link", "// llgo:link", "//export", "//go:wasmexport", "//go:wasmimport", + } { + if text == prefix || strings.HasPrefix(text, prefix+" ") { + return "", false + } + } + if strings.HasPrefix(text, "//go:cgo_") { + return "", false + } + } + if found == "" || fn.Signature == nil || fn.Signature.Recv() != nil || fn.Signature.Variadic() || functionNeedsLinkOnce(fn) { + return "", false + } + if params := fn.TypeParams(); params != nil && params.Len() != 0 { + return "", false + } + if params := fn.Signature.TypeParams(); params != nil && params.Len() != 0 { + return "", false + } + if params := fn.Signature.RecvTypeParams(); params != nil && params.Len() != 0 { + return "", false + } + return found, true +} + +// freezeCoroGoLinknameVisibilityCertificates binds the strict source shape to +// the already frozen frontend kind, owner, structural ABI, final symbol, and +// target identity. A source directive alone is never sufficient evidence. +func (u *EmissionUniverse) freezeCoroGoLinknameVisibilityCertificates() error { + for _, fn := range u.functions { + if _, exact := attachedGoLinknameVisibilityDirective(fn); !exact { + continue + } + if fn.Pkg == nil || fn.Pkg.Pkg == nil || len(fn.Blocks) == 0 { + continue + } + canonical := u.canonicalAlias(fn) + if canonical == nil { + return fmt.Errorf("prepare emission universe: go:linkname visibility function %q has cyclic canonical aliases", fn.Name()) + } + if canonical != fn { + continue + } + background, classified, err := u.FunctionBackground(fn) + if err != nil { + return err + } + if !classified || background != llssa.InGo { + continue + } + owners := u.sortedUseOwners(fn) + if len(owners) != 1 { + continue + } + owner := owners[0] + ownerKey := emissionFunctionOwnerKey{function: fn, owner: owner} + if u.functionKinds[ownerKey] != goFunc { + continue + } + finalKey := u.finalKeys[ownerKey] + kind, symbol, signature, valid := splitManagedSymbolKey(finalKey) + if !valid || kind != goFunc || signature == "" { + continue + } + if physical := u.physicalNames[ownerKey]; physical != "" { + symbol = physical + } + defaultSymbol := funcName(fn.Pkg.Pkg, fn, false) + if symbol != defaultSymbol { + continue + } + linkIdentity := u.linkIdentities[fn] + if linkIdentity == "" { + return fmt.Errorf("prepare emission universe: go:linkname visibility function %q has no frozen link identity", fn.Name()) + } + target := u.prog.TargetSpec() + u.goLinknameVisibility[fn] = CoroGoLinknameVisibilityCertificate{ + ID: framedEmissionKey( + "llgo-coro-go-linkname-visibility-v0", + owner.identity, + owner.pkgPath, + linkIdentity, + finalKey, + symbol, + signature, + target.Triple, + target.CPU, + target.Features, + target.TargetABI, + u.prog.DataLayout(), + ), + PhysicalSymbol: symbol, + ABISignature: signature, + } + } + return nil +} + +func (u *EmissionUniverse) coroGoLinknameVisibilityCertificate(fn *ssa.Function) (certificate CoroGoLinknameVisibilityCertificate, certified bool, err error) { + if u == nil { + return certificate, false, fmt.Errorf("coroutine go:linkname visibility certificate: nil emission universe") + } + if fn == nil { + return certificate, false, fmt.Errorf("coroutine go:linkname visibility certificate: nil function") + } + canonical := u.canonicalAlias(fn) + if canonical == nil { + return certificate, false, fmt.Errorf("coroutine go:linkname visibility certificate: function has cyclic canonical aliases") + } + if _, required := u.required[canonical]; !required { + return certificate, false, fmt.Errorf("coroutine go:linkname visibility certificate: function %q is absent from the frozen emission universe", canonical.Name()) + } + certificate, certified = u.goLinknameVisibility[canonical] + return certificate, certified, nil +} diff --git a/cl/coro_linkname_visibility_test.go b/cl/coro_linkname_visibility_test.go new file mode 100644 index 0000000000..d6765aa9e2 --- /dev/null +++ b/cl/coro_linkname_visibility_test.go @@ -0,0 +1,271 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroSysctlCPUFixture = `package cpu +import _ "unsafe" + +func sysctlbynameInt32(name []byte) (int32, int32) +func sysctlbynameBytes(name, out []byte) int32 + +//go:linkname sysctlEnabled +func sysctlEnabled(name []byte) bool { + return len(name) != 0 +} +` + +const coroSysctlRuntimeFixture = `package runtimebridge +import "unsafe" + +//llgo:coro sync +//go:linkname cSysctlbyname C.sysctlbyname +func cSysctlbyname(name *byte, oldp unsafe.Pointer, oldlenp *uintptr, newp unsafe.Pointer, newlen uintptr) int32 + +//go:linkname internalCPUSysctlbynameInt32 internal/cpu.sysctlbynameInt32 +func internalCPUSysctlbynameInt32(name []byte) (int32, int32) { + return cSysctlbyname(nil, nil, nil, nil, 0), 0 +} + +//go:linkname internalCPUSysctlbynameBytes internal/cpu.sysctlbynameBytes +func internalCPUSysctlbynameBytes(name, out []byte) int32 { + return cSysctlbyname(nil, nil, nil, nil, 0) +} +` + +const coroSysctlConsumerFixture = `package consumer +//go:linkname linkedSysctlEnabled internal/cpu.sysctlEnabled +func linkedSysctlEnabled(name []byte) bool +func Root(name []byte) bool { return linkedSysctlEnabled(name) } +` + +func TestCoroGoLinknameVisibilitySysctlBridgeNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + cpuPkg := testProg.addPackage(t, "internal/cpu", coroSysctlCPUFixture) + runtimePkg := testProg.addPackage(t, "example.com/runtimebridge", coroSysctlRuntimeFixture) + consumerPkg := testProg.addPackage(t, "example.com/consumer", coroSysctlConsumerFixture) + testProg.ssa.Build() + + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + var prog llssa.Program + if test.target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, test.target) + } + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{ + {SSA: cpuPkg.ssa, Files: []*ast.File{cpuPkg.file}}, + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: consumerPkg.ssa, Files: []*ast.File{consumerPkg.file}}, + }) + if err != nil { + t.Fatal(err) + } + + sysctlEnabled := cpuPkg.ssa.Func("sysctlEnabled") + declInt32 := cpuPkg.ssa.Func("sysctlbynameInt32") + defInt32 := runtimePkg.ssa.Func("internalCPUSysctlbynameInt32") + linked := consumerPkg.ssa.Func("linkedSysctlEnabled") + root := consumerPkg.ssa.Func("Root") + cSysctl := runtimePkg.ssa.Func("cSysctlbyname") + if resolved, ok := universe.Resolve(declInt32); !ok || resolved != defInt32 { + t.Fatalf("bodyless sysctl bridge resolution = %v, %t; want %v", resolved, ok, defInt32) + } + if resolved, ok := universe.Resolve(linked); !ok || resolved != sysctlEnabled { + t.Fatalf("bodyless visibility consumer resolution = %v, %t; want %v", resolved, ok, sysctlEnabled) + } + visibility, certified, err := universe.coroGoLinknameVisibilityCertificate(sysctlEnabled) + if err != nil || !certified || visibility.ID == "" || visibility.PhysicalSymbol != "internal/cpu.sysctlEnabled" || visibility.ABISignature == "" { + t.Fatalf("sysctl visibility certificate = %+v, %t, %v", visibility, certified, err) + } + syncCertificate, syncCertified, err := universe.CoroForeignSyncCertificate(cSysctl) + if err != nil || !syncCertified || syncCertificate.ID == "" { + t.Fatalf("sysctl C sync certificate = %+v, %t, %v", syncCertificate, syncCertified, err) + } + + ssaUniverse, err := coro.NewSSAEmissionUniverse(testProg.ssa, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(testProg.ssa, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ResolveFunction: func(fn *ssa.Function) (*ssa.Function, bool, error) { + canonical, ok := universe.Resolve(fn) + return canonical, ok, nil + }, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == sysctlEnabled { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + certificate, ok, err := universe.CoroForeignSyncCertificate(fn) + if err != nil { + return coro.SSAFunctionPolicy{}, err + } + if ok { + return coro.SSAFunctionPolicy{ + Effect: coro.NoSuspend, Exec: coro.IRQUnsafe, + External: coro.ExternalKnown, OverrideExternal: true, IgnoreBody: true, + ForeignSyncCertificate: certificate.ID, + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + if err := universe.ValidatePlanCoverage(plan); err != nil { + t.Fatal(err) + } + sysctlPlan, ok := plan.FunctionPlan(sysctlEnabled) + if !ok || sysctlPlan.Emission != coro.EmitCoroutine || sysctlPlan.RawPlainEntry || plan.HasRawPlainVariant(sysctlEnabled) { + t.Fatalf("sysctl plan = %+v, present=%t raw-variant=%t; want one managed coroutine body", sysctlPlan, ok, plan.HasRawPlainVariant(sysctlEnabled)) + } + if err := validateCoroPhysicalABIWithUniverse(sysctlEnabled, sysctlPlan, plan, universe, true, true); err != nil { + t.Fatalf("visibility-only physical ABI: %v", err) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + for name, fixture := range map[string]emissionTestPackage{"cpu": cpuPkg, "consumer": consumerPkg} { + compiled, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, fixture.ssa, []*ast.File{fixture.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile %s: %v", name, err) + } + module := compiled.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify %s: %v\n%s", name, err, module.String()) + } + if name == "cpu" { + if raw := module.NamedFunction("internal/cpu.sysctlEnabled"); !raw.IsNil() { + t.Fatalf("visibility-only function emitted a raw body:\n%s", raw.String()) + } + if managed := module.NamedFunction("internal/cpu.sysctlEnabled" + coroPrimarySuffix); managed.IsNil() { + t.Fatalf("visibility-only coroutine body is absent:\n%s", module.String()) + } + } else { + rootIR := module.NamedFunction("example.com/consumer.Root" + coroPrimarySuffix).String() + if !strings.Contains(rootIR, "internal/cpu.sysctlEnabled$coro") || strings.Contains(rootIR, "internal/cpu.sysctlEnabled\"(") { + t.Fatalf("paired consumer did not select managed sysctl entry:\n%s", rootIR) + } + } + runCoroABITestPipeline(t, prog, module) + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit %s object: %v", name, err) + } + if len(object.Bytes()) == 0 { + object.Dispose() + t.Fatalf("%s object is empty", name) + } + object.Dispose() + } + }) + } +} + +func TestCoroGoLinknameVisibilityRejectsNonPlainShapes(t *testing.T) { + for _, test := range []struct { + name string + source string + find func(*ssa.Package) *ssa.Function + }{ + {name: "redirecting two argument", source: `package bad +//go:linkname Visible example.com/elsewhere.Visible +func Visible() {} +`, find: func(pkg *ssa.Package) *ssa.Function { return pkg.Func("Visible") }}, + {name: "additional export", source: `package bad +//go:linkname Visible +//export Visible +func Visible() {} +`, find: func(pkg *ssa.Package) *ssa.Function { return pkg.Func("Visible") }}, + {name: "variadic", source: `package bad +//go:linkname Visible +func Visible(...int) {} +`, find: func(pkg *ssa.Package) *ssa.Function { return pkg.Func("Visible") }}, + {name: "generic", source: `package bad +//go:linkname Visible +func Visible[T any](T) {} +`, find: func(pkg *ssa.Package) *ssa.Function { return pkg.Func("Visible") }}, + {name: "method receiver", source: `package bad +type T struct{} +//go:linkname T.Visible +func (T) Visible() {} +func Root() { T{}.Visible() } +`, find: func(pkg *ssa.Package) *ssa.Function { + named := pkg.Pkg.Scope().Lookup("T").Type() + selection := pkg.Prog.MethodSets.MethodSet(named).Lookup(pkg.Pkg, "Visible") + return pkg.Prog.MethodValue(selection) + }}, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, test.source) + fn := test.find(ssaPkg) + if fn == nil { + t.Fatal("fixture function is absent") + } + if directive, ok := attachedGoLinknameVisibilityDirective(fn); ok || directive != "" { + t.Fatalf("visibility source proof = %q, %t; want rejected", directive, ok) + } + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + if universe.Contains(fn) { + if certificate, certified, err := universe.coroGoLinknameVisibilityCertificate(fn); err != nil || certified || certificate.ID != "" { + t.Fatalf("frozen visibility certificate = %+v, %t, %v; want absent", certificate, certified, err) + } + } + }) + } +} diff --git a/cl/coro_lowered_call.go b/cl/coro_lowered_call.go new file mode 100644 index 0000000000..af740dbb20 --- /dev/null +++ b/cl/coro_lowered_call.go @@ -0,0 +1,149 @@ +/* + * 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 ( + "fmt" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" +) + +// resolveCoroLoweredRuntimeCall replaces one rtFunc call with the exact +// physical entry frozen for the current SSA owner. Missing or divergent input +// is a compiler-plan error: falling back to the legacy symbol would recreate a +// hidden call edge after the whole-program fixed point was sealed. +func (p *context) resolveCoroLoweredRuntimeCall(b llssa.Builder, helper string, marker llssa.Expr, args []llssa.Expr) (llssa.Expr, bool) { + if p.compilation == nil { + return llssa.Nil, false + } + if p.emissionUniverse == nil || !p.emissionUniverse.CompleteRuntimeABI() { + // Isolated package/report tests do not carry the production runtime ABI + // and must keep the legacy rtFunc marker. internal/build always prepares + // a complete universe for active entry resolution, where every missing + // owner-scoped mapping remains a hard compiler-plan error below. + return llssa.Nil, false + } + if p.goFn == nil || p.emissionUniverse == nil || p.compilation.CoroPlan == nil { + panic("coroutine lowered runtime call requires an exact owner, emission universe, and SSA plan") + } + if b.Func != p.fn { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q escaped into another LLVM function", helper, p.goFn.Name())) + } + p.observeCoroSiteRuntimeHelper(helper) + + frozenCall, ok, err := p.emissionUniverse.ResolveCoroLoweredCallRecord(p.goFn, helper) + if err != nil { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q: %w", helper, p.goFn.Name(), err)) + } + target := frozenCall.Target + rawPlainOccurrence := ok && frozenCall.RawPlain + plainOnly := false + if !ok && p.coroBody() == nil { + target, ok, err = p.emissionUniverse.ResolveCoroPlainLoweredCall(p.goFn, helper) + if err != nil { + panic(fmt.Errorf("coroutine plain lowered runtime call %q in %q: %w", helper, p.goFn.Name(), err)) + } + plainOnly = ok + } + if !ok || target == nil { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q is absent from the frozen emission universe", helper, p.goFn.Name())) + } + if !plainOnly { + plannedCall, planned := p.compilation.CoroPlan.ResolveLoweredCallRecord(p.goFn, helper) + if !planned || plannedCall.Target != target || plannedCall.RawPlain != rawPlainOccurrence || + plannedCall.UnwindOnly != frozenCall.UnwindOnly || + plannedCall.ExplicitStatusElided != frozenCall.ExplicitStatusElided { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q disagrees between the frozen emission universe and SSA plan", helper, p.goFn.Name())) + } + } + targetPlan, planned := p.compilation.CoroPlan.FunctionPlan(target) + if !planned { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q targets an unplanned function", helper, p.goFn.Name())) + } + sourceSig, err := p.emissionUniverse.coroPhysicalSourceSignature(target) + if err != nil { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q: derive target %q signature: %w", helper, p.goFn.Name(), targetPlan.ID, err)) + } + markerSig, ok := types.Unalias(marker.RawType()).(*types.Signature) + if !ok { + panic(fmt.Errorf( + "coroutine lowered runtime call %q in %q target %q marker has non-signature type %T (%v)", + helper, p.goFn.Name(), targetPlan.ID, types.Unalias(marker.RawType()), marker.RawType(), + )) + } + // x/tools SSA has already packed a variadic invocation into the final + // slice argument. The frozen physical source signature deliberately clears + // that source-only flag, so compare the compiler-created rtFunc marker in + // the same normalized domain. This does not relax named-type identity or + // any transported parameter/result type. + markerSig = coroPhysicalNormalizeSourceSignature(markerSig) + if !types.Identical(markerSig, sourceSig) { + panic(fmt.Errorf( + "coroutine lowered runtime call %q in %q target %q has a different effective source signature: marker=%s target=%s", + helper, p.goFn.Name(), targetPlan.ID, + types.TypeString(markerSig, types.RelativeTo(nil)), + types.TypeString(sourceSig, types.RelativeTo(nil)), + )) + } + if plainOnly || rawPlainOccurrence { + if !targetPlan.RawPlainDemand || !p.compilation.CoroPlan.HasRawPlainVariant(target) { + panic(fmt.Errorf("coroutine raw/plain lowered runtime call %q in %q targets %q without an exact raw-plain variant", helper, p.goFn.Name(), targetPlan.ID)) + } + fn, _, kind := p.compileRawPlainFunction(target) + if fn == nil || kind != goFunc && kind != cFunc { + panic(fmt.Errorf("coroutine raw/plain lowered runtime call %q in %q target %q did not resolve to a raw-callable Go/C entry", helper, p.goFn.Name(), targetPlan.ID)) + } + return b.Call(fn.Expr, args...), true + } + if p.rawPlainBody { + if targetPlan.Emission == coro.EmitCoroutine && !p.compilation.CoroPlan.HasRawPlainVariant(target) { + panic(fmt.Errorf("coroutine lowered runtime call %q in raw plain body %q targets managed coroutine %q without a raw plain variant", helper, p.goFn.Name(), targetPlan.ID)) + } + fn, _, kind := p.compileRawPlainFunction(target) + if fn == nil || kind != goFunc && kind != cFunc { + panic(fmt.Errorf("coroutine lowered runtime call %q in raw plain body %q target %q did not resolve to a raw-callable Go/C entry", helper, p.goFn.Name(), targetPlan.ID)) + } + return b.Call(fn.Expr, args...), true + } + + switch targetPlan.Emission { + case coro.EmitPlain: + if targetPlan.External != coro.Defined || targetPlan.Demand == coro.NoDemand || targetPlan.Effect.MaySuspend() || targetPlan.FuncRep == coro.DirectCoro { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q cannot call suspending target %q through a plain entry", helper, p.goFn.Name(), targetPlan.ID)) + } + fn, _, kind := p.compileFunction(target) + if fn == nil || kind != goFunc { + panic(fmt.Errorf("coroutine lowered runtime call %q in %q target %q did not resolve to a Go entry", helper, p.goFn.Name(), targetPlan.ID)) + } + return b.Call(fn.Expr, args...), true + case coro.EmitCoroutine: + return p.compileCoroTargetAwait(b, target, args), true + case coro.EmitRawPlain: + panic(fmt.Errorf( + "coroutine lowered runtime call %q in managed body %q targets raw-plain-only function %q without a managed entry", + helper, p.goFn.Name(), targetPlan.ID, + )) + case coro.EmitNone: + panic(fmt.Errorf("coroutine lowered runtime call %q in %q targets non-emitted function %q", helper, p.goFn.Name(), targetPlan.ID)) + case coro.EmitExternal: + panic(fmt.Errorf("coroutine lowered runtime call %q in %q requires an unimplemented external helper adapter for %q", helper, p.goFn.Name(), targetPlan.ID)) + default: + panic(fmt.Errorf("coroutine lowered runtime call %q in %q targets function %q with invalid emission %d", helper, p.goFn.Name(), targetPlan.ID, uint8(targetPlan.Emission))) + } +} diff --git a/cl/coro_lowering_facts.go b/cl/coro_lowering_facts.go new file mode 100644 index 0000000000..53c91da164 --- /dev/null +++ b/cl/coro_lowering_facts.go @@ -0,0 +1,387 @@ +/* + * 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 ( + "fmt" + "strconv" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// CoroLoweringFactsReport is a canonical snapshot built from one frozen +// emission universe and its completed whole-program plan. Active build-driver +// compilations install its digest into CoroPlanDigest and every archive cache +// identity; focused report-only callers may still build it independently. +type CoroLoweringFactsReport struct { + Facts coro.LoweringFacts + Digest string +} + +// BuildCoroLoweringFactsReport constructs the sparse lowering-fact projection +// associated with c. It performs no LLVM emission and changes no plan, cache, +// archive, or runtime artifact. +func (c *Compilation) BuildCoroLoweringFactsReport() (CoroLoweringFactsReport, error) { + if c == nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts require a compilation") + } + if c.CoroLoweringFacts.Schema != "" || c.CoroLoweringFactsDigest != "" { + if err := c.validateCoroLoweringFactsIdentity(); err != nil { + return CoroLoweringFactsReport{}, err + } + return CoroLoweringFactsReport{Facts: c.CoroLoweringFacts, Digest: c.CoroLoweringFactsDigest}, nil + } + if c.CoroPlan == nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts require a frozen CoroPlan") + } + if c.EmissionUniverse == nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts require a frozen emission universe") + } + return c.EmissionUniverse.BuildCoroLoweringFactsReport(c.CoroPlan) +} + +// BuildCoroLoweringFactsReport scans only exact functions and owner contexts +// already frozen in u. Incomplete runtime profiles are accepted for entry-only +// or helper-free compilations; any materialized hidden helper that lacks an +// exact frozen target still fails at its source site below. +func (u *EmissionUniverse) BuildCoroLoweringFactsReport(plan *coro.SSAPlan) (CoroLoweringFactsReport, error) { + if u == nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts require a frozen emission universe") + } + if plan == nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts require a frozen CoroPlan") + } + if u.prog == nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts require a frozen target program") + } + if err := u.ValidateCoroPlan(plan); err != nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts validate plan coverage: %w", err) + } + + functions := make([]coro.FunctionLoweringFacts, 0, len(u.functions)) + for _, function := range u.functions { + functionID, ok := plan.FunctionID(function) + if !ok { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts: function %q has no frozen FunctionID", function.Name()) + } + functionPlan, ok := plan.FunctionPlan(function) + if !ok { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts: function %q has no frozen FunctionPlan", function.Name()) + } + owners := u.sortedUseOwners(function) + if len(owners) == 0 { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts: function %q has no frozen owner", function.Name()) + } + for _, owner := range owners { + instance, err := u.coroLoweringFactsInstanceID(function, functionID, owner) + if err != nil { + return CoroLoweringFactsReport{}, err + } + sites, err := u.coroLoweringFunctionSites(plan, function, owner, instance) + if err != nil { + return CoroLoweringFactsReport{}, err + } + functions = append(functions, coro.FunctionLoweringFacts{ + Instance: instance, + LocalEffect: functionPlan.LocalEffect, + LocalExec: functionPlan.LocalExec, + Sites: sites, + }) + } + } + + facts := coro.NewLoweringFacts(functions) + canonical, err := facts.Canonical() + if err != nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts verify frozen ledger: %w", err) + } + digest, err := canonical.Digest() + if err != nil { + return CoroLoweringFactsReport{}, fmt.Errorf("coroutine lowering facts canonical digest: %w", err) + } + return CoroLoweringFactsReport{Facts: canonical, Digest: digest}, nil +} + +func (u *EmissionUniverse) coroLoweringFactsInstanceID(function *ssa.Function, functionID coro.FunctionID, owner *preparedEmissionPackage) (coro.EmissionInstanceID, error) { + if function == nil || owner == nil { + return coro.EmissionInstanceID{}, fmt.Errorf("coroutine lowering facts require an exact function owner") + } + if u.linkIdentities[function] == "" { + return coro.EmissionInstanceID{}, fmt.Errorf("coroutine lowering facts: function %q link identity is not frozen", function.Name()) + } + key := emissionFunctionOwnerKey{function: function, owner: owner} + kind, kindOK := u.functionKinds[key] + state, stateOK := u.ownerStates[function][owner] + if !kindOK || !stateOK { + return coro.EmissionInstanceID{}, fmt.Errorf("coroutine lowering facts: function %q owner %q has incomplete frozen provenance", function.Name(), owner.identity) + } + opcode := "" + if value, ok := u.intrinsicOps[key]; ok { + opcode = strconv.Itoa(value) + } + target := u.prog.TargetSpec() + context := emissionDigest(framedEmissionKey( + "cl-coro-lowering-context-v0", + target.Triple, + target.CPU, + target.Features, + target.TargetABI, + u.prog.DataLayout(), + strconv.Itoa(u.prog.PointerSize()*8), + strconv.FormatBool(u.completeRuntimeABI), + strconv.FormatBool(u.CoroChannelEnabled()), + owner.identity, + strconv.Itoa(kind), + strconv.Itoa(int(state.state)), + strconv.FormatBool(state.fromPatch), + u.finalKeys[key], + u.syntheticKeys[function], + opcode, + )) + instance, err := coro.NewEmissionInstanceID(functionID, owner.identity, context) + if err != nil { + return coro.EmissionInstanceID{}, fmt.Errorf("coroutine lowering facts: function %q owner %q instance: %w", function.Name(), owner.identity, err) + } + return instance, nil +} + +func (u *EmissionUniverse) coroLoweringFunctionSites(plan *coro.SSAPlan, function *ssa.Function, owner *preparedEmissionPackage, instance coro.EmissionInstanceID) ([]coro.LoweringFact, error) { + key := emissionFunctionOwnerKey{function: function, owner: owner} + if u.functionKinds[key] != goFunc || plan.IgnoresBody(function) || len(function.Blocks) == 0 { + return []coro.LoweringFact{}, nil + } + ctx, err := u.functionABIContext(function, owner) + if err != nil { + return nil, fmt.Errorf("coroutine lowering facts: function %q owner %q context: %w", function.Name(), owner.identity, err) + } + loweredCalls := make(map[string]coro.SSALoweredCall) + for _, call := range plan.LoweredCalls(function) { + loweredCalls[call.LogicalName] = call + } + + sites := make([]coro.LoweringFact, 0) + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + if _, unevaluated := ctx.unevaluatedSSA[instruction]; unevaluated { + continue + } + if _, debug := instruction.(*ssa.DebugRef); debug { + continue + } + fact, materialized, err := u.coroInstructionLoweringFact(ctx, plan, function, instance, instruction, loweredCalls) + if err != nil { + return nil, fmt.Errorf("coroutine lowering facts: function %q block %d: %w", function.Name(), block.Index, err) + } + if materialized { + sites = append(sites, fact) + } + } + } + return sites, nil +} + +func (u *EmissionUniverse) coroInstructionLoweringFact(ctx *context, plan *coro.SSAPlan, function *ssa.Function, instance coro.EmissionInstanceID, instruction ssa.Instruction, loweredCalls map[string]coro.SSALoweredCall) (coro.LoweringFact, bool, error) { + siteRole := coro.RolePrimary + contract := coro.ContractID("") + barrier := false + helperNames, err := u.coroProgramIR.plannedRuntimeHelpers(ctx, instruction) + if err != nil { + return coro.LoweringFact{}, false, fmt.Errorf("load frozen site helper plan: %w", err) + } + helpers := make([]coro.ManagedEdge, 0, len(helperNames)) + for _, logicalName := range helperNames { + if coroCompilerElidesImplicitFaultRuntimeHelper(instruction, logicalName) { + continue + } + planned, ok := loweredCalls[logicalName] + if !ok || planned.Target == nil { + return coro.LoweringFact{}, false, fmt.Errorf("instruction helper %q is absent from the frozen plan", logicalName) + } + targetID, ok := plan.FunctionID(planned.Target) + if !ok { + return coro.LoweringFact{}, false, fmt.Errorf("instruction helper %q target %q has no frozen FunctionID", logicalName, planned.Target.Name()) + } + helpers = append(helpers, coro.ManagedEdge{ + Order: len(helpers), + Role: coro.RoleHelper, + Ordinal: len(helpers), + LogicalName: logicalName, + Target: targetID, + UnwindOnly: planned.UnwindOnly, + ExplicitStatusElided: planned.ExplicitStatusElided, + }) + } + + semantic, err := u.coroProgramIR.semanticInstructionPlan(function, ctx.emissionOwner, instruction) + if err != nil { + return coro.LoweringFact{}, false, fmt.Errorf("load frozen semantic SitePlan: %w", err) + } + class, recipe, effect, exec, materialized := semantic.class, semantic.recipe, semantic.effect, semantic.exec, semantic.materialized + functionUses := []coro.FunctionValueFact{} + if store, ok := instruction.(*ssa.Store); ok { + if target, conditional := plan.ConditionalManagedStoreTarget(store); conditional { + if store.Parent() != function || target == nil { + return coro.LoweringFact{}, false, fmt.Errorf("conditional managed Store has no exact owner/target") + } + targetID, planned := plan.FunctionID(target) + if !planned { + return coro.LoweringFact{}, false, fmt.Errorf("conditional managed Store target %q has no frozen FunctionID", target.Name()) + } + class = coro.OpLowered + recipe = coro.RecipeID("cl.ssa.conditional-managed-store.publish.v0") + if plan.ElidesConditionalManagedStore(store) { + recipe = coro.RecipeID("cl.ssa.conditional-managed-store.elide.v0") + } + materialized = true + contract = coro.ContractID("llgo.coro.conditional-managed-publication.v0") + functionUses = []coro.FunctionValueFact{{ + Order: 0, Role: coro.RolePrimary, Ordinal: 0, + Targets: []coro.FunctionID{targetID}, Open: false, MayBeNil: false, + }} + } + } + implicitPanic := coroImplicitPanicFacts(helperNames) + if len(helpers) != 0 || len(implicitPanic) != 0 { + materialized = true + if !semantic.materialized { + class = coro.OpLowered + if len(helpers) == 0 { + recipe = coro.RecipeID("cl.ssa.implicit-fault-guard.v0") + } else { + recipe = coro.RecipeID("cl.ssa.hidden-helpers.v0") + } + } + } + if call, ok := instruction.(ssa.CallInstruction); ok && call.Common() != nil { + if callee := call.Common().StaticCallee(); callee != nil { + if _, frozen := u.Resolve(callee); frozen { + semantics, intrinsic, err := coroIntrinsicCallSiteSemantics(u, call) + if err != nil { + return coro.LoweringFact{}, false, err + } + if intrinsic && semantics.ElidesManagedCall() { + if !plan.ElidesCall(call) { + return coro.LoweringFact{}, false, fmt.Errorf("elided intrinsic call is not elided by the frozen plan") + } + materialized = true + class = coro.OpIntrinsic + recipe, effect = coroIntrinsicLoweringRecipe(semantics) + if direct, ok := instruction.(*ssa.Call); ok { + role, critical, criticalErr := u.coroCriticalCallSite(direct) + if criticalErr != nil { + return coro.LoweringFact{}, false, criticalErr + } + if critical { + barrier = true + contract = coro.ContractID("llgo.coro.critical-depth.v1") + switch role { + case coroCriticalCallEnter: + siteRole = coro.RoleRegionBegin + recipe = coro.RecipeID("cl.intrinsic.coro-critical-enter.v1") + case coroCriticalCallExit: + siteRole = coro.RoleRegionEnd + recipe = coro.RecipeID("cl.intrinsic.coro-critical-exit.v1") + default: + return coro.LoweringFact{}, false, fmt.Errorf("critical intrinsic has no exact region role") + } + } + } + } + } + } + } + if !materialized { + return coro.LoweringFact{}, false, nil + } + + site, err := coro.NewInstructionEmissionSiteID(instance, instruction, siteRole, 0) + if err != nil { + return coro.LoweringFact{}, false, err + } + footprint := coro.BackendFootprint(0) + if len(helpers) != 0 { + footprint |= coro.FootprintManagedCall + } + if effect.MaySuspend() { + footprint |= coro.FootprintSuspend + } + if barrier { + footprint |= coro.FootprintBarrier + } + if exec.Contains(coro.MayUnwind) { + footprint |= coro.FootprintUnwind + } + if len(implicitPanic) != 0 { + footprint |= coro.FootprintPanic + } + if _, explicitPanic := instruction.(*ssa.Panic); explicitPanic { + footprint |= coro.FootprintPanic + } + return coro.LoweringFact{ + Site: site, + Class: class, + Recipe: recipe, + Effect: effect, + Exec: exec, + Footprint: footprint, + Helpers: helpers, + ImplicitPanic: implicitPanic, + FunctionUses: functionUses, + Contract: contract, + }, true, nil +} + +func coroIntrinsicLoweringRecipe(semantics CoroIntrinsicCallSemantics) (coro.RecipeID, coro.Effect) { + switch semantics { + case CoroIntrinsicCallInlineNoSuspend: + return coro.RecipeID("cl.intrinsic.inline-nosuspend.v0"), coro.NoSuspend + case CoroIntrinsicCallInlineWithLoweredCalls: + return coro.RecipeID("cl.intrinsic.inline-with-helpers.v0"), coro.NoSuspend + case CoroIntrinsicCallInlineSuspend: + return coro.RecipeID("cl.intrinsic.inline-suspend.v0"), coro.MayPark + case CoroIntrinsicCallInlineYield: + return coro.RecipeID("cl.intrinsic.inline-yield.v0"), coro.YieldOnly + default: + return coro.RecipeID("cl.intrinsic.unsupported.v0"), coro.NoSuspend + } +} + +func coroImplicitPanicFacts(helperNames []string) []coro.ImplicitPanicFact { + ret := make([]coro.ImplicitPanicFact, 0) + for _, helper := range helperNames { + kind := "" + switch helper { + case "AssertNilDeref", "AssertNilDerefPtr": + kind = "nil-deref" + case "CheckIndexRange": + kind = "index-range" + case "PanicSliceConvert": + kind = "slice-convert" + } + if kind == "" { + continue + } + ret = append(ret, coro.ImplicitPanicFact{ + Order: len(ret), + Role: coro.RolePanic, + Ordinal: len(ret), + Kind: kind, + }) + } + return ret +} diff --git a/cl/coro_lowering_facts_test.go b/cl/coro_lowering_facts_test.go new file mode 100644 index 0000000000..01e6f4e0af --- /dev/null +++ b/cl/coro_lowering_facts_test.go @@ -0,0 +1,457 @@ +//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 ( + "bytes" + "encoding/hex" + "go/ast" + "go/importer" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const coroLoweringFactsCallerSource = `package loweringfacts + +func AllocatePair(flag bool) (*int, *int) { + first := new(int) + if flag { + *first = 1 + } + second := new(int) + return first, second +} +` + +func TestCoroLoweringFactsReportIsStableSparseAndPreservesHelperSites(t *testing.T) { + plain, plainOwnerID, plainDebugRefs := buildCoroLoweringFactsTestReport(t, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) + debug, debugOwnerID, debugRefs := buildCoroLoweringFactsTestReport(t, ssa.SanityCheckFunctions|ssa.InstantiateGenerics|ssa.GlobalDebug) + if plainDebugRefs != 0 || debugRefs == 0 { + t.Fatalf("debug refs: plain=%d debug=%d", plainDebugRefs, debugRefs) + } + if plainOwnerID != debugOwnerID { + t.Fatalf("DebugRef changed owner FunctionID: %q != %q", plainOwnerID, debugOwnerID) + } + plainJSON, err := plain.Facts.CanonicalJSON() + if err != nil { + t.Fatal(err) + } + debugJSON, err := debug.Facts.CanonicalJSON() + if err != nil { + t.Fatal(err) + } + if !bytes.Equal(plainJSON, debugJSON) || plain.Digest != debug.Digest { + t.Fatalf("DebugRef changed lowering facts:\nplain %s %s\ndebug %s %s", plain.Digest, plainJSON, debug.Digest, debugJSON) + } + if len(plain.Digest) != 64 { + t.Fatalf("facts digest length = %d", len(plain.Digest)) + } + if _, err := hex.DecodeString(plain.Digest); err != nil { + t.Fatalf("facts digest is not canonical hexadecimal: %v", err) + } + + ownerFacts := loweringFactsFunctionByID(t, plain.Facts, plainOwnerID) + if ownerFacts.Instance.Owner != "caller-variant" { + t.Fatalf("owner identity = %q", ownerFacts.Instance.Owner) + } + if len(ownerFacts.Instance.Context) != 64 { + t.Fatalf("owner context = %q, want SHA-256 identity", ownerFacts.Instance.Context) + } + if _, err := hex.DecodeString(ownerFacts.Instance.Context); err != nil { + t.Fatalf("owner context is not hexadecimal: %v", err) + } + helperSites := 0 + var helperTarget coro.FunctionID + seenSites := make(map[coro.EmissionSiteID]bool) + for _, fact := range ownerFacts.Sites { + if seenSites[fact.Site] { + t.Fatalf("duplicate fact site %+v", fact.Site) + } + seenSites[fact.Site] = true + if fact.Site.Source.Kind != coro.SourceInstruction || fact.Site.Source.Function != plainOwnerID { + t.Fatalf("non-instruction or wrong-function fact site %+v", fact.Site) + } + for _, helper := range fact.Helpers { + if helper.LogicalName != "AllocZ" { + continue + } + helperSites++ + if helper.Order != 0 || helper.Ordinal != 0 || helper.Role != coro.RoleHelper { + t.Fatalf("AllocZ helper subsite = %+v", helper) + } + if helperTarget == "" { + helperTarget = helper.Target + } else if helper.Target != helperTarget { + t.Fatalf("AllocZ sites resolved different targets: %q and %q", helperTarget, helper.Target) + } + } + } + if helperSites != 2 { + t.Fatalf("AllocZ helper sites = %d, want two exact source occurrences; facts=%+v", helperSites, ownerFacts.Sites) + } + if len(ownerFacts.Sites) >= loweringFactsSemanticInstructionCount(t, coroLoweringFactsCallerSource) { + t.Fatalf("facts are not sparse: sites=%d", len(ownerFacts.Sites)) + } +} + +func TestCompilationBuildCoroLoweringFactsReportIsDeterministic(t *testing.T) { + report, _, _, compilation := buildCoroLoweringFactsTestFixture(t, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) + first, err := compilation.BuildCoroLoweringFactsReport() + if err != nil { + t.Fatal(err) + } + second, err := compilation.BuildCoroLoweringFactsReport() + if err != nil { + t.Fatal(err) + } + firstJSON, err := first.Facts.CanonicalJSON() + if err != nil { + t.Fatal(err) + } + secondJSON, err := second.Facts.CanonicalJSON() + if err != nil { + t.Fatal(err) + } + if first.Digest != report.Digest || first.Digest != second.Digest || !bytes.Equal(firstJSON, secondJSON) { + t.Fatalf("repeated report changed: initial=%q first=%q second=%q", report.Digest, first.Digest, second.Digest) + } + if err := first.Facts.Verify(); err != nil { + t.Fatalf("reported facts do not verify: %v", err) + } +} + +func TestCoroLoweringFactsRecordsConditionalManagedStoreDecision(t *testing.T) { + for _, test := range []struct { + name string + liveTarget bool + wantRecipe coro.RecipeID + }{ + {"dormant target", false, "cl.ssa.conditional-managed-store.elide.v0"}, + {"live target", true, "cl.ssa.conditional-managed-store.publish.v0"}, + } { + t.Run(test.name, func(t *testing.T) { + testProgram := newCoroLoweringFactsEmissionTestProgram(ssa.SanityCheckFunctions | ssa.InstantiateGenerics) + runtimePackage := testProgram.addPackage(t, llssa.PkgRuntime, `package runtime +func AllocZ(size uintptr) uintptr { return 0 } +`) + callerPackage := testProgram.addPackage(t, "example.com/emission/conditional-store", `package conditionalstore +var slot func() +func Target() {} +func Publish() { slot = Target } +func Live() { Target() } +`) + testProgram.ssa.Build() + publish := callerPackage.ssa.Func("Publish") + target := callerPackage.ssa.Func("Target") + var publication *ssa.Store + for _, block := range publish.Blocks { + for _, instruction := range block.Instrs { + if store, ok := instruction.(*ssa.Store); ok && store.Val == target { + publication = store + } + } + } + if publication == nil { + t.Fatal("Publish has no exact Target Store") + } + prog := newLLSSAProg(t) + t.Cleanup(prog.Dispose) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePackage.ssa, Files: []*ast.File{runtimePackage.file}, Identity: "runtime-variant"}, + {SSA: callerPackage.ssa, Files: []*ast.File{callerPackage.file}, Identity: "caller-variant"}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(testProgram.ssa, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + roots := coro.Roots{{Function: publish, Demand: coro.AsyncDemand}} + if test.liveTarget { + roots = append(roots, coro.Root{Function: callerPackage.ssa.Func("Live"), Demand: coro.AsyncDemand}) + } + plan, err := coro.AnalyzeSSA(testProgram.ssa, roots, coro.SSAConfig{ + FunctionIDs: functionIDs, EmissionUniverse: ssaUniverse, MaxPlainInstructions: -1, + ResolveFunction: func(function *ssa.Function) (*ssa.Function, bool, error) { + resolved, ok := universe.Resolve(function) + return resolved, ok, nil + }, + ClassifyConditionalManagedStoreReference: func(owner *ssa.Function, store *ssa.Store) (*ssa.Function, bool, error) { + if owner == publish && store == publication { + return target, true, nil + } + return nil, false, nil + }, + }) + if err != nil { + t.Fatal(err) + } + report, err := (&Compilation{CoroPlan: plan, EmissionUniverse: universe}).BuildCoroLoweringFactsReport() + if err != nil { + t.Fatal(err) + } + publishID, _ := plan.FunctionID(publish) + targetID, _ := plan.FunctionID(target) + facts := loweringFactsFunctionByID(t, report.Facts, publishID) + var matched []coro.LoweringFact + for _, fact := range facts.Sites { + if fact.Contract == "llgo.coro.conditional-managed-publication.v0" { + matched = append(matched, fact) + } + } + if len(matched) != 1 || matched[0].Recipe != test.wantRecipe || len(matched[0].FunctionUses) != 1 || + len(matched[0].FunctionUses[0].Targets) != 1 || matched[0].FunctionUses[0].Targets[0] != targetID { + t.Fatalf("conditional Store lowering facts = %+v", matched) + } + }) + } +} + +func TestCoroLoweringFactsReportCriticalRegionContract(t *testing.T) { + testProgram := newCoroLoweringFactsEmissionTestProgram(ssa.SanityCheckFunctions | ssa.InstantiateGenerics) + testProgram.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePackage := testProgram.addPackage(t, llssa.PkgRuntime, `package runtime`) + callerPackage := testProgram.addPackage(t, "example.com/emission/loweringfacts-critical", `package critical +import _ "unsafe" +//go:linkname enter llgo.coroCriticalEnter +func enter() +//go:linkname exit llgo.coroCriticalExit +func exit() +var cell uint32 +func Root(value uint32) uint32 { + enter() + cell = value + value = cell + exit() + return value +}`) + testProgram.ssa.Build() + prog := newLLSSAProg(t) + t.Cleanup(prog.Dispose) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePackage.ssa, Files: []*ast.File{runtimePackage.file}, Identity: "runtime-critical"}, + {SSA: callerPackage.ssa, Files: []*ast.File{callerPackage.file}, Identity: "caller-critical"}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + t.Fatal(err) + } + root := callerPackage.ssa.Func("Root") + ssaUniverse, err := coro.NewSSAEmissionUniverse(testProgram.ssa, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(testProgram.ssa, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + FunctionIDs: functionIDs, + EmissionUniverse: ssaUniverse, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, Exec: coro.NeedsPreempt}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + }) + if err != nil { + t.Fatal(err) + } + report, err := (&Compilation{CoroPlan: plan, EmissionUniverse: universe}).BuildCoroLoweringFactsReport() + if err != nil { + t.Fatal(err) + } + rootID, ok := plan.FunctionID(root) + if !ok { + t.Fatal("critical lowering-facts Root has no FunctionID") + } + facts := loweringFactsFunctionByID(t, report.Facts, rootID) + found := map[coro.SiteRole]coro.LoweringFact{} + for _, fact := range facts.Sites { + if fact.Site.Source.Role == coro.RoleRegionBegin || fact.Site.Source.Role == coro.RoleRegionEnd { + found[fact.Site.Source.Role] = fact + } + } + begin, beginOK := found[coro.RoleRegionBegin] + end, endOK := found[coro.RoleRegionEnd] + if !beginOK || begin.Recipe != "cl.intrinsic.coro-critical-enter.v1" || begin.Effect != coro.NoSuspend || + begin.Contract != "llgo.coro.critical-depth.v1" || !begin.Footprint.Contains(coro.FootprintBarrier) || begin.Footprint.Contains(coro.FootprintSuspend) { + t.Fatalf("critical begin fact = %+v, present=%t", begin, beginOK) + } + if !endOK || end.Recipe != "cl.intrinsic.coro-critical-exit.v1" || end.Effect != coro.YieldOnly || + end.Contract != "llgo.coro.critical-depth.v1" || + !end.Footprint.Contains(coro.FootprintBarrier|coro.FootprintSuspend) { + t.Fatalf("critical end fact = %+v, present=%t", end, endOK) + } +} + +func TestCoroLoweringFactsReportFailsClosedWithoutFrozenInputs(t *testing.T) { + var nilCompilation *Compilation + if _, err := nilCompilation.BuildCoroLoweringFactsReport(); err == nil || !strings.Contains(err.Error(), "compilation") { + t.Fatalf("nil Compilation error = %v", err) + } + if _, err := (&Compilation{}).BuildCoroLoweringFactsReport(); err == nil || !strings.Contains(err.Error(), "CoroPlan") { + t.Fatalf("missing plan error = %v", err) + } + + _, _, _, complete := buildCoroLoweringFactsTestFixture(t, ssa.SanityCheckFunctions|ssa.InstantiateGenerics) + testProgram := newCoroLoweringFactsEmissionTestProgram(ssa.SanityCheckFunctions | ssa.InstantiateGenerics) + caller := testProgram.addPackage(t, "example.com/emission/loweringfacts-incomplete", coroLoweringFactsCallerSource) + testProgram.ssa.Build() + prog := newLLSSAProg(t) + t.Cleanup(prog.Dispose) + incomplete, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{ + SSA: caller.ssa, Files: []*ast.File{caller.file}, Identity: "incomplete-caller", + }}) + if err != nil { + t.Fatal(err) + } + if _, err := incomplete.BuildCoroLoweringFactsReport(complete.CoroPlan); err == nil || !strings.Contains(err.Error(), "validate plan coverage") { + t.Fatalf("incomplete universe error = %v", err) + } +} + +func buildCoroLoweringFactsTestReport(t *testing.T, mode ssa.BuilderMode) (CoroLoweringFactsReport, coro.FunctionID, int) { + t.Helper() + report, ownerID, debugRefs, _ := buildCoroLoweringFactsTestFixture(t, mode) + return report, ownerID, debugRefs +} + +func buildCoroLoweringFactsTestFixture(t *testing.T, mode ssa.BuilderMode) (CoroLoweringFactsReport, coro.FunctionID, int, *Compilation) { + t.Helper() + testProgram := newCoroLoweringFactsEmissionTestProgram(mode) + runtimePackage := testProgram.addPackage(t, llssa.PkgRuntime, `package runtime +func AllocZ(size uintptr) uintptr { return 0 } +`) + callerPackage := testProgram.addPackage(t, "example.com/emission/loweringfacts", coroLoweringFactsCallerSource) + testProgram.ssa.Build() + prog := newLLSSAProg(t) + t.Cleanup(prog.Dispose) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePackage.ssa, Files: []*ast.File{runtimePackage.file}, Identity: "runtime-variant"}, + {SSA: callerPackage.ssa, Files: []*ast.File{callerPackage.file}, Identity: "caller-variant"}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + t.Fatal(err) + } + owner := callerPackage.ssa.Func("AllocatePair") + ssaUniverse, err := coro.NewSSAEmissionUniverse(testProgram.ssa, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(testProgram.ssa, coro.Roots{{Function: owner, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + FunctionIDs: functionIDs, + EmissionUniverse: ssaUniverse, + ClassifyLocalBody: universe.CoroLocalBodyFacts, + ResolveFunction: func(function *ssa.Function) (*ssa.Function, bool, error) { + resolved, ok := universe.Resolve(function) + return resolved, ok, nil + }, + MaxPlainInstructions: -1, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + }) + if err != nil { + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + report, err := compilation.BuildCoroLoweringFactsReport() + if err != nil { + t.Fatal(err) + } + ownerID, ok := plan.FunctionID(owner) + if !ok { + t.Fatal("owner has no FunctionID") + } + debugRefs := 0 + for _, block := range owner.Blocks { + for _, instruction := range block.Instrs { + if _, debug := instruction.(*ssa.DebugRef); debug { + debugRefs++ + } + } + } + return report, ownerID, debugRefs, compilation +} + +func newCoroLoweringFactsEmissionTestProgram(mode ssa.BuilderMode) *emissionTestProgram { + fset := token.NewFileSet() + return &emissionTestProgram{ + fset: fset, + ssa: ssa.NewProgram(fset, mode), + importer: &emissionTestImporter{ + packages: make(map[string]*types.Package), + fallback: importer.Default(), + }, + } +} + +func loweringFactsFunctionByID(t *testing.T, facts coro.LoweringFacts, id coro.FunctionID) coro.FunctionLoweringFacts { + t.Helper() + var matches []coro.FunctionLoweringFacts + for _, function := range facts.Functions { + if function.Instance.Function == id { + matches = append(matches, function) + } + } + if len(matches) != 1 { + t.Fatalf("facts for function %q = %d, want one owner instance", id, len(matches)) + } + return matches[0] +} + +func loweringFactsSemanticInstructionCount(t *testing.T, source string) int { + t.Helper() + program := newCoroLoweringFactsEmissionTestProgram(ssa.SanityCheckFunctions | ssa.InstantiateGenerics) + pkg := program.addPackage(t, "example.com/emission/loweringfacts-count", source) + program.ssa.Build() + count := 0 + for _, block := range pkg.ssa.Func("AllocatePair").Blocks { + for _, instruction := range block.Instrs { + if _, debug := instruction.(*ssa.DebugRef); !debug { + count++ + } + } + } + return count +} diff --git a/cl/coro_managed_dispatch_validate.go b/cl/coro_managed_dispatch_validate.go new file mode 100644 index 0000000000..0d2dd77d21 --- /dev/null +++ b/cl/coro_managed_dispatch_validate.go @@ -0,0 +1,178 @@ +/* + * 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 ( + "fmt" + "go/types" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// validateCoroManagedDispatchCall proves the source and plan half of the v1 +// universal {descriptor, environment} call contract. Capability ownership is +// intentionally checked by each physical consumer: this helper cannot turn a +// disabled frontend feature into an accepted lowering. +// +// UnknownManaged and UnknownForeign remain distinct fail-closed domains. Only +// UnknownManagedDispatch certifies that an open operand already has the +// universal descriptor representation. A closed Dispatch call needs no +// unknown-domain certificate: its exact descriptor targets were frozen by +// value flow, but it uses the same physical capability dispatch (notably when +// a callback parameter can carry both plain and coroutine producers). +func validateCoroManagedDispatchCall( + plan *coro.SSAPlan, + owner *ssa.Function, + call ssa.CallInstruction, + callPlan coro.SSACallPlan, + universes ...*EmissionUniverse, +) error { + return validateCoroManagedDispatchCallKind(plan, owner, call, callPlan, coro.CallDirect, universes...) +} + +func validateCoroManagedDispatchDefer( + plan *coro.SSAPlan, + owner *ssa.Function, + call *ssa.Defer, + callPlan coro.SSACallPlan, + universes ...*EmissionUniverse, +) error { + return validateCoroManagedDispatchCallKind(plan, owner, call, callPlan, coro.CallDefer, universes...) +} + +func validateCoroManagedDispatchCallKind( + plan *coro.SSAPlan, + owner *ssa.Function, + call ssa.CallInstruction, + callPlan coro.SSACallPlan, + expectedKind coro.CallKind, + universes ...*EmissionUniverse, +) error { + var universe *EmissionUniverse + if len(universes) != 0 { + universe = universes[0] + } + fail := func(format string, args ...any) error { + return coroPlainDispatchInstructionError(owner, call, fmt.Sprintf(format, args...)) + } + if plan == nil { + return fail("managed descriptor dispatch requires a compilation plan") + } + if call == nil { + return fail("managed descriptor dispatch requires one exact call instruction") + } + switch expectedKind { + case coro.CallDirect: + if direct, ordinary := call.(*ssa.Call); !ordinary || direct == nil { + return fail("managed descriptor dispatch is supported only for an ordinary direct call instruction") + } + if callPlan.Kind != coro.CallDirect { + return fail("managed descriptor dispatch requires an ordinary direct call instruction with a matching CallDirect plan") + } + case coro.CallDefer: + if deferred, ordinary := call.(*ssa.Defer); !ordinary || deferred == nil || deferred.DeferStack != nil { + return fail("managed descriptor cleanup requires one owner-local defer instruction") + } + if callPlan.Kind != coro.CallDefer { + return fail("managed descriptor cleanup requires one owner-local defer instruction with a matching CallDefer plan") + } + default: + return fail("managed descriptor dispatch has unsupported call kind %v", expectedKind) + } + common := call.Common() + if common == nil || common.StaticCallee() != nil || common.IsInvoke() || common.Method != nil { + return fail("managed descriptor dispatch requires an ordinary dynamic function call") + } + if _, builtin := common.Value.(*ssa.Builtin); builtin { + return fail("managed descriptor dispatch cannot target a builtin") + } + if callPlan.Rep != coro.Dispatch || callPlan.Transport != coro.ManagedTransport { + return fail("requires a managed Dispatch CallPlan, got transport=%s representation=%s", callPlan.Transport, callPlan.Rep) + } + if callPlan.SyncDispatch && expectedKind != coro.CallDefer { + return fail("synchronous descriptor CallPlan must use plain dispatch lowering") + } + if callPlan.Open && callPlan.Unresolved != coro.UnknownManagedDispatch { + return fail( + "open Dispatch CallPlan is not certified as UnknownManagedDispatch (unresolved=%v)", + callPlan.Unresolved, + ) + } + sig := common.Signature() + if sig == nil || sig.Recv() != nil || sig.Variadic() { + return fail("v1 descriptor requires an ordinary non-variadic function signature") + } + if params := sig.TypeParams(); params != nil && params.Len() != 0 { + return fail("v1 descriptor does not support generic signatures") + } + if params := sig.RecvTypeParams(); params != nil && params.Len() != 0 { + return fail("v1 descriptor does not support generic receiver signatures") + } + if err := validateCoroManagedDispatchSignatureShape(sig); err != nil { + return fail("v1 descriptor signature: %v", err) + } + + valuePlan, found := plan.ValuePlan(common.Value) + if !found || len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 || + valuePlan.Funcs[0].Rep != coro.Dispatch || valuePlan.Funcs[0].Transport != coro.ManagedTransport { + return fail("callee has no exact scalar Dispatch ValuePlan") + } + leaf := valuePlan.Funcs[0] + // ValuePlan contains the targets established by structural value flow. A + // field load or parameter can have an empty/strict-subset list while the + // exact call occurrence is closed by whole-program dynamic CHA. CallPlan is + // therefore authoritative for execution; every producer-known target must + // be present, but additional call-site candidates are valid descriptors. + if missing, ok := coroDispatchTargetsSubset(leaf.Targets, callPlan.Targets); !ok { + return fail("callee ValuePlan target %q is absent from CallPlan", missing) + } + if leaf.MayBeNil != callPlan.MayBeNil { + return fail("callee nilability %t conflicts with CallPlan nilability %t", leaf.MayBeNil, callPlan.MayBeNil) + } + + for _, targetID := range callPlan.Targets { + target, found := plan.Function(targetID) + if !found || target == nil { + return fail("target %q is absent from the compilation plan", targetID) + } + targetPlan, found := plan.FunctionPlan(target) + if !found || targetPlan.ID != targetID { + return fail("target %q has no canonical function plan", targetID) + } + if err := validateCoroDynamicDispatchTarget(target, targetPlan, universe); err != nil { + return fail("target %q: %v", targetID, err) + } + if target.Signature == nil || !types.Identical(sig, target.Signature) { + return fail("call signature %s does not match target %q signature %s", sig, targetID, target.Signature) + } + } + return nil +} + +func coroDispatchTargetsSubset(values, calls []coro.FunctionID) (coro.FunctionID, bool) { + callSet := make(map[coro.FunctionID]struct{}, len(calls)) + for _, target := range calls { + callSet[target] = struct{}{} + } + for _, target := range values { + if _, ok := callSet[target]; !ok { + return target, false + } + } + return "", true +} diff --git a/cl/coro_managed_dispatch_validate_test.go b/cl/coro_managed_dispatch_validate_test.go new file mode 100644 index 0000000000..7ba2ec4fbe --- /dev/null +++ b/cl/coro_managed_dispatch_validate_test.go @@ -0,0 +1,269 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +func TestCoroManagedDispatchValidationRequiresCapability(t *testing.T) { + fn, call, plan, functionPlan := buildCoroManagedDispatchValidationFixture( + t, `func Apply(callback func()) { callback() }`, coro.UnknownManagedDispatch, + ) + callPlan, ok := plan.CallPlan(call) + if !ok { + t.Fatal("managed dynamic call has no CallPlan") + } + if callPlan.Rep != coro.Dispatch || !callPlan.Open || callPlan.Unresolved != coro.UnknownManagedDispatch { + t.Fatalf("managed dynamic CallPlan = %+v, want open UnknownManagedDispatch", callPlan) + } + if functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Apply plan = %+v, want an await-structured coroutine", functionPlan) + } + if err := validateCoroManagedDispatchCall(plan, fn, call, callPlan); err != nil { + t.Fatalf("valid managed descriptor call rejected: %v", err) + } + + if err := validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + fn, functionPlan, plan, nil, true, false, false, false, "", false, false, false, + ); err == nil || !strings.Contains(err.Error(), "requires the v1 descriptor dispatch capability") { + t.Fatalf("physical gate-off error = %v", err) + } + if err := validateCoroPhysicalConsumersCapabilities(plan, nil, true, false, false); err == nil || + !strings.Contains(err.Error(), "requires the v1 descriptor dispatch capability") { + t.Fatalf("consumer gate-off error = %v", err) + } + + if err := validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + fn, functionPlan, plan, nil, true, false, false, false, "", false, true, false, + ); err != nil { + t.Fatalf("physical gate-on validation rejected managed descriptor call: %v", err) + } + if err := validateCoroPhysicalConsumersCapabilities(plan, nil, true, false, true); err != nil { + t.Fatalf("consumer gate-on validation rejected managed descriptor call: %v", err) + } + // validateCoroPlainDispatchConsumers is reached only when + // The stackless profile is active. It must recognize the same open call rather + // than routing it through the legacy closed/plain-only validator. + if err := validateCoroPlainDispatchConsumers(plan, nil, nil, nil); err != nil { + t.Fatalf("descriptor consumer validation rejected managed descriptor call: %v", err) + } +} + +func TestCoroManagedDispatchValidationTreatsCallPlanAsOccurrenceAuthority(t *testing.T) { + callTargets := []coro.FunctionID{"a", "b", "c"} + for _, test := range []struct { + name string + values []coro.FunctionID + want bool + }{ + {name: "empty structural set", want: true}, + {name: "strict structural subset", values: []coro.FunctionID{"a", "c"}, want: true}, + {name: "exact set", values: []coro.FunctionID{"a", "b", "c"}, want: true}, + {name: "producer outside occurrence", values: []coro.FunctionID{"a", "d"}}, + } { + t.Run(test.name, func(t *testing.T) { + missing, ok := coroDispatchTargetsSubset(test.values, callTargets) + if ok != test.want { + t.Fatalf("subset = %t, missing=%q, want %t", ok, missing, test.want) + } + if !ok && missing != "d" { + t.Fatalf("missing target = %q, want d", missing) + } + }) + } +} + +func TestCoroManagedDispatchValidationAllowsConstantDeadAwaitSeed(t *testing.T) { + fn, call, plan, functionPlan := buildCoroManagedDispatchValidationFixture(t, ` + const disabled = true + func Apply(callback func()) { + if disabled { return } + callback() + }`, coro.UnknownManagedDispatch, + ) + if !functionPlan.LocalEffect.Contains(coro.AwaitStructured) { + t.Fatalf("Apply local effect = %s, want conservative await seed from the dead SSA block", functionPlan.LocalEffect) + } + audit, err := newCoroPhysicalPureSSAAudit(nil, plan, fn, "") + if err != nil { + t.Fatal(err) + } + if audit.reachableBlocks[call.Block()] { + t.Fatal("constant-disabled managed call is physically reachable") + } + if err := validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + fn, functionPlan, plan, nil, true, false, false, false, "", false, true, false, + ); err != nil { + t.Fatalf("constant-dead managed await seed rejected: %v", err) + } +} + +func TestCoroManagedDispatchValidationKeepsUnknownDomainsFailClosed(t *testing.T) { + for _, unresolved := range []coro.UnknownTarget{coro.UnknownManaged, coro.UnknownForeign} { + t.Run(coroManagedDispatchUnknownName(unresolved), func(t *testing.T) { + fn, call, plan, functionPlan := buildCoroManagedDispatchValidationFixture( + t, `func Apply(callback func()) { callback() }`, unresolved, + ) + callPlan, ok := plan.CallPlan(call) + if !ok { + t.Fatal("dynamic call has no CallPlan") + } + if err := validateCoroManagedDispatchCall(plan, fn, call, callPlan); err == nil || + (!strings.Contains(err.Error(), "certified as UnknownManagedDispatch") && + !strings.Contains(err.Error(), "ordinary direct call instruction")) { + t.Fatalf("managed descriptor validator error = %v", err) + } + if err := validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + fn, functionPlan, plan, nil, true, true, false, false, "", false, true, false, + ); err == nil { + t.Fatalf("physical validator accepted unresolved domain %v: %v", unresolved, err) + } + if err := validateCoroPhysicalConsumersCapabilities(plan, nil, true, false, true); err == nil || + !strings.Contains(err.Error(), "uncertified execution domain") { + t.Fatalf("consumer validator accepted unresolved domain %v: %v", unresolved, err) + } + }) + } +} + +func TestCoroManagedDispatchValidationAcceptsStdlibCallShapes(t *testing.T) { + declarations := []string{ + `func Apply(callback func() error) error { return callback() }`, + `func Apply(callback func(int, []byte) (int, error), fd int, data []byte) (int, error) { + return callback(fd, data) + }`, + `func Apply(callback func(int) (int, error), fd int) (int, error) { return callback(fd) }`, + `type Conn interface { Close() error } + func Apply(callback func() (Conn, error)) (Conn, error) { return callback() }`, + `func Apply(callback func(string, any, *byte) (string, any, *byte), text string, value any, pointer *byte) (string, any, *byte) { + return callback(text, value, pointer) + }`, + } + for _, declaration := range declarations { + fn, call, plan, functionPlan := buildCoroManagedDispatchValidationFixture( + t, declaration, coro.UnknownManagedDispatch, + ) + callPlan, ok := plan.CallPlan(call) + if !ok { + t.Fatal("dynamic call has no CallPlan") + } + if err := validateCoroManagedDispatchCall(plan, fn, call, callPlan); err != nil { + t.Fatalf("stdlib-shaped v1 signature rejected: %v", err) + } + if err := validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + fn, functionPlan, plan, nil, true, false, false, false, "", false, true, false, + ); err != nil { + t.Fatalf("physical validator rejected stdlib-shaped signature: %v", err) + } + if err := validateCoroPhysicalConsumersCapabilities(plan, nil, true, false, true); err != nil { + t.Fatalf("consumer validator rejected stdlib-shaped signature: %v", err) + } + } +} + +func TestCoroManagedDispatchValidationAcceptsInlineNestedFunctionTransport(t *testing.T) { + fn, call, plan, functionPlan := buildCoroManagedDispatchValidationFixture( + t, `type Inline struct { Callback func() } + func Apply(callback func(Inline), value Inline) { callback(value) }`, coro.UnknownManagedDispatch, + ) + callPlan, ok := plan.CallPlan(call) + if !ok { + t.Fatal("dynamic call has no CallPlan") + } + if err := validateCoroManagedDispatchCall(plan, fn, call, callPlan); err != nil { + t.Fatalf("inline nested-function signature rejected: %v", err) + } + if err := validateCoroPhysicalABIWithUniverseCapabilitiesFrameRetentionAndChannel( + fn, functionPlan, plan, nil, true, false, false, false, "", false, true, false, + ); err != nil { + t.Fatalf("physical inline nested-function signature rejected: %v", err) + } + if err := validateCoroPhysicalConsumersCapabilities(plan, nil, true, false, true); err != nil { + t.Fatalf("consumer inline nested-function signature rejected: %v", err) + } +} + +func buildCoroManagedDispatchValidationFixture( + t *testing.T, + declaration string, + unresolved coro.UnknownTarget, +) (*ssa.Function, *ssa.Call, *coro.SSAPlan, coro.FunctionPlan) { + t.Helper() + ssaPkg, _, _ := buildGoSSAPkg(t, "package foo\n"+declaration) + fn := ssaPkg.Func("Apply") + call := onlyCoroManagedDispatchValidationCall(t, fn) + plan, err := coro.AnalyzeSSA( + ssaPkg.Prog, + coro.Roots{{Function: fn, Demand: coro.AsyncDemand}}, + coro.SSAConfig{ + MaxPlainInstructions: -1, + ClassifyUnknownCall: func(*ssa.Function, ssa.CallInstruction) (coro.UnknownTarget, error) { + return unresolved, nil + }, + }, + ) + if err != nil { + t.Fatal(err) + } + functionPlan, ok := plan.FunctionPlan(fn) + if !ok { + t.Fatal("Apply has no FunctionPlan") + } + return fn, call, plan, functionPlan +} + +func onlyCoroManagedDispatchValidationCall(t *testing.T, fn *ssa.Function) *ssa.Call { + t.Helper() + var found *ssa.Call + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + if _, builtin := call.Common().Value.(*ssa.Builtin); builtin { + continue + } + if found != nil { + t.Fatal("Apply contains more than one non-builtin call") + } + found = call + } + } + if found == nil { + t.Fatal("Apply contains no non-builtin call") + } + return found +} + +func coroManagedDispatchUnknownName(target coro.UnknownTarget) string { + switch target { + case coro.UnknownManaged: + return "managed" + case coro.UnknownForeign: + return "foreign" + default: + return "unknown" + } +} diff --git a/cl/coro_managed_heap_test.go b/cl/coro_managed_heap_test.go new file mode 100644 index 0000000000..2475e18970 --- /dev/null +++ b/cl/coro_managed_heap_test.go @@ -0,0 +1,515 @@ +//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 ( + "bytes" + "go/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/cl/blocks" + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroManagedHeapFixture = `package foo + +type Node struct { + Value uint32 + Next *Node +} + +type Empty struct{} + +var ObservedWritten int64 +var ObservedHandled bool + +func Child(value uint32) uint32 { return value } + +func Root(value uint32) *Node { + first := &Node{Value: value} + observed := Child(first.Value) + second := &Node{Value: observed} + first.Next = second + return first +} + +func Zero() *Empty { return &Empty{} } + +func CapturedResults(value uint32) (written int64, err error, handled bool, node *Node) { + defer func() { + ObservedWritten = written + _ = err + ObservedHandled = handled + }() + node = &Node{Value: value} + if value != 0 { + node = &Node{Value: value + 1} + } + for index := uint32(0); index < value; index++ { + node = &Node{Value: value + index} + } + written = int64(value) + value = Child(value) + handled = value != 0 + return +} + +func Conditional(value uint32, allocate bool) *Node { + if allocate { + return &Node{Value: value} + } + return nil +} + +func Loop(value, count uint32) *Node { + var last *Node + for index := uint32(0); index < count; index++ { + last = &Node{Value: value + index} + } + return last +} +` + +func TestCoroTerminalReconstructionAllocationSubset(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroManagedHeapFixture) + captured := ssaPkg.Func("CapturedResults") + selected, err := coroStaticTerminalReconstructionAllocations(captured) + if err != nil { + t.Fatal(err) + } + heap := coroManagedHeapAllocs(captured) + if len(selected) != 3 || len(heap) < 6 { + t.Fatalf("CapturedResults terminal/heap allocations = %d/%d, want 3/at least 6", len(selected), len(heap)) + } + selectedSet := make(map[*ssa.Alloc]struct{}, len(selected)) + for index, allocation := range selected { + selectedSet[allocation] = struct{}{} + if allocation != heap[index] || allocation.Block() == nil || allocation.Block().Index != 0 { + t.Fatalf("CapturedResults selected allocation %d = %v; want the same-order source-entry named-result heap cell", index, allocation) + } + } + infos := blocks.Infos(captured.Blocks) + ordinaryEntry, ordinaryBranch, ordinaryLoop := false, false, false + for _, allocation := range heap { + if _, selected := selectedSet[allocation]; selected { + continue + } + block := allocation.Block() + if block == nil { + t.Fatalf("ordinary CapturedResults heap allocation has no source block: %v", allocation) + } + switch { + case block.Index == 0: + ordinaryEntry = true + case block.Index >= 0 && block.Index < len(infos) && infos[block.Index].InLoop: + ordinaryLoop = true + default: + ordinaryBranch = true + } + } + if !ordinaryEntry || !ordinaryBranch || !ordinaryLoop { + t.Fatalf("CapturedResults ordinary heap coverage: entry=%t branch=%t loop=%t", ordinaryEntry, ordinaryBranch, ordinaryLoop) + } + for _, name := range []string{"Root", "Conditional", "Loop"} { + function := ssaPkg.Func(name) + allocations := coroManagedHeapAllocs(function) + if len(allocations) == 0 { + t.Fatalf("%s fixture has no ordinary heap allocation", name) + } + selected, err := coroStaticTerminalReconstructionAllocations(function) + if err != nil { + t.Fatalf("%s collector: %v", name, err) + } + if len(selected) != 0 { + t.Fatalf("%s ordinary entry/branch/loop allocations were selected as terminal results: %v", name, selected) + } + } +} + +func TestCoroManagedHeapAllocationNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, ssaPkg, files, universe, plan := prepareCoroManagedHeapTestPlan(t, test.target) + defer prog.Dispose() + root := ssaPkg.Func("Root") + zero := ssaPkg.Func("Zero") + captured := ssaPkg.Func("CapturedResults") + + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + proof := audit.currentFrameRetentionProof() + if got := proof.exactRootCapabilityProfile(); got != coroFrameRetentionExactRootProfileV2 { + t.Fatalf("managed-heap root profile = %q", got) + } + if got := proof.exactRootCapabilityDigest(); len(got) != 64 { + t.Fatalf("managed-heap root digest = %q", got) + } + heapAllocs := coroManagedHeapAllocs(root) + if len(heapAllocs) != 2 || len(proof.managedHeapAllocations) != 2 { + t.Fatalf("Root managed heap allocations: SSA=%d proof=%d, want 2/2", len(heapAllocs), len(proof.managedHeapAllocations)) + } + for _, allocation := range heapAllocs { + fact, managed := proof.managedHeapAllocations[allocation] + if !managed || fact.zeroSized || fact.helper != "AllocZ" || fact.helperTarget == "" { + t.Fatalf("managed allocation %q fact = %+v, present=%t", allocation, fact, managed) + } + if rootFact, rooted := proof.exactRoots[allocation]; !rooted || rootFact.kind != coroFrameRetentionRootManagedHeapAllocation { + t.Fatalf("managed allocation %q exact root = %+v, present=%t", allocation, rootFact, rooted) + } + if reason := audit.validateAlloc(allocation); reason != "" { + t.Fatalf("managed allocation %q rejected: %s", allocation, reason) + } + } + + pointerStore := false + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + store, ok := instruction.(*ssa.Store) + if !ok || !coroTypeContainsGCPointer(store.Val.Type(), make(map[types.Type]bool)) { + continue + } + addressRoot, reason := audit.stableAddressAt(store.Addr, store, make(map[ssa.Value]bool)) + if reason != "" || addressRoot != coroPhysicalAddressManagedHeap { + t.Fatalf("pointer store address root=%d reason=%q; want exact managed heap", addressRoot, reason) + } + if reason := audit.validateStore(store); reason != "" { + t.Fatalf("managed-heap pointer store rejected: %s", reason) + } + pointerStore = true + } + } + if !pointerStore { + t.Fatal("Root fixture has no pointer-containing managed-heap store") + } + + zeroAudit, err := newCoroPhysicalPureSSAAudit(universe, plan, zero, "") + if err != nil { + t.Fatal(err) + } + zeroProof := zeroAudit.currentFrameRetentionProof() + zeroAllocs := coroManagedHeapAllocs(zero) + if len(zeroAllocs) != 1 { + t.Fatalf("Zero heap allocations = %d, want 1", len(zeroAllocs)) + } + if fact, ok := zeroProof.managedHeapAllocations[zeroAllocs[0]]; !ok || !fact.zeroSized || fact.helper != "" { + t.Fatalf("zero-sized allocation fact = %+v, present=%t", fact, ok) + } + + capturedAudit, err := newCoroPhysicalPureSSAAudit(universe, plan, captured, "") + if err != nil { + t.Fatal(err) + } + capturedProof := capturedAudit.currentFrameRetentionProof() + cleanupPlan, err := prepareCoroStaticCleanupPlan(captured, plan, universe, "", true) + if err != nil { + t.Fatal(err) + } + if cleanupPlan == nil || len(cleanupPlan.terminalResultAllocations) != 3 || + !coroTerminalResultAllocationSetMatches(capturedProof, cleanupPlan.terminalResultAllocations) { + t.Fatalf("CapturedResults cleanup/proof terminal allocation sets disagree: plan=%v proof=%v", + cleanupPlan.terminalResultAllocations, capturedProof.terminalResultAllocations) + } + withoutTerminal := *capturedProof + withoutTerminal.terminalResultAllocations = make(map[*ssa.Alloc]struct{}) + withoutDigest := coroFrameRetentionRootDigest(capturedAudit, &withoutTerminal) + if withoutDigest == "" || withoutDigest == capturedProof.exactRootCapabilityDigest() { + t.Fatalf("terminal reconstruction subset is absent from frame proof digest: with=%q without=%q", + capturedProof.exactRootCapabilityDigest(), withoutDigest) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed-heap coroutine before CoroSplit: %v\n%s", err, module.String()) + } + rootPhysical := requireCoroPhysicalFunction(t, module, "foo.Root") + rootIR := rootPhysical.String() + if got := strings.Count(rootIR, "runtime.AllocZ"); got != 2 { + t.Fatalf("Root AllocZ calls = %d, want 2 ordinary managed allocations:\n%s", got, rootIR) + } + rampEntry := rootPhysical.EntryBasicBlock() + entryHeapCalls := 0 + for _, block := range rootPhysical.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || !strings.HasSuffix(instruction.CalledValue().Name(), "/runtime.AllocZ") { + continue + } + entryHeapCalls++ + if instruction.InstructionParent() == rampEntry { + t.Fatalf("ordinary Root AllocZ was incorrectly moved to the physical ramp entry:\n%s", rootIR) + } + } + } + if entryHeapCalls != 2 { + t.Fatalf("Root ordinary AllocZ calls = %d, want 2:\n%s", entryHeapCalls, rootIR) + } + for _, forbidden := range []string{"AllocRoot", "alloca %foo.Node"} { + if strings.Contains(rootIR, forbidden) { + t.Fatalf("Root managed allocation incorrectly uses %q:\n%s", forbidden, rootIR) + } + } + if !strings.Contains(rootIR, "foo.Child$coro") { + t.Fatalf("Root does not suspend through Child after its first allocation:\n%s", rootIR) + } + capturedPhysical := requireCoroPhysicalFunction(t, module, "foo.CapturedResults") + capturedIR := capturedPhysical.String() + capturedHeapAllocs := coroManagedHeapAllocs(captured) + if len(capturedHeapAllocs) < 6 { + t.Fatalf("CapturedResults SSA heap allocations = %d, want three named-result cells plus entry/branch/loop objects", len(capturedHeapAllocs)) + } + for _, allocation := range cleanupPlan.terminalResultAllocations { + if allocation.Block() == nil || allocation.Block().Index != 0 { + t.Fatalf("CapturedResults named-result allocation is outside SSA entry block: %s", allocation) + } + } + if got := strings.Count(capturedIR, "runtime.AllocZ"); got != len(capturedHeapAllocs) { + t.Fatalf("CapturedResults AllocZ calls = %d, want one per %d SSA heap allocations:\n%s", got, len(capturedHeapAllocs), capturedIR) + } + var publishBlock llvm.BasicBlock + for _, block := range capturedPhysical.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() == llvm.Call && instruction.CalledValue().Name() == coroFramePublishHookV1 { + publishBlock = instruction.InstructionParent() + } + } + } + if publishBlock.IsNil() { + t.Fatalf("CapturedResults has no PhysicalABIV1 frame publication:\n%s", capturedIR) + } + hoistedHeapCalls, ordinaryHeapCalls := 0, 0 + for _, block := range capturedPhysical.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call || !strings.HasSuffix(instruction.CalledValue().Name(), "/runtime.AllocZ") { + continue + } + if instruction.InstructionParent() == publishBlock { + hoistedHeapCalls++ + } else { + ordinaryHeapCalls++ + } + } + } + if hoistedHeapCalls != 3 || ordinaryHeapCalls != len(capturedHeapAllocs)-3 { + t.Fatalf("CapturedResults hoisted/ordinary AllocZ calls = %d/%d, want 3/%d:\n%s", + hoistedHeapCalls, ordinaryHeapCalls, len(capturedHeapAllocs)-3, capturedIR) + } + publish := strings.Index(capturedIR, "call void @"+coroFramePublishHookV1) + alloc := strings.Index(capturedIR, "runtime.AllocZ") + initialSuspend := strings.Index(capturedIR, "%coro.suspend = call i8 @llvm.coro.suspend") + if publish < 0 || alloc < 0 || initialSuspend < 0 || publish >= alloc || alloc >= initialSuspend { + t.Fatalf("CapturedResults terminal allocations are not ordered publish -> AllocZ -> initial suspend:\n%s", capturedIR) + } + if !strings.Contains(capturedIR, "foo.Child$coro") || !strings.Contains(capturedIR, "CapturedResults$1$coro") { + t.Fatalf("CapturedResults does not suspend through both body and captured cleanup:\n%s", capturedIR) + } + + runCoroABITestPipeline(t, prog, module) + post := module.String() + if strings.Contains(post, "AllocRoot") { + t.Fatalf("CoroSplit changed managed allocation identity to AllocRoot:\n%s", post) + } + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() { + t.Fatal("CoroSplit did not emit foo.Root$coro.resume") + } + ramp := module.NamedFunction("foo.Root$coro") + if ramp.IsNil() { + t.Fatal("CoroSplit lost foo.Root$coro ramp") + } + rampIR := ramp.String() + resumeIR := resume.String() + if got := strings.Count(resumeIR, "runtime.AllocZ"); got != 2 || + strings.Contains(rampIR, "runtime.AllocZ") || + !strings.Contains(resumeIR, "foo.Child$coro") || !strings.Contains(resumeIR, ".reload") || + !strings.Contains(resumeIR, "store ptr") { + t.Fatalf("CoroSplit moved ordinary Root AllocZ calls out of resume (resume AllocZ=%d):\nramp:\n%s\nresume:\n%s", + got, rampIR, resumeIR) + } + capturedRamp := module.NamedFunction("foo.CapturedResults$coro") + capturedResume := module.NamedFunction("foo.CapturedResults$coro.resume") + if capturedRamp.IsNil() || capturedResume.IsNil() || + strings.Count(capturedRamp.String(), "runtime.AllocZ") != 3 || + strings.Count(capturedResume.String(), "runtime.AllocZ") != len(capturedHeapAllocs)-3 || + !strings.Contains(capturedResume.String(), ".reload") { + t.Fatalf("CoroSplit did not keep three result AllocZ calls in the ramp and %d ordinary calls in resume:\nramp:\n%s\nresume:\n%s", + len(capturedHeapAllocs)-3, + capturedRamp.String(), capturedResume.String()) + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit managed-heap coroutine object: %v\n%s", err, post) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte("foo.Root$coro")) { + t.Fatal("managed-heap object lost the Root coroutine symbol") + } + }) + } +} + +func TestCoroManagedHeapAllocationRejectsPreciseShadowProfile(t *testing.T) { + prog, ssaPkg, _, universe, plan := prepareCoroManagedHeapTestPlan(t, nil) + defer prog.Dispose() + root := ssaPkg.Func("Root") + old := emitShadowStackInstrumentation + emitShadowStackInstrumentation = true + defer func() { emitShadowStackInstrumentation = old }() + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "") + if err != nil { + t.Fatal(err) + } + proof := audit.currentFrameRetentionProof() + if proof.exactRootCapabilityProfile() != "" || len(proof.managedHeapAllocations) != 0 || len(proof.exactRetainedRoots()) != 0 { + t.Fatalf("precise/shadow profile received managed heap roots: profile=%q managed=%d roots=%d", + proof.exactRootCapabilityProfile(), len(proof.managedHeapAllocations), len(proof.exactRetainedRoots())) + } + allocations := coroManagedHeapAllocs(root) + if len(allocations) == 0 || !strings.Contains(audit.validateAlloc(allocations[0]), "non-moving conservative-or-no-GC") { + t.Fatalf("precise/shadow managed allocation rejection = %q", audit.validateAlloc(allocations[0])) + } +} + +func prepareCoroManagedHeapTestPlan(t *testing.T, target *llssa.Target) ( + llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan, +) { + t.Helper() + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, `package runtime +import "unsafe" +func AllocZ(size uintptr) unsafe.Pointer { + if size == 0 { return nil } + return nil +} +func AllocU(size uintptr) unsafe.Pointer { + if size == 0 { return nil } + return nil +} +`) + fooPkg := testProg.addPackage(t, "foo", coroManagedHeapFixture) + testProg.ssa.Build() + ssaPkg := fooPkg.ssa + files := []*ast.File{fooPkg.file} + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: ssaPkg, Files: files}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + root, zero, child, captured := ssaPkg.Func("Root"), ssaPkg.Func("Zero"), ssaPkg.Func("Child"), ssaPkg.Func("CapturedResults") + var capturedCleanup *ssa.Function + for _, block := range captured.Blocks { + for _, instruction := range block.Instrs { + deferred, ok := instruction.(*ssa.Defer) + if !ok { + continue + } + closure, _ := deferred.Common().Value.(*ssa.MakeClosure) + capturedCleanup, _ = closure.Fn.(*ssa.Function) + break + } + if capturedCleanup != nil { + break + } + } + if capturedCleanup == nil { + prog.Dispose() + t.Fatal("CapturedResults fixture has no exact captured cleanup target") + } + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: root, Demand: coro.AsyncDemand}, + {Function: zero, Demand: coro.AsyncDemand}, + {Function: captured, Demand: coro.AsyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == child || fn == capturedCleanup { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, ssaPkg, files, universe, plan +} + +func coroManagedHeapAllocs(fn *ssa.Function) []*ssa.Alloc { + var allocations []*ssa.Alloc + if fn == nil { + return allocations + } + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + if allocation, ok := instruction.(*ssa.Alloc); ok && allocation.Heap { + allocations = append(allocations, allocation) + } + } + } + return allocations +} diff --git a/cl/coro_managed_interface.go b/cl/coro_managed_interface.go new file mode 100644 index 0000000000..6a4f47eae7 --- /dev/null +++ b/cl/coro_managed_interface.go @@ -0,0 +1,609 @@ +/* + * 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 ( + "crypto/sha256" + "encoding/hex" + "fmt" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const coroManagedInterfaceRawTrapPrefix = "__llgo_coro_method_raw_trap_v1." + +// coroManagedInterfaceDispatchPlan freezes the exact method families whose +// ABI Method.Ifn_ word uses the universal {descriptor, receiver-environment} +// transport. The stackless profile uses this transport for both closed and +// open managed invokes: ABI type data has one Ifn_ word per concrete method, +// independent of the source call site, and a second receiver-aware plain path +// would split panic/outcome semantics. +type coroManagedInterfaceDispatchPlan struct { + calls map[ssa.CallInstruction]struct{} + methods map[string]struct{} + targets map[coro.FunctionID]*ssa.Function +} + +func (p *coroManagedInterfaceDispatchPlan) acceptsCall(call ssa.CallInstruction) bool { + if p == nil || call == nil { + return false + } + _, ok := p.calls[call] + return ok +} + +func (p *coroManagedInterfaceDispatchPlan) acceptsMethod(method *types.Func, signature *types.Signature) bool { + if p == nil { + return false + } + _, ok := p.methods[coroManagedInterfaceMethodKey(method, signature)] + return ok +} + +func (p *coroManagedInterfaceDispatchPlan) acceptsTarget(fn *ssa.Function, plan coro.FunctionPlan) bool { + if p == nil || fn == nil { + return false + } + target, ok := p.targets[plan.ID] + return ok && target == fn +} + +func coroManagedInterfaceMethodKey(method *types.Func, signature *types.Signature) string { + if method == nil || signature == nil { + return "" + } + callable := coroInterfaceDispatchCanonicalSignature(coroInterfaceDispatchCallableSignature(signature)) + if callable == nil { + return "" + } + return method.Id() + "\x00" + structuralEmissionABITypeKey(callable) +} + +func coroManagedInterfaceInvokeMethodKey( + universe *EmissionUniverse, owner *ssa.Function, call ssa.CallInstruction, +) (string, error) { + if owner == nil || call == nil || call.Common() == nil { + return "", fmt.Errorf("managed interface descriptor requires an exact owner and call") + } + common := call.Common() + if common.StaticCallee() != nil || !common.IsInvoke() || common.Method == nil { + return "", fmt.Errorf("managed interface descriptor requires an ordinary interface invoke") + } + signature, err := coroInterfaceDispatchEffectiveCallableSignature(universe, owner, common.Signature()) + if err != nil { + return "", err + } + key := coroManagedInterfaceMethodKey(common.Method, signature) + if key == "" { + return "", fmt.Errorf("managed interface descriptor has no exact method/signature key") + } + return key, nil +} + +func analyzeCoroManagedInterfaceDispatchPlan( + plan *coro.SSAPlan, universe *EmissionUniverse, enabled bool, +) (*coroManagedInterfaceDispatchPlan, error) { + result := &coroManagedInterfaceDispatchPlan{ + calls: make(map[ssa.CallInstruction]struct{}), + methods: make(map[string]struct{}), + targets: make(map[coro.FunctionID]*ssa.Function), + } + if plan == nil { + return nil, fmt.Errorf("managed interface descriptor requires a compilation plan") + } + // First freeze every emitted managed method family. Open families retain + // their explicit UnknownManagedInterfaceDispatch proof. Closed families are + // resolved to exact candidates and publish the same universal descriptor + // transport, so plain and coroutine targets share one receiver-aware path. + for _, owner := range plan.Functions() { + if owner.Function == nil || (owner.Plan.Emission != coro.EmitPlain && owner.Plan.Emission != coro.EmitCoroutine) { + continue + } + for _, block := range owner.Function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if !ok || plan.ElidesCall(call) { + continue + } + callPlan, found := plan.CallPlan(call) + if !found || call.Common() == nil || !call.Common().IsInvoke() || callPlan.Rep != coro.Dispatch { + continue + } + if !enabled { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + "managed interface descriptor transport is disabled") + } + common := call.Common() + key, err := coroManagedInterfaceInvokeMethodKey(universe, owner.Function, call) + if err != nil { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, err.Error()) + } + if callPlan.Open { + if callPlan.Unresolved != coro.UnknownManagedInterfaceDispatch { + continue + } + if err := validateCoroManagedInterfaceDispatchCall(plan, universe, owner.Function, call, callPlan); err != nil { + return nil, err + } + } else { + direct, ok := call.(*ssa.Call) + if !ok { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + "managed closed interface descriptor requires an ordinary call") + } + dispatch, err := resolveCoroInterfaceDispatchPlan(plan, universe, direct) + if err != nil { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + "managed closed interface descriptor: "+err.Error()) + } + for _, candidate := range dispatch.candidates { + if previous := result.targets[candidate.id]; previous != nil && previous != candidate.function { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("managed interface target %q resolves to both %q and %q", candidate.id, previous.Name(), candidate.function.Name())) + } + result.targets[candidate.id] = candidate.function + } + } + result.methods[key] = struct{}{} + result.calls[call] = struct{}{} + + // An open managed invoke can retain a bounded set of exact CHA + // candidates in addition to its UnknownManagedInterfaceDispatch + // tail. Some candidates are not otherwise materialized in ABI type + // data (for example, a dead promoted wrapper), but the planner still + // demands their bodies conservatively. Freeze those exact receiver + // targets here so entry validation uses the existing method + // descriptor/receiver-environment ABI rather than misrouting them + // through the receiver-free function-value descriptor validator. + iface, ok := types.Unalias(common.Value.Type()).Underlying().(*types.Interface) + if !ok { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("managed interface receiver %s is not an interface", common.Value.Type())) + } + iface.Complete() + sourceSignature, err := coroInterfaceDispatchSourceSignature(common) + if err != nil { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, err.Error()) + } + for _, targetID := range callPlan.Targets { + target, found := plan.Function(targetID) + if !found || target == nil { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("managed interface target %q is absent from the compilation plan", targetID)) + } + targetPlan, found := plan.FunctionPlan(target) + if !found || targetPlan.ID != targetID { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("managed interface target %q has no exact function plan", targetID)) + } + if _, _, _, err := validateCoroInterfaceDispatchCandidate( + common, iface, sourceSignature, universe, owner.Function, + targetID, target, targetPlan, + ); err != nil { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, err.Error()) + } + if previous := result.targets[targetID]; previous != nil && previous != target { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("managed interface target %q resolves to both %q and %q", targetID, previous.Name(), target.Name())) + } + result.targets[targetID] = target + } + } + } + } + + if len(result.methods) == 0 { + return result, nil + } + // Then bind every source invoke of those method families to the one physical + // transport. An open call in another execution domain cannot safely share an + // Ifn_ word and therefore fails before LLVM emission. + for _, owner := range plan.Functions() { + if owner.Function == nil || (owner.Plan.Emission != coro.EmitPlain && owner.Plan.Emission != coro.EmitCoroutine) { + continue + } + for _, block := range owner.Function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if !ok || plan.ElidesCall(call) || call.Common() == nil || !call.Common().IsInvoke() { + continue + } + key, err := coroManagedInterfaceInvokeMethodKey(universe, owner.Function, call) + if err != nil { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, err.Error()) + } + if _, required := result.methods[key]; !required { + continue + } + callPlan, found := plan.CallPlan(call) + if !found || callPlan.Rep != coro.Dispatch { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + "managed interface method family has no Dispatch CallPlan") + } + if callPlan.Open && callPlan.Unresolved != coro.UnknownManagedInterfaceDispatch { + return nil, coroLeafInstructionError(owner.Function, owner.Plan, instruction, + fmt.Sprintf("managed interface method family has conflicting open domain %v", callPlan.Unresolved)) + } + result.calls[call] = struct{}{} + } + } + } + return result, nil +} + +func validateCoroManagedInterfaceDispatchCall( + plan *coro.SSAPlan, + universe *EmissionUniverse, + owner *ssa.Function, + call ssa.CallInstruction, + callPlan coro.SSACallPlan, +) error { + fail := func(format string, args ...any) error { + return coroPlainDispatchInstructionError(owner, call, "managed interface descriptor: "+fmt.Sprintf(format, args...)) + } + direct, ordinary := call.(*ssa.Call) + if plan == nil || owner == nil || !ordinary || direct == nil || direct.Parent() != owner || direct.Common() == nil { + return fail("requires one exact ordinary call in the compilation plan") + } + common := direct.Common() + if callPlan.Call != call || callPlan.Kind != coro.CallDirect || callPlan.Rep != coro.Dispatch || + callPlan.SyncDispatch || !callPlan.Open || callPlan.Unresolved != coro.UnknownManagedInterfaceDispatch || + common.StaticCallee() != nil || !common.IsInvoke() || common.Method == nil { + return fail("requires an open UnknownManagedInterfaceDispatch CallPlan") + } + ownerPlan, ok := plan.FunctionPlan(owner) + if !ok || ownerPlan.Emission != coro.EmitCoroutine || ownerPlan.Primary != coro.PrimaryCoroutine { + return fail("owner plan present=%t emission=%s primary=%s demand=%s effect=%s exec=%s is not one coroutine primary", + ok, ownerPlan.Emission, ownerPlan.Primary, ownerPlan.Demand, ownerPlan.Effect, ownerPlan.Exec) + } + if !callPlan.MayBeNil { + return fail("open interface invoke lost its nil-interface check") + } + signature, err := coroInterfaceDispatchSourceSignature(common) + if err != nil { + return fail("signature: %v", err) + } + if err := validateCoroManagedDispatchSignatureShape(signature); err != nil { + return fail("signature: %v", err) + } + if _, err := coroManagedInterfaceInvokeMethodKey(universe, owner, call); err != nil { + return fail("signature: %v", err) + } + return nil +} + +func (p *context) tryCompileCoroManagedInterfaceDispatch( + b llssa.Builder, call *ssa.Call, +) (llssa.Expr, bool) { + if call == nil || call.Common() == nil || p.hasCoroPhysicalBody() || + p.compilation == nil || p.compilation.CoroPlan == nil || p.compilation.coroManagedInterface == nil { + return llssa.Nil, false + } + if !p.compilation.coroManagedInterface.acceptsCall(call) { + return llssa.Nil, false + } + callPlan, found := p.compilation.CoroPlan.CallPlan(call) + if !found || callPlan.Rep != coro.Dispatch { + panic("managed interface descriptor call lost its frozen Dispatch CallPlan") + } + if callPlan.Open { + if err := validateCoroManagedInterfaceDispatchCall( + p.compilation.CoroPlan, p.compilation.EmissionUniverse, p.goFn, call, callPlan, + ); err != nil { + panic(err) + } + } + signature, err := coroInterfaceDispatchSourceSignature(call.Common()) + if err != nil { + panic(err) + } + method, args := p.compileCoroManagedInterfaceOperands(b, call) + if callPlan.Open || coroDispatchCallHasCoroutineTarget(p.compilation.CoroPlan, callPlan) { + panic("managed interface descriptor requires a coroutine owner for an open or coroutine-capable target") + } + abi, err := newCoroPlainDispatchABI(p, signature) + if err != nil { + panic(fmt.Errorf("managed interface plain dispatch: %w", err)) + } + return b.CallCoroDispatchPlain(method, args, llssa.CoroDispatchCallOptions{ + Version: coroPlainDispatchVersion, + ABIHash: abi.hash, + Result: p.prog.Type(abi.resultSlotType, llssa.InC), + }), true +} + +func (p *context) compileCoroManagedInterfaceAwait( + b llssa.Builder, call *ssa.Call, instructionPlan coroPhysicalInstructionPlan, +) llssa.Expr { + if !p.hasCoroPhysicalBody() || call == nil || call.Common() == nil || + instructionPlan.control != coroPhysicalControlManagedInterfaceAwait || instructionPlan.controlSignature == nil { + panic("managed interface await escaped its frozen physical control recipe") + } + method, args := p.compileCoroManagedInterfaceOperands(b, call) + keepaliveSlots := p.compileCoroCallKeepaliveSlots(b, call) + return p.compileCoroManagedDispatchAwaitValue( + b, method, args, instructionPlan.controlSignature, keepaliveSlots, + ) +} + +func (p *context) compileCoroManagedInterfaceOperands( + b llssa.Builder, call *ssa.Call, +) (llssa.Expr, []llssa.Expr) { + common := call.Common() + p.recordCallerLocationForCall(b, &call.Call) + p.emitPCLineLabel(b, call.Pos()) + // Evaluate the interface receiver before arguments, exactly as the ordinary + // LLGo invoke path does. Imethod preserves the nil-interface panic and pairs + // the descriptor Ifn_ word with IfacePtrData as its receiver environment. + intf := p.compileValue(b, common.Value) + method := b.Imethod(intf, common.Method) + args := p.compileValues(b, call.Call.Args, fnNormal) + return method, args +} + +func (p *context) resolveInterfaceMethodSSA(method *types.Func, signature *types.Signature) *ssa.Function { + if method == nil || signature == nil || signature.Recv() == nil { + panic("coroutine interface method resolution requires a method and receiver signature") + } + selection := p.goProg.MethodSets.MethodSet(signature.Recv().Type()).Lookup(method.Pkg(), method.Name()) + if selection == nil { + panic(fmt.Errorf("coroutine interface method resolution: method %q is absent from receiver %s", method.Name(), signature.Recv().Type())) + } + fn := p.methodValue(selection) + if fn == nil { + panic(fmt.Errorf("coroutine interface method resolution: method %q has no SSA implementation", method.Name())) + } + return fn +} + +// resolveInterfaceMethodDescriptor is installed only for active coroutine +// compilation. It replaces an Ifn_ word iff preflight froze that exact method +// family as universal descriptor transport. Returning false preserves the +// legacy callable method word for every unrelated raw/foreign family. +func (p *context) resolveInterfaceMethodDescriptor( + _ string, method *types.Func, signature *types.Signature, +) (llssa.Expr, bool) { + if p.compilation == nil || p.compilation.coroManagedInterface == nil || signature == nil { + return llssa.Nil, false + } + patched, ok := p.patchType(signature).(*types.Signature) + if !ok || !p.compilation.coroManagedInterface.acceptsMethod(method, patched) { + return llssa.Nil, false + } + target := p.resolveInterfaceMethodSSA(method, signature) + descriptor, err := p.emitCoroManagedInterfaceMethodDescriptor(target, patched) + if err != nil { + panic(err) + } + return descriptor, true +} + +// resolveManagedInterfaceRawMethodSymbol preserves the independent raw-method +// address domain while a method family's Ifn_ uses universal descriptor +// transport. A real RawPlainEntry selects its separately planned legacy body. +// Without that capability, Tfn_ receives a signature-correct trap stub rather +// than an invalid call to the coroutine primary. +func (p *context) resolveManagedInterfaceRawMethodSymbol( + method *types.Func, signature *types.Signature, +) (string, bool) { + if p.compilation == nil || p.compilation.coroManagedInterface == nil || signature == nil { + return "", false + } + patched, ok := p.patchType(signature).(*types.Signature) + if !ok || !p.compilation.coroManagedInterface.acceptsMethod(method, patched) { + return "", false + } + target := p.resolveInterfaceMethodSSA(method, signature) + entry := p.mustFunctionSymbol(target) + if entry.plan.Emission != coro.EmitCoroutine { + return entry.name, true + } + if entry.plan.RawPlainEntry { + if err := validatePlannedRawPlainEntry(entry.function, entry.plan); err != nil { + panic(err) + } + return p.mustRawPlainFunctionSymbol(target).name, true + } + key := sha256.Sum256([]byte(string(entry.plan.ID) + "\x00" + structuralEmissionABITypeKey(patched))) + name := coroManagedInterfaceRawTrapPrefix + hex.EncodeToString(key[:16]) + // ABI method tables for one concrete type may be materialized in several + // package archives. The content-addressed trap is therefore a coalescible + // definition, just like its descriptor, rather than a package-owned strong + // symbol. + stub := p.pkg.NewFuncEx(name, patched, llssa.InGo, false, true) + if !stub.HasBody() { + body := stub.MakeBody(1) + trap := p.pkg.NewFunc( + "llvm.trap", types.NewSignatureType(nil, nil, nil, nil, nil, false), llssa.InC, + ) + body.Call(trap.Expr) + body.Unreachable() + body.EndBuild() + body.Dispose() + } + return name, true +} + +func (p *context) emitCoroManagedInterfaceMethodDescriptor( + target *ssa.Function, interfaceEntrySignature *types.Signature, +) (llssa.Expr, error) { + if p == nil || p.compilation == nil || p.compilation.CoroPlan == nil || target == nil { + return llssa.Nil, fmt.Errorf("managed interface descriptor requires an exact target and compilation plan") + } + entry := p.mustFunctionSymbol(target) + logicalSignature := coroInterfaceDispatchCanonicalSignature(coroInterfaceDispatchCallableSignature(interfaceEntrySignature)) + if err := validateCoroManagedInterfaceDescriptorTarget( + entry.function, entry.plan, p.compilation.EmissionUniverse, logicalSignature, + ); err != nil { + return llssa.Nil, err + } + // interfaceEntrySignature was already patched by + // resolveInterfaceMethodDescriptor. Keep this exact effective signature as + // the descriptor/thunk ABI instead of rebuilding its interface graph again. + abi, err := newCoroPlainDispatchEffectiveABI(p, logicalSignature) + if err != nil { + return llssa.Nil, fmt.Errorf("managed interface descriptor target %q: %w", entry.plan.ID, err) + } + // A method descriptor always publishes the managed primary selected by its + // frozen FunctionPlan. ABI type data may be materialized while the current + // owner is a separately emitted raw-plain body; inheriting rawPlainBody here + // would incorrectly ask the descriptor target for a legacy variant which it + // neither needs nor is required to have. + physical, py, kind := p.compileManagedFunction(entry.function) + if kind != goFunc || physical == nil || py != nil { + return llssa.Nil, fmt.Errorf("managed interface descriptor target %q did not compile as one Go function", entry.plan.ID) + } + patchedTarget, ok := p.patchType(entry.function.Signature).(*types.Signature) + if !ok || patchedTarget.Recv() == nil { + return llssa.Nil, fmt.Errorf("managed interface descriptor target %q lost its receiver signature", entry.plan.ID) + } + receiver := patchedTarget.Recv().Type() + targetHash := sha256.Sum256([]byte(entry.plan.ID)) + targetKey := "method." + hex.EncodeToString(targetHash[:8]) + "." + hex.EncodeToString(abi.hash[:]) + descriptorName := coroPlainDispatchDescriptorPrefix + targetKey + if descriptor, found := p.coroPlainDescriptors[descriptorName]; found { + return descriptor, nil + } + flags := uint32(0) + var plainEntry, coroEntry llssa.Expr + switch entry.plan.Emission { + case coro.EmitPlain: + flags |= llssa.CoroDispatchFlagHasPlain + plainEntry = p.newCoroDynamicDispatchEntryThunk( + coroPlainDispatchThunkPrefix+targetKey, physical.Expr, abi, entry.plan.Emission, receiver, + ) + case coro.EmitCoroutine: + flags |= llssa.CoroDispatchFlagHasCoro + coroEntry = p.newCoroDynamicDispatchEntryThunk( + coroCoroDispatchThunkPrefix+targetKey, physical.Expr, abi, entry.plan.Emission, receiver, + ) + default: + return llssa.Nil, fmt.Errorf("managed interface descriptor target %q has unsupported emission %s", entry.plan.ID, entry.plan.Emission) + } + // The descriptor environment is the dynamic receiver supplied by + // IfacePtrData, so NoCapture must remain clear even for a top-level method. + descriptor := p.pkg.NewCoroDispatchDescriptor(descriptorName, llssa.CoroDispatchDescriptorOptions{ + Version: coroPlainDispatchVersion, + Flags: flags, + ABIHash: abi.hash, + Signature: abi.signature, + PlainEntry: plainEntry, + CoroEntry: coroEntry, + Result: p.prog.Type(abi.resultSlotType, llssa.InC), + }) + if p.coroPlainDescriptors == nil { + p.coroPlainDescriptors = make(map[string]llssa.Expr) + } + p.coroPlainDescriptors[descriptorName] = descriptor + return descriptor, nil +} + +func validateCoroManagedInterfaceDescriptorTarget( + target *ssa.Function, + functionPlan coro.FunctionPlan, + universe *EmissionUniverse, + logicalSignature *types.Signature, +) error { + fail := func(format string, args ...any) error { + name := "" + if target != nil { + name = target.String() + } + return fmt.Errorf("managed interface descriptor target %q (%s): %s", name, functionPlan.ID, fmt.Sprintf(format, args...)) + } + if target == nil || target.Signature == nil || target.Signature.Recv() == nil || len(target.Blocks) == 0 || len(target.FreeVars) != 0 { + return fail("requires one defined non-capturing receiver body") + } + if functionPlan.External != coro.Defined || functionPlan.Demand == coro.NoDemand { + return fail("requires a demanded defined body, got external=%s representation=%s demand=%s", + functionPlan.External, functionPlan.FuncRep, functionPlan.Demand) + } + if functionPlan.Effect.IsOpaque() || functionPlan.Exec.IsOpaque() || + functionPlan.Exec.Contains(coro.BlockForeign|coro.ThreadAffine) { + return fail("opaque/foreign/thread-affine policy cannot publish a managed capability, got effect=%s exec=%s", + functionPlan.Effect, functionPlan.Exec) + } + switch functionPlan.Emission { + case coro.EmitPlain: + // The receiver-aware method descriptor is an ABI-type use, not a + // receiver-free Go function value. A method which has no other dynamic + // consumer therefore keeps DirectPlain while this exact Ifn_ word gains a + // descriptor thunk; a real function-value/invoke consumer is independently + // frozen as Dispatch by SSA analysis. + if functionPlan.FuncRep != coro.DirectPlain && functionPlan.FuncRep != coro.Dispatch { + return fail("plain capability has incompatible representation %s", functionPlan.FuncRep) + } + if functionPlan.Primary != coro.PrimaryPlain || functionPlan.Effect != coro.NoSuspend || + functionPlan.Exec.Contains(coro.NeedsPreempt) { + return fail("plain capability is not exact bounded no-suspend, got primary=%s effect=%s exec=%s", + functionPlan.Primary, functionPlan.Effect, functionPlan.Exec) + } + case coro.EmitCoroutine: + // DirectCoro is valid for the same reason: the ABI method descriptor wraps + // the one coroutine primary and does not manufacture a second body. + // BothDemand/RawPlainEntry still publishes only that managed primary; the + // raw alternate remains reachable solely through its exact raw consumers. + if functionPlan.FuncRep != coro.DirectCoro && functionPlan.FuncRep != coro.Dispatch { + return fail("coroutine capability has incompatible representation %s", functionPlan.FuncRep) + } + if functionPlan.Primary != coro.PrimaryCoroutine || !functionPlan.Demand.Contains(coro.AsyncDemand) || + !functionPlan.Effect.MaySuspend() { + return fail("coroutine capability has primary=%s demand=%s effect=%s", + functionPlan.Primary, functionPlan.Demand, functionPlan.Effect) + } + default: + return fail("unsupported emission %s", functionPlan.Emission) + } + if target.Signature.Variadic() || typeParamCount(target.Signature.TypeParams()) != 0 || + typeParamCount(target.Signature.RecvTypeParams()) != 0 || len(target.TypeArgs()) != 0 || target.Origin() != nil { + return fail("variadic or generic method ABI is not implemented") + } + directive, err := coroRawABIDirective(target, universe) + if err != nil { + return fail("classify ABI directive: %v", err) + } + if directive != "" { + return fail("ABI directive %q requires an explicit boundary adapter", directive) + } + if logicalSignature == nil || logicalSignature.Recv() != nil { + return fail("missing receiver-free logical signature") + } + if universe == nil { + return fail("requires a prepared emission universe") + } + effective, err := universe.coroPhysicalSourceSignature(target) + if err != nil { + return fail("derive effective target signature: %v", err) + } + if effective == nil || effective.Params().Len() == 0 { + return fail("effective target signature has no receiver parameter") + } + params := make([]*types.Var, effective.Params().Len()-1) + for i := range params { + params[i] = effective.Params().At(i + 1) + } + targetLogical := coroInterfaceDispatchCanonicalSignature(types.NewSignatureType( + nil, nil, nil, types.NewTuple(params...), effective.Results(), effective.Variadic(), + )) + if !coroInterfaceDispatchSignaturesIdentical(logicalSignature, targetLogical) { + return fail("logical signature %s does not match effective target signature %s", logicalSignature, targetLogical) + } + return nil +} diff --git a/cl/coro_method_test.go b/cl/coro_method_test.go new file mode 100644 index 0000000000..79fe437c21 --- /dev/null +++ b/cl/coro_method_test.go @@ -0,0 +1,367 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroStaticMethodSource = `package foo + +var gate chan uint32 + +type Counter struct { value uint32 } + +func (counter Counter) Plain(delta uint32) uint32 { + return counter.value + delta +} + +func (counter *Counter) PlainPointer(delta uint32) uint32 { + return delta + 2 +} + +func (counter Counter) WaitValue(delta uint32) uint32 { + received := <-gate + return counter.value + delta + received +} + +func (counter *Counter) WaitPointer(delta uint32) uint32 { + received := <-gate + return delta + received +} + +func Root(counter Counter, pointer *Counter) uint32 { + received := <-gate + first := counter.Plain(received) + second := pointer.PlainPointer(first) + third := counter.WaitValue(second) + return pointer.WaitPointer(third) +} +` + +func TestCoroStaticMethodReceiverABIPlainAndAwaitCoroSplit(t *testing.T) { + prog, pkg, universe, plan, ssaPkg, methods := compileCoroStaticMethodFixture(t, coroStaticMethodSource, coro.DynamicCHAOpen) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + root := ssaPkg.Func("Root") + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.FuncRep != coro.DirectCoro || + !rootPlan.Effect.Contains(coro.MayPark|coro.AwaitStructured) { + t.Fatalf("Root plan = %+v, present=%t; want parking child-await coroutine", rootPlan, ok) + } + for _, spec := range []struct { + name string + emission coro.BodyEmission + represent coro.FuncRep + coroutine bool + pointerRec bool + }{ + {name: "Plain", emission: coro.EmitPlain, represent: coro.DirectPlain}, + {name: "PlainPointer", emission: coro.EmitPlain, represent: coro.DirectPlain, pointerRec: true}, + {name: "WaitValue", emission: coro.EmitCoroutine, represent: coro.DirectCoro, coroutine: true}, + {name: "WaitPointer", emission: coro.EmitCoroutine, represent: coro.DirectCoro, coroutine: true, pointerRec: true}, + } { + method := methods[spec.name] + if method == nil || method.Signature == nil || method.Signature.Recv() == nil { + t.Fatalf("method %s is absent or has no declared receiver", spec.name) + } + _, isPointer := types.Unalias(method.Signature.Recv().Type()).(*types.Pointer) + if isPointer != spec.pointerRec { + t.Fatalf("method %s pointer receiver=%t, want %t", spec.name, isPointer, spec.pointerRec) + } + methodPlan, found := plan.FunctionPlan(method) + if !found || methodPlan.Emission != spec.emission || methodPlan.FuncRep != spec.represent { + t.Fatalf("method %s plan = %+v, present=%t", spec.name, methodPlan, found) + } + sourceSig, err := universe.coroPhysicalSourceSignature(method) + if err != nil { + t.Fatalf("method %s effective physical signature: %v", spec.name, err) + } + if sourceSig.Recv() != nil || sourceSig.Params().Len() != len(method.Params) { + t.Fatalf("method %s normalized signature = %v, SSA params=%d", spec.name, sourceSig, len(method.Params)) + } + for index, parameter := range method.Params { + if !types.Identical(sourceSig.Params().At(index).Type(), parameter.Type()) { + t.Fatalf("method %s normalized parameter %d %s != SSA parameter %s", spec.name, index, sourceSig.Params().At(index).Type(), parameter.Type()) + } + } + + name := funcName(ssaPkg.Pkg, method, false) + if spec.coroutine { + entry := plannedFunctionSymbol{function: method, plan: methodPlan, planned: true} + abiContext := &context{prog: prog, compilation: coroStaticMethodCompilation(plan, universe)} + fromDeclared := newCoroPhysicalABI(abiContext, entry, method.Signature) + fromNormalized := newCoroPhysicalABI(abiContext, entry, sourceSig) + if fromDeclared.hash != fromNormalized.hash || fromDeclared.descriptorName != fromNormalized.descriptorName || + !types.Identical(fromDeclared.physicalSig, fromNormalized.physicalSig) || + !types.Identical(fromDeclared.resultSlotType, fromNormalized.resultSlotType) { + t.Fatalf("method %s declared and normalized physical ABI/hash disagree", spec.name) + } + name += coroPrimarySuffix + ramp := module.NamedFunction(name) + if ramp.IsNil() { + t.Fatalf("method %s has no physical coroutine ramp %q:\n%s", spec.name, name, module.String()) + } + if got, want := ramp.ParamsCount(), sourceSig.Params().Len()+2; got != want { + t.Fatalf("method %s physical params=%d, want hidden+normalized=%d", spec.name, got, want) + } + } else if fn := module.NamedFunction(name); fn.IsNil() || fn.ParamsCount() != sourceSig.Params().Len() { + t.Fatalf("plain method %s did not keep the ordinary receiver-first declaration ABI", spec.name) + } + } + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify static method coroutine before CoroSplit: %v\n%s", err, module.String()) + } + rootIR := requireCoroPhysicalFunction(t, module, "foo.Root").String() + for _, name := range []string{"Plain", "PlainPointer", "WaitValue", "WaitPointer"} { + methodName := funcName(ssaPkg.Pkg, methods[name], false) + if strings.HasPrefix(name, "Wait") { + methodName += coroPrimarySuffix + } + if !strings.Contains(rootIR, methodName) { + t.Fatalf("Root does not call method entry %q:\n%s", methodName, rootIR) + } + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{"WaitValue", "WaitPointer"} { + resumeName := funcName(ssaPkg.Pkg, methods[name], false) + coroPrimarySuffix + ".resume" + if resume := module.NamedFunction(resumeName); resume.IsNil() { + t.Fatalf("CoroSplit did not create method resume %q:\n%s", resumeName, module.String()) + } + } + if rootResume := module.NamedFunction("foo.Root$coro.resume"); rootResume.IsNil() { + t.Fatalf("CoroSplit did not preserve Root method awaits:\n%s", module.String()) + } +} + +func TestCoroPointerReceiverInterfaceAwaitCoroSplit(t *testing.T) { + const source = `package foo +var gate chan uint32 +type Waiter interface { Wait() uint32 } +type Counter struct{} +func (*Counter) Wait() uint32 { return <-gate } +func Root(waiter Waiter) uint32 { <-gate; return waiter.Wait() } +` + prog, pkg, _, plan, ssaPkg, methods := compileCoroStaticMethodFixture(t, source, coro.DynamicCHAClosed) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + root := ssaPkg.Func("Root") + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.Primary != coro.PrimaryCoroutine || + !rootPlan.Effect.Contains(coro.MayPark|coro.AwaitStructured) { + t.Fatalf("Root plan = %+v, present=%t; want parking interface-await coroutine", rootPlan, ok) + } + wait := methods["Wait"] + waitPlan, ok := plan.FunctionPlan(wait) + if wait == nil || !ok || waitPlan.Emission != coro.EmitCoroutine || waitPlan.Primary != coro.PrimaryCoroutine || + waitPlan.FuncRep != coro.Dispatch { + t.Fatalf("Wait plan = %+v, present=%t; want coroutine Dispatch target", waitPlan, ok) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify pointer-receiver interface await before CoroSplit: %v\n%s", err, module.String()) + } + rootIR := requireCoroPhysicalFunction(t, module, "foo.Root").String() + waitName := funcName(ssaPkg.Pkg, wait, false) + coroPrimarySuffix + for _, required := range []string{"coro.dispatch", "call void @" + coroAwaitPrepareHookV1} { + if !strings.Contains(rootIR, required) { + t.Fatalf("pointer-receiver interface await lacks %q:\n%s", required, rootIR) + } + } + + runCoroABITestPipeline(t, prog, module) + if resume := module.NamedFunction("foo.Root$coro.resume"); resume.IsNil() { + t.Fatalf("CoroSplit did not create pointer-receiver interface await resume:\n%s", module.String()) + } + if waitResume := module.NamedFunction(waitName + ".resume"); waitResume.IsNil() { + t.Fatalf("CoroSplit did not create pointer-receiver method resume %q:\n%s", waitName+".resume", module.String()) + } +} + +func TestCoroStaticMethodReceiverABICompatibility(t *testing.T) { + tests := []struct { + name string + source string + resolution coro.DynamicResolution + want string + }{ + { + name: "bound method value", + source: `package foo +var gate chan uint32 +type Counter struct{} +func (Counter) Wait() uint32 { return <-gate } +func Root(counter Counter) uint32 { + <-gate + wait := counter.Wait + return wait() +} +`, + resolution: coro.DynamicCHAClosed, + want: "approved runtime helper(s) lack an exact coroutine-safe lowered-call plan: AllocU", + }, + { + name: "dynamic suspending interface", + source: `package foo +var gate chan uint32 +type Waiter interface { Wait() uint32 } +type Counter struct{} +func (Counter) Wait() uint32 { return <-gate } +func Root(waiter Waiter) uint32 { <-gate; return waiter.Wait() } +`, + resolution: coro.DynamicCHAClosed, + want: "", + }, + { + name: "variadic method", + source: `package foo +var gate chan uint32 +type Counter struct{} +func (Counter) Wait(values ...uint32) uint32 { <-gate; return uint32(len(values)) } +func Root(counter Counter) uint32 { <-gate; return counter.Wait(nil...) } +`, + resolution: coro.DynamicCHAOpen, + want: "", + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, test.source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, plan, methods, err := prepareCoroStaticMethodPlan(prog, ssaPkg, files, test.resolution) + var pkg llssa.Package + if err == nil { + pkg, _, err = NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: coroStaticMethodCompilation(plan, universe)}, + ) + } + if test.want == "" { + if err != nil { + t.Fatalf("compile supported static method ABI: %v", err) + } + if test.name == "variadic method" { + method := methods["Wait"] + if method == nil || method.Signature == nil || !method.Signature.Variadic() { + t.Fatalf("variadic method fixture lost its source signature: %v", method) + } + effective, err := universe.coroPhysicalSourceSignature(method) + if err != nil || effective == nil || effective.Variadic() { + t.Fatalf("variadic method effective signature = %v, %v; want packed non-variadic slice ABI", effective, err) + } + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify supported static/interface method: %v\n%s", err, module.String()) + } + return + } + if err == nil || !strings.Contains(strings.ToLower(err.Error()), strings.ToLower(test.want)) { + t.Fatalf("compile error = %v, want substring %q", err, test.want) + } + }) + } +} + +func compileCoroStaticMethodFixture(t *testing.T, source string, resolution coro.DynamicResolution) ( + llssa.Program, llssa.Package, *EmissionUniverse, *coro.SSAPlan, *ssa.Package, map[string]*ssa.Function, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + universe, plan, methods, err := prepareCoroStaticMethodPlan(prog, ssaPkg, files, resolution) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: coroStaticMethodCompilation(plan, universe)}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, universe, plan, ssaPkg, methods +} + +func prepareCoroStaticMethodPlan(prog llssa.Program, ssaPkg *ssa.Package, files []*ast.File, resolution coro.DynamicResolution) ( + *EmissionUniverse, *coro.SSAPlan, map[string]*ssa.Function, error, +) { + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}, EmissionUniverseOptions{CoroProfile: CoroProfileStackless}, + ) + if err != nil { + return nil, nil, nil, err + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + return nil, nil, nil, err + } + methods := make(map[string]*ssa.Function) + for _, function := range universe.Functions() { + if function != nil && function.Signature != nil && function.Signature.Recv() != nil && function.Synthetic == "" { + methods[function.Name()] = function + } + } + root := ssaPkg.Func("Root") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + DynamicResolution: resolution, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + }) + if err != nil { + return universe, nil, methods, err + } + return universe, plan, methods, nil +} + +func coroStaticMethodCompilation(plan *coro.SSAPlan, universe *EmissionUniverse) *Compilation { + return &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + } +} diff --git a/cl/coro_minmax_builtin_test.go b/cl/coro_minmax_builtin_test.go new file mode 100644 index 0000000000..efcdd1e8ca --- /dev/null +++ b/cl/coro_minmax_builtin_test.go @@ -0,0 +1,80 @@ +//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 ( + "strings" + "testing" +) + +const coroMinMaxBuiltinFixture = `package foo +type Count int +func MinInt(a, b, c int) int { return min(a, b, c) } +func MaxFloat(a, b float64) float64 { return max(a, b) } +func MinNamed(a, b Count) Count { return min(a, b) } +func MaxString(a, b string) string { return max(a, b) } +` + +func TestCoroMinMaxNumericBuiltinsArePureSelects(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroMinMaxBuiltinFixture) + for _, test := range []struct { + function string + builtin string + }{ + {function: "MinInt", builtin: "min"}, + {function: "MaxFloat", builtin: "max"}, + {function: "MinNamed", builtin: "min"}, + } { + t.Run(test.function, func(t *testing.T) { + fn := ssaPkg.Func(test.function) + call := coroComplexBuiltinCall(t, fn, test.builtin) + audit := &coroPhysicalPureSSAAudit{fn: fn, reachableBlocks: coroPhysicalConstantReachableBlocks(fn)} + if reason := audit.validateBuiltin(call); reason != "" { + t.Fatalf("%s rejected: %s", test.builtin, reason) + } + }) + } +} + +func TestCoroMinMaxStringBuiltinFreezesStringLess(t *testing.T) { + prog, _, universe, root, audit, _ := prepareCoroFrameRootAudit( + t, coroMinMaxBuiltinFixture, "MaxString", EmissionUniverseOptions{}, + ) + defer prog.Dispose() + call := coroComplexBuiltinCall(t, root, "max") + if got := strings.Join(universe.loweredRuntimeHelpers(audit.ctx, call), ","); got != "StringLess" { + t.Fatalf("max string helpers = %q, want StringLess", got) + } + if reason := audit.validateBuiltin(call); reason != "runtime helper capability validation requires a frozen emission universe" { + t.Fatalf("max string validation = %q", reason) + } +} + +func TestCoroMinMaxBuiltinRejectsMalformedShape(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroMinMaxBuiltinFixture) + fn := ssaPkg.Func("MinInt") + call := coroComplexBuiltinCall(t, fn, "min") + args := call.Call.Args + call.Call.Args = nil + defer func() { call.Call.Args = args }() + audit := &coroPhysicalPureSSAAudit{fn: fn, reachableBlocks: coroPhysicalConstantReachableBlocks(fn)} + if reason := audit.validateBuiltin(call); !strings.Contains(reason, "invalid argument/result shape") { + t.Fatalf("malformed min rejection = %q", reason) + } +} diff --git a/cl/coro_panic.go b/cl/coro_panic.go new file mode 100644 index 0000000000..9fb0062957 --- /dev/null +++ b/cl/coro_panic.go @@ -0,0 +1,55 @@ +/* + * 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 ( + "fmt" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// compileCoroExplicitStatusPanic owns a terminal source instruction selected +// by the frozen physical outcome recipe. Preflight has +// already proved that X is one empty-interface value whose type/data words +// remain valid after this coroutine frame is destroyed; reaching this path +// with any other shape is a compiler-plan violation, never permission to fall +// back to the legacy runtime.Panic call. +func (p *context) compileCoroExplicitStatusPanic(b llssa.Builder, instruction *ssa.Panic) { + body := p.coroBody() + if instruction == nil || body == nil || !p.coroEmissionExplicitStatus() || b == nil || b.Func != p.fn { + goName, llvmName := "", "" + if p.goFn != nil { + goName = p.goFn.String() + } + if p.fn != nil { + llvmName = p.fn.Name() + } + panic(fmt.Errorf( + "explicit-status panic in %q (%s) escaped its exact physical coroutine body (active=%t builder-matches=%t)", + llvmName, goName, body != nil, b != nil && b.Func == p.fn, + )) + } + value := p.compileValue(b, instruction.X) + typeWord := b.EfaceType(value) + dataWord := b.InterfaceData(value) + if body.cleanup == nil { + body.panic(b, typeWord, dataWord) + } else { + body.cleanup.enterPanic(b, typeWord, dataWord) + } +} diff --git a/cl/coro_panic_test.go b/cl/coro_panic_test.go new file mode 100644 index 0000000000..e8c2f40910 --- /dev/null +++ b/cl/coro_panic_test.go @@ -0,0 +1,495 @@ +//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 ( + "bytes" + "regexp" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroExplicitStatusPanicFixture = `package foo + +var FirstPayload uint32 +var SecondPayload uint32 +var InterfacePayload any = &FirstPayload + +func Root(mode uint32) uint32 { + if mode == 0 { + return 11 + } + if mode == 1 { + panic(&FirstPayload) + } + if mode == 2 { + return 13 + } + if mode == 3 { + panic(InterfacePayload) + } + panic(&SecondPayload) +} +` + +func TestCoroExplicitStatusPanicNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root := compileCoroExplicitStatusPanicFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.FuncRep != coro.DirectCoro || + rootPlan.Demand != coro.AsyncDemand || !rootPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("Root plan = %+v, present=%t; want may-unwind direct coroutine", rootPlan, ok) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify explicit-status panic before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Root").String() + assertCoroExplicitStatusPanicBody(t, body, 3) + assertNoLegacyCoroPanicSymbol(t, module.String()) + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() { + t.Fatalf("CoroSplit did not create Root resume entry:\n%s", module.String()) + } + if got := strings.Count(resume.String(), "call void @"+coroPanicPrepareHookV1); got != 3 { + t.Fatalf("Root.resume panic prepare calls = %d, want 3:\n%s", got, resume.String()) + } + assertNoLegacyCoroPanicSymbol(t, module.String()) + for _, intrinsic := range []string{"llvm.coro.id", "llvm.coro.begin", "llvm.coro.suspend", "llvm.coro.end"} { + if hasLLVMCall(module.String(), intrinsic) { + t.Fatalf("post-split panic module still calls %s:\n%s", intrinsic, module.String()) + } + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit panic object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroPanicPrepareHookV1)) || + !bytes.Contains(object.Bytes(), []byte("foo.Root$coro")) { + t.Fatal("post-CoroSplit object lost the panic hook or physical coroutine symbol") + } + }) + } +} + +func assertCoroExplicitStatusPanicBody(t *testing.T, body string, panicSites int) { + t.Helper() + if got := strings.Count(body, "call void @"+coroPanicPrepareHookV1); got != panicSites { + t.Fatalf("panic prepare calls = %d, want %d:\n%s", got, panicSites, body) + } + if got := strings.Count(body, "call void @"+coroCompletePrepareHookV2); got != 1 { + t.Fatalf("completion prepare calls = %d, want one shared normal completion:\n%s", got, body) + } + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 2 { + t.Fatalf("coro.suspend calls = %d, want initial + one shared final:\n%s", got, body) + } + if got := strings.Count(body, "@llvm.coro.suspend(token none, i1 true)"); got != 1 { + t.Fatalf("final coro.suspend calls = %d, want exactly one shared final suspend:\n%s", got, body) + } + stateAndHook := regexp.MustCompile( + `(?s)store i16 5,.*?store i16 4,.*?store i32 [1-9][0-9]*,.*?call void @` + regexp.QuoteMeta(coroPanicPrepareHookV1) + + `\(ptr [^,]+, ptr [^,]+, ptr [^,]+, ptr [^,]+, ptr [^)]+\)`, + ) + if got := len(stateAndHook.FindAllStringIndex(body, -1)); got != panicSites { + t.Fatalf("Panic/FinalSuspended/stateID publication followed by the five-pointer hook = %d, want %d:\n%s", got, panicSites, body) + } + hookBranch := regexp.MustCompile( + `call void @`+regexp.QuoteMeta(coroPanicPrepareHookV1)+`\([^\n]+\)\n\s+br label (%[-a-zA-Z$._0-9]+)`, + ).FindAllStringSubmatch(body, -1) + if len(hookBranch) != panicSites { + t.Fatalf("panic hooks followed immediately by an ordinary branch = %d, want %d (no source panic/unreachable path):\n%s", len(hookBranch), panicSites, body) + } + completeBranch := regexp.MustCompile( + `call void @` + regexp.QuoteMeta(coroCompletePrepareHookV2) + `\([^\n]+\)\n\s+br label (%[-a-zA-Z$._0-9]+)`, + ).FindStringSubmatch(body) + if len(completeBranch) != 2 { + t.Fatalf("normal completion does not branch to the shared terminal block:\n%s", body) + } + for _, branch := range hookBranch { + if branch[1] != completeBranch[1] { + t.Fatalf("panic branch target %s differs from normal completion target %s:\n%s", branch[1], completeBranch[1], body) + } + } + finalSuspend := strings.Index(body, "@llvm.coro.suspend(token none, i1 true)") + if finalSuspend < 0 { + t.Fatalf("shared final suspend is absent:\n%s", body) + } + for offset := 0; ; { + relative := strings.Index(body[offset:], "call void @"+coroPanicPrepareHookV1) + if relative < 0 { + break + } + hook := offset + relative + if hook >= finalSuspend { + t.Fatalf("panic hook does not precede the shared final suspend:\n%s", body) + } + offset = hook + len(coroPanicPrepareHookV1) + } +} + +func assertNoLegacyCoroPanicSymbol(t *testing.T, ir string) { + t.Helper() + for _, forbidden := range []string{"runtime.Panic", "runtime.Rethrow"} { + if strings.Contains(ir, forbidden) { + t.Fatalf("explicit-status coroutine retained legacy panic symbol %q:\n%s", forbidden, ir) + } + } +} + +func compileCoroExplicitStatusPanicFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroExplicitStatusPanicFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root +} + +func TestCoroExplicitStatusPanicPreflightRemainsFailClosed(t *testing.T) { + for _, test := range []struct { + name string + source string + want string + exec coro.ExecFlags + }{ + { + name: "dynamic interface operand", + source: `package foo +func Root(value any, trigger bool) { if trigger { panic(value) } } +`, + want: "has no post-destroy lifetime proof", + }, + { + name: "unproven interface load address", + source: `package foo +import "unsafe" +func Root(address uintptr, trigger bool) { if trigger { panic(*(*any)(unsafe.Pointer(address))) } } +`, + want: "uintptr-to-pointer conversion has no traceable exact pointer provenance", + }, + { + name: "untyped nil", + source: `package foo +func Root(trigger bool) { if trigger { panic(nil) } } +`, + want: "explicit-status panic", + }, + { + name: "boxed scalar", + source: `package foo +func Root(trigger bool) { if trigger { panic(uint32(7)) } } +`, + want: "structured runtime helper validation requires a frozen emission universe", + }, + { + name: "frame local pointer", + source: `package foo +func Root(trigger bool) { value := uint32(7); if trigger { panic(&value) }; _ = value } +`, + want: "heap allocation requires managed allocation", + }, + { + name: "parameter pointer", + source: `package foo +func Root(value *uint32, trigger bool) { if trigger { panic(value) } } +`, + want: "may outlive its coroutine frame", + }, + { + name: "cleanup frame", + source: `package foo +var Payload uint32 +func cleanup() {} +func Root(trigger bool) { defer cleanup(); if trigger { panic(&Payload) } } +`, + want: "execution flags", + exec: coro.NeedsCleanupFrame, + }, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, test.source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + plan := coro.FunctionPlan{ + ID: coro.FunctionID("foo.Root"), + External: coro.Defined, + Demand: coro.AsyncDemand, + ManagedDemand: coro.AsyncDemand, + Emission: coro.EmitCoroutine, + Primary: coro.PrimaryCoroutine, + FuncRep: coro.DirectCoro, + Effect: coro.YieldOnly, + Exec: coro.MayUnwind | test.exec, + } + err = validateCoroPhysicalABIWithUniverseCapabilities(root, plan, nil, universe, true, false, false, true) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("preflight error = %v, want %q", err, test.want) + } + }) + } +} + +func TestCoroExplicitStatusPanicAcceptsStableClosureInterfaceLoad(t *testing.T) { + const source = `package foo +type state struct { payload any } +func Root(value *state) { + inner := func() { panic(value.payload) } + inner() +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + if root == nil { + t.Fatal("Root function is absent") + } + if len(root.AnonFuncs) != 1 { + t.Fatalf("Root anonymous functions = %d, want one", len(root.AnonFuncs)) + } + inner := root.AnonFuncs[0] + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == root || function == inner { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + innerPlan, ok := plan.FunctionPlan(inner) + if !ok || innerPlan.Emission != coro.EmitCoroutine || !innerPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("inner plan = %+v, present=%t; want may-unwind coroutine", innerPlan, ok) + } + if err := validateCoroPhysicalABIWithUniverseCapabilities( + inner, innerPlan, plan, universe, true, false, false, true, + ); err != nil { + t.Fatalf("stable closure interface load rejected: %v", err) + } +} + +func TestCoroExplicitStatusPanicRejectsPlainCallFromPhysicalBody(t *testing.T) { + const source = `package foo +var Payload uint32 +func Plain(value, divisor uint32) uint32 { return value / divisor } +func Root(value, divisor uint32, trigger bool) uint32 { + result := Plain(value, divisor) + if trigger { panic(&Payload) } + return result +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + plainPlan, ok := plan.FunctionPlan(ssaPkg.Func("Plain")) + if !ok || !plainPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("Plain plan = %+v, present=%t; want exact unknown-divisor unwind fact", plainPlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + got, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err == nil || !strings.Contains(err.Error(), "direct plain target") || !strings.Contains(err.Error(), "hidden-outcome/unwind contract") { + t.Fatalf("plain-call preflight result = %v, %v; want exact hidden-outcome rejection", got, err) + } + if got != nil { + t.Fatal("plain-body preflight failure returned a partial package") + } +} + +func TestCoroExplicitStatusPanicAcceptsExactNoUnwindPlainCall(t *testing.T) { + const source = `package foo +var Payload uint32 +func Plain(value uint32) uint32 { return value + 1 } +func Root(value uint32, trigger bool) uint32 { + result := Plain(value) + if trigger { panic(&Payload) } + return result +} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + plain := ssaPkg.Func("Plain") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + plainPlan, ok := plan.FunctionPlan(plain) + if !ok || plainPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("Plain plan = %+v, present=%t; want exact no-unwind proof", plainPlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + got, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("exact no-unwind plain call rejected: %v", err) + } + if got == nil { + t.Fatal("exact no-unwind plain call returned no package") + } +} diff --git a/cl/coro_park_emitter.go b/cl/coro_park_emitter.go new file mode 100644 index 0000000000..c840cb88b5 --- /dev/null +++ b/cl/coro_park_emitter.go @@ -0,0 +1,163 @@ +/* + * 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 ( + "fmt" + + llssa "github.com/goplus/llgo/ssa" +) + +func (p *context) requireCoroParkV2Body(b llssa.Builder, operation string) *coroBodyContext { + body := p.coroBody() + plan := p.coroEmissionPlan() + if body == nil || plan == nil || plan.frameRetentionABI != CoroFrameRetentionParkABIV2 || b.Func != p.fn { + panic("coroutine " + operation + " lowering requires an active planned ParkABIV2 physical coroutine body") + } + if body.abi.version < coroPhysicalABIVersionV1 || body.completion == nil || + body.finalSuspend == nil || body.unsupportedRunDecision == nil || body.cancelRunDecision == nil { + panic("coroutine " + operation + " lowering requires the complete PhysicalABIV1 scheduler ABI") + } + return body +} + +// coroParkFaultRoute maps one source-specific resume status to the canonical +// terminal-fault path. Keeping the route semantic prevents feature lowerers +// from injecting arbitrary physical dispatch callbacks into the envelope. +type coroParkFaultRoute struct { + status uint64 + kind uint32 +} + +// coroParkOperation is the target-neutral compiler envelope shared by one +// timer, poll, worker, channel, or WaitSet park. Feature lowerers bind only +// their typed park/resume hooks, finite status vocabulary, and terminal fault +// outcomes; the emitter owns the state transition, cancellation targets, +// fail-closed default, and joined continuation. +// Multi-candidate channel/select winner reconciliation remains one typed +// WaitSet transaction inside these hooks and is never represented as several +// independent parks. +type coroParkOperation struct { + shouldSuspend llssa.Expr + park func(llssa.Builder) + resume func(llssa.Builder) llssa.Expr + normal []uint64 + faults []coroParkFaultRoute + abort uint64 + shutdown uint64 +} + +const maxCoroParkResumeStatus = uint64(^uint32(0)) + +func validateCoroParkOperationStatuses( + normal []uint64, + faults []coroParkFaultRoute, + abort, shutdown uint64, +) error { + if len(normal) == 0 { + return fmt.Errorf("coroutine park operation has no normal resume status") + } + seen := make(map[uint64]string, len(normal)+len(faults)+2) + add := func(kind string, status uint64) error { + if status > maxCoroParkResumeStatus { + return fmt.Errorf("coroutine park %s resume status %d does not fit the uint32 runtime ABI", kind, status) + } + if previous, duplicate := seen[status]; duplicate { + return fmt.Errorf("coroutine park %s resume status %d duplicates %s status", kind, status, previous) + } + seen[status] = kind + return nil + } + for _, status := range normal { + if err := add("normal", status); err != nil { + return err + } + } + for index, route := range faults { + if route.kind == 0 || route.kind >= coroFaultLimitV1 { + return fmt.Errorf("coroutine park fault resume route %d has invalid fault kind %d", index, route.kind) + } + if err := add("fault", route.status); err != nil { + return err + } + } + if err := add("abort", abort); err != nil { + return err + } + if err := add("shutdown", shutdown); err != nil { + return err + } + return nil +} + +func (c *coroBodyContext) emitCoroParkOperation(p *context, b llssa.Builder, operation coroParkOperation) { + if c == nil || p == nil || b == nil || b.Func != p.fn || c.coro == nil || c.unsupportedRunDecision == nil || + operation.shouldSuspend.IsNil() || operation.park == nil || operation.resume == nil { + panic("coroutine park operation requires a complete physical emitter and protocol") + } + if err := validateCoroParkOperationStatuses( + operation.normal, + operation.faults, + operation.abort, + operation.shutdown, + ); err != nil { + panic(err) + } + if operation.shouldSuspend.Type != b.Prog.Bool() { + panic("coroutine park suspend predicate must be bool") + } + faultTargets := make([]llssa.BasicBlock, len(operation.faults)) + for index := range faultTargets { + faultTargets[index] = b.Func.MakeBlock() + } + join := c.coro.SuspendCurrentBlockIfWithResumeDispatch( + operation.shouldSuspend, + func(suspend llssa.Builder) { + stateID := c.nextState + c.nextState++ + c.instructions = 0 + c.publishState(suspend, coroSuspendPark, coroLifecycleSuspended, stateID) + operation.park(suspend) + }, + func(resume llssa.Builder, normal llssa.BasicBlock) { + status := operation.resume(resume) + if status.IsNil() { + panic("coroutine park resume hook returned no status") + } + if status.Type != resume.Prog.Uint32() { + panic("coroutine park resume hook must return a uint32 runtime status") + } + abort, shutdown := c.cancellationRunDecisionTargets(resume) + dispatch := resume.Switch(status, c.unsupportedRunDecision) + for _, value := range operation.normal { + dispatch.Case(resume.Prog.IntVal(value, resume.Prog.Uint32()), normal) + } + for index, route := range operation.faults { + dispatch.Case(resume.Prog.IntVal(route.status, resume.Prog.Uint32()), faultTargets[index]) + } + dispatch.Case(resume.Prog.IntVal(operation.abort, resume.Prog.Uint32()), abort) + dispatch.Case(resume.Prog.IntVal(operation.shutdown, resume.Prog.Uint32()), shutdown) + dispatch.End(resume) + }, + ) + for index, target := range faultTargets { + b.SetBlockEx(target, llssa.AtEnd, false) + p.compileCoroTerminalFault(b, operation.faults[index].kind) + } + b.SetBlock(join) + c.activate(b) +} diff --git a/cl/coro_park_emitter_test.go b/cl/coro_park_emitter_test.go new file mode 100644 index 0000000000..44c5761ed0 --- /dev/null +++ b/cl/coro_park_emitter_test.go @@ -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 cl + +import ( + "strings" + "testing" +) + +func TestCoroParkOperationStatusPlanRejectsIncompleteOrAmbiguousVocabulary(t *testing.T) { + for _, test := range []struct { + name string + normal []uint64 + faults []coroParkFaultRoute + abort uint64 + shutdown uint64 + want string + }{ + {name: "no normal", abort: 1, shutdown: 2, want: "no normal"}, + {name: "duplicate normal", normal: []uint64{1, 1}, abort: 2, shutdown: 3, want: "duplicates normal"}, + {name: "missing fault kind", normal: []uint64{1}, faults: []coroParkFaultRoute{{status: 2}}, abort: 3, shutdown: 4, want: "invalid fault kind"}, + {name: "unknown fault kind", normal: []uint64{1}, faults: []coroParkFaultRoute{{status: 2, kind: coroFaultLimitV1}}, abort: 3, shutdown: 4, want: "invalid fault kind"}, + {name: "fault collision", normal: []uint64{1}, faults: []coroParkFaultRoute{{status: 1, kind: 2}}, abort: 3, shutdown: 4, want: "duplicates normal"}, + {name: "duplicate fault", normal: []uint64{1}, faults: []coroParkFaultRoute{{status: 2, kind: 2}, {status: 2, kind: 3}}, abort: 3, shutdown: 4, want: "duplicates fault"}, + {name: "abort collision", normal: []uint64{1}, abort: 1, shutdown: 3, want: "duplicates normal"}, + {name: "shutdown collision", normal: []uint64{1}, abort: 2, shutdown: 2, want: "duplicates abort"}, + {name: "normal ABI overflow", normal: []uint64{maxCoroParkResumeStatus + 1}, abort: 2, shutdown: 3, want: "uint32"}, + {name: "fault ABI overflow", normal: []uint64{1}, faults: []coroParkFaultRoute{{status: maxCoroParkResumeStatus + 1, kind: 2}}, abort: 2, shutdown: 3, want: "uint32"}, + {name: "abort ABI overflow", normal: []uint64{1}, abort: maxCoroParkResumeStatus + 1, shutdown: 3, want: "uint32"}, + {name: "shutdown ABI overflow", normal: []uint64{1}, abort: 2, shutdown: maxCoroParkResumeStatus + 1, want: "uint32"}, + } { + t.Run(test.name, func(t *testing.T) { + err := validateCoroParkOperationStatuses(test.normal, test.faults, test.abort, test.shutdown) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("status plan error = %v, want %q", err, test.want) + } + }) + } + if err := validateCoroParkOperationStatuses( + []uint64{1, 2, 3}, + []coroParkFaultRoute{{status: 4, kind: 1}}, + 5, + 6, + ); err != nil { + t.Fatalf("valid status plan: %v", err) + } +} diff --git a/cl/coro_park_test.go b/cl/coro_park_test.go new file mode 100644 index 0000000000..826ddf99d6 --- /dev/null +++ b/cl/coro_park_test.go @@ -0,0 +1,284 @@ +//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 ( + "bytes" + "regexp" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroParkTestSource = `package foo + +import _ "unsafe" + +type WaitToken struct { word uint32 } +type WaitTicket uint32 + +//go:linkname park llgo.coroPark +func park(token *WaitToken, ticket WaitTicket) + +func Root(token *WaitToken, ticket WaitTicket) uint32 { + before := uint32(ticket) + 7 + park(token, ticket) + return before + uint32(ticket) +} +` + +func TestCoroParkCurrentFrameNativeAndWasm32(t *testing.T) { + tests := []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, _, root, parkCall := compileCoroParkFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.FuncRep != coro.DirectCoro || + !rootPlan.DeclaredEffect.Contains(coro.MayPark) || !rootPlan.LocalEffect.Contains(coro.MayPark) || + !rootPlan.Effect.Contains(coro.MayPark) { + t.Fatalf("Root plan = %+v, present=%t; want one may-park coroutine primary", rootPlan, ok) + } + if !plan.ElidesCall(parkCall) { + t.Fatal("coroPark declaration call is not frozen as a frontend-elided intrinsic site") + } + if _, ok := plan.CallPlan(parkCall); ok { + t.Fatal("coroPark declaration unexpectedly retained a managed CallPlan") + } + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify park coroutine before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Root").String() + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("Root coro.suspend calls = %d, want initial + park + final:\n%s", got, body) + } + if strings.Contains(body, "@foo.park") || strings.Contains(body, "@llgo.coroPark") { + t.Fatalf("structured park leaked an ordinary sync helper call:\n%s", body) + } + stateAndHook := regexp.MustCompile( + `(?s)store i16 4,.*store i16 3,.*store i32 1,.*call void @` + regexp.QuoteMeta(coroKeyedParkHookV2) + + `\(ptr [^,]+, ptr [^,]+, ptr [^,]+, ptr [^)]+\)`, + ) + if !stateAndHook.MatchString(body) { + t.Fatalf("Root does not publish Park/Suspended/stateID=1 before the exact v1 hook:\n%s", body) + } + hook := strings.Index(body, "call void @"+coroKeyedParkHookV2) + parkSuspendRelative := strings.Index(body[hook:], "call i8 @llvm.coro.suspend") + if hook < 0 || parkSuspendRelative < 0 { + t.Fatalf("Root has no park hook followed by a caller-frame suspend:\n%s", body) + } + parkSuspend := hook + parkSuspendRelative + resumeRelative := strings.Index(body[parkSuspend:], "call i32 @"+coroKeyedResumeHookV2) + if resumeRelative < 0 { + t.Fatalf("Root does not consume its keyed decision after park resume:\n%s", body) + } + activate := regexp.MustCompile(`(?s)store i16 0,.*store i16 2,`).FindStringIndex(body) + if activate == nil { + t.Fatalf("Root does not reactivate its exact frame after resume:\n%s", body) + } + assertCoroScalarRunDecisionCalls(t, "Root park", body, 1) + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), "call void @"+coroKeyedParkHookV2) || + !strings.Contains(resume.String(), "call i32 @"+coroKeyedResumeHookV2) { + t.Fatalf("CoroSplit lost the park handoff in Root.resume:\n%s", module.String()) + } + assertCoroRunDecisionResumeOnly(t, module, "foo.Root$coro", 1) + for _, intrinsic := range []string{"llvm.coro.id", "llvm.coro.begin", "llvm.coro.suspend", "llvm.coro.end"} { + if hasLLVMCall(module.String(), intrinsic) { + t.Fatalf("post-split park module still calls %s:\n%s", intrinsic, module.String()) + } + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit park object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroKeyedParkHookV2)) || + !bytes.Contains(object.Bytes(), []byte(coroKeyedResumeHookV2)) { + t.Fatalf("post-CoroSplit object lost unresolved keyed Park V2 ABI symbols") + } + if !bytes.Contains(object.Bytes(), []byte(coroRunDecisionTakeZeroHookV1)) { + t.Fatalf("post-CoroSplit object lost unresolved run-decision ABI symbol %q", coroRunDecisionTakeZeroHookV1) + } + }) + } +} + +func TestCoroIntrinsicEmissionObserverFailsClosed(t *testing.T) { + prog, pkg, plan, universe, root, parkCall := compileCoroParkFixture(t, nil) + defer prog.Dispose() + defer pkg.Module().Dispose() + owners := universe.sortedUseOwners(root) + if len(owners) != 1 { + t.Fatalf("Root owners = %d, want 1", len(owners)) + } + // The fixture intentionally uses an isolated/report universe for compact + // LLVM tests. Enable only the already-frozen ledger lookup below; no further + // helper resolution or code generation occurs after this point. + universe.completeRuntimeABI = true + for _, test := range []struct { + name string + run func(*context) + want string + }{ + { + name: "mismatched recipe", + run: func(ctx *context) { + finish := ctx.beginCoroSiteEmission(parkCall) + defer finish() + ctx.observeCoroIntrinsicCallEmission(llgoCoroPark, CoroIntrinsicCallInlineNoSuspend) + }, + want: "emitted intrinsic recipe 1, frozen SitePlan requires 3", + }, + { + name: "mismatched opcode", + run: func(ctx *context) { + finish := ctx.beginCoroSiteEmission(parkCall) + defer finish() + ctx.observeCoroIntrinsicCallEmission(llgoCoroYield, CoroIntrinsicCallInlineSuspend) + }, + want: "emitted intrinsic opcode", + }, + { + name: "missing recipe", + run: func(ctx *context) { + ctx.beginCoroSiteEmission(parkCall)() + }, + want: "omitted frozen intrinsic recipe 3", + }, + { + name: "mismatched elision", + run: func(ctx *context) { + finish := ctx.beginCoroSiteEmission(parkCall) + defer finish() + ctx.observeCoroCallElision(CoroCallElidedNoInit) + }, + want: "emitted call elision 1, frozen SitePlan requires 3", + }, + } { + t.Run(test.name, func(t *testing.T) { + ctx, err := universe.functionABIContext(root, owners[0]) + if err != nil { + t.Fatal(err) + } + ctx.compilation = &Compilation{CoroPlan: plan, EmissionUniverse: universe} + ctx.coroEmission = &coroPhysicalEmissionSession{phase: coroPhysicalEmissionPrologue} + message := captureCoroSitePlanPanic(func() { test.run(ctx) }) + if !strings.Contains(message, test.want) { + t.Fatalf("observer panic = %q, want %q", message, test.want) + } + }) + } +} + +func compileCoroParkFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *EmissionUniverse, *ssa.Function, *ssa.Call, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroParkTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + var parkCall *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Call.StaticCallee() == nil || call.Call.StaticCallee().Name() != "park" { + continue + } + parkCall = call + } + } + if parkCall == nil { + prog.Dispose() + t.Fatal("fixture has no direct park call") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.MayPark}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{ + CoroPlan: plan, EmissionUniverse: universe, + CoroFrameRetentionABI: CoroFrameRetentionParkABIV2, + } + enableCoroChildAwaitCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, universe, root, parkCall +} diff --git a/cl/coro_patch_init.go b/cl/coro_patch_init.go new file mode 100644 index 0000000000..f98250cfa1 --- /dev/null +++ b/cl/coro_patch_init.go @@ -0,0 +1,108 @@ +/* + * 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 ( + "fmt" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// tryCompileCoroPatchInitRedirect replaces one x/tools dependency call to an +// original package initializer with the exact public initializer selected by +// package patching. Analysis sees the same physical edge through the owner's +// frozen lowered-call occurrence. +func (p *context) tryCompileCoroPatchInitRedirect(b llssa.Builder, call *ssa.Call) (result llssa.Expr, handled bool) { + if p.compilation == nil || p.emissionUniverse == nil || call == nil { + return llssa.Nil, false + } + logicalName, target, redirected, err := p.emissionUniverse.CoroPatchInitRedirect(call) + if err != nil { + panic(fmt.Errorf("coroutine patch initializer replacement: %w", err)) + } + if !redirected { + return llssa.Nil, false + } + defer func() { + if handled { + p.observeCoroCallElision(CoroCallElidedPatchRedirect) + } + }() + if p.goFn == nil || call.Parent() != p.goFn || p.compilation.CoroPlan == nil || b.Func != p.fn { + panic("coroutine patch initializer replacement requires its exact active owner and SSA plan") + } + if !p.compilation.CoroPlan.ElidesCall(call) { + panic("coroutine patch initializer replacement source occurrence is not frontend-elided in the SSA plan") + } + frozen, planned := p.compilation.CoroPlan.ResolveLoweredCallRecord(p.goFn, logicalName) + if !planned || frozen.Target != target || frozen.RawPlain || frozen.UnwindOnly || frozen.ExplicitStatusElided { + panic("coroutine patch initializer replacement disagrees between the emission universe and SSA plan") + } + targetPlan, planned := p.compilation.CoroPlan.FunctionPlan(target) + if !planned || targetPlan.External != coro.Defined || targetPlan.Demand == coro.NoDemand { + panic("coroutine patch initializer replacement targets an unavailable function") + } + if target.Signature == nil || target.Signature.Recv() != nil || target.Signature.Params().Len() != 0 || + target.Signature.Results().Len() != 0 || len(target.FreeVars) != 0 { + panic("coroutine patch initializer replacement target does not have exact func() shape") + } + + if p.rawPlainBody { + var fn llssa.Function + var kind int + switch targetPlan.Emission { + case coro.EmitPlain: + fn, _, kind = p.compileManagedFunction(target) + case coro.EmitCoroutine: + if !p.compilation.CoroPlan.HasRawPlainVariant(target) { + panic("raw plain patch initializer replacement has no exact raw target variant") + } + fn, _, kind = p.compileRawPlainFunction(target) + default: + panic(fmt.Sprintf("raw plain patch initializer replacement has unsupported target emission %s", targetPlan.Emission)) + } + if fn == nil || kind != goFunc { + panic("raw plain patch initializer replacement did not resolve to a Go entry") + } + b.Call(fn.Expr) + return llssa.Nil, true + } + + switch targetPlan.Emission { + case coro.EmitPlain: + if targetPlan.Effect.MaySuspend() || targetPlan.FuncRep == coro.DirectCoro { + panic("plain patch initializer replacement target has coroutine-only semantics") + } + fn, _, kind := p.compileFunction(target) + if fn == nil || kind != goFunc { + panic("plain patch initializer replacement did not resolve to a Go entry") + } + b.Call(fn.Expr) + case coro.EmitCoroutine: + if p.coroBody() == nil { + panic("coroutine patch initializer replacement escaped into a plain owner") + } + if result := p.compileCoroTargetAwait(b, target, nil); !result.IsNil() { + panic("coroutine patch initializer replacement returned a value") + } + default: + panic(fmt.Sprintf("managed patch initializer replacement has unsupported target emission %s", targetPlan.Emission)) + } + return llssa.Nil, true +} diff --git a/cl/coro_patch_init_ir_test.go b/cl/coro_patch_init_ir_test.go new file mode 100644 index 0000000000..2495109cc9 --- /dev/null +++ b/cl/coro_patch_init_ir_test.go @@ -0,0 +1,263 @@ +//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/ast" + "regexp" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + "github.com/goplus/llgo/internal/typepatch" + "github.com/goplus/llgo/ssa/abi" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +type patchInitCFGSnapshot struct { + blocks []*ssa.BasicBlock + succs [][]*ssa.BasicBlock +} + +func snapshotPatchInitCFG(fn *ssa.Function) patchInitCFGSnapshot { + snapshot := patchInitCFGSnapshot{ + blocks: append([]*ssa.BasicBlock(nil), fn.Blocks...), + succs: make([][]*ssa.BasicBlock, len(fn.Blocks)), + } + for index, block := range fn.Blocks { + snapshot.succs[index] = append([]*ssa.BasicBlock(nil), block.Succs...) + } + return snapshot +} + +func assertPatchInitCFGUnchanged(t *testing.T, phase, name string, fn *ssa.Function, before patchInitCFGSnapshot) { + t.Helper() + if len(fn.Blocks) != len(before.blocks) { + t.Fatalf("%s %s blocks = %d, want unchanged %d", phase, name, len(fn.Blocks), len(before.blocks)) + } + for index, block := range fn.Blocks { + if block != before.blocks[index] { + t.Fatalf("%s %s block %d identity changed", phase, name, index) + } + if len(block.Succs) != len(before.succs[index]) { + t.Fatalf("%s %s block %d successors = %d, want unchanged %d", phase, name, index, len(block.Succs), len(before.succs[index])) + } + for successor, got := range block.Succs { + if want := before.succs[index][successor]; got != want { + t.Fatalf("%s %s block %d successor %d = %p, want unchanged %p", phase, name, index, successor, got, want) + } + } + } +} + +func patchInitDirectCallCount(body, symbol string) int { + pattern := regexp.MustCompile(`(?m)^\s*(?:%[-a-zA-Z$._0-9]+\s*=\s*)?(?:musttail\s+|tail\s+)?call\b[^\n]*@"?` + regexp.QuoteMeta(symbol) + `"?\(`) + return len(pattern.FindAllStringIndex(body, -1)) +} + +func requirePatchInitDirectCall(t *testing.T, owner, body, target string) { + t.Helper() + if count := patchInitDirectCallCount(body, target); count != 1 { + t.Fatalf("%s direct calls to %q = %d, want exactly one:\n%s", owner, target, count, body) + } +} + +func forbidPatchInitDirectCall(t *testing.T, owner, body, target string) { + t.Helper() + if count := patchInitDirectCallCount(body, target); count != 0 { + t.Fatalf("%s directly calls forbidden target %q %d time(s):\n%s", owner, target, count, body) + } +} + +func TestCoroPatchInitIRUsesPublicThenPrivateSymbolsWithoutMutatingSSA(t *testing.T) { + const ( + patchedPath = "example.com/emission/patchir" + importerPath = "example.com/emission/patchirimporter" + ) + testProg := newEmissionTestProgram() + original := testProg.addPackage(t, patchedPath, `package patchir + +var Original = originalValue() + +func originalValue() int { + Yield() + return 1 +} + +func Yield() {} +`) + alternate := testProg.addPackage(t, abi.PatchPathPrefix+patchedPath, `package patchir + +var Patched = patchedValue() + +func patchedValue() int { return 2 } +`) + importer := testProg.addPackage(t, importerPath, `package patchirimporter + +import _ "example.com/emission/patchir" + +var Ready = true +`) + testProg.ssa.Build() + + originalInit := original.ssa.Func("init") + publicInit := alternate.ssa.Func("init") + importerInit := importer.ssa.Func("init") + if originalInit == nil || publicInit == nil || importerInit == nil { + t.Fatalf("fixture initializers = original %v, public %v, importer %v", originalInit, publicInit, importerInit) + } + watched := []struct { + name string + function *ssa.Function + before patchInitCFGSnapshot + }{ + {name: "original init", function: originalInit, before: snapshotPatchInitCFG(originalInit)}, + {name: "public patch init", function: publicInit, before: snapshotPatchInitCFG(publicInit)}, + {name: "importer init", function: importerInit, before: snapshotPatchInitCFG(importerInit)}, + } + assertUnchanged := func(phase string) { + t.Helper() + for _, function := range watched { + assertPatchInitCFGUnchanged(t, phase, function.name, function.function, function.before) + } + } + + patches := Patches{patchedPath: { + Alt: alternate.ssa, + Types: typepatch.Clone(alternate.types), + }} + patchedFiles := []*ast.File{original.file, alternate.file} + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, patches, []EmissionPackage{ + {SSA: original.ssa, Files: patchedFiles}, + {SSA: importer.ssa, Files: []*ast.File{importer.file}}, + }) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(testProg.ssa, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(testProg.ssa, coro.Roots{ + {Function: importerInit, Demand: coro.AsyncDemand}, + // Build orchestration roots every public patch initializer independently: + // no unpatched source function object denotes that public symbol. + {Function: publicInit, Demand: coro.AsyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + _, _, redirected, err := universe.CoroPatchInitRedirect(call) + return redirected, err + }, + ClassifyLocalBody: func(fn *ssa.Function) (coro.SSAFunctionBodyFacts, error) { + facts, err := universe.CoroLocalBodyFacts(fn) + if fn == alternate.ssa.Func("patchedValue") { + facts.Exec &^= coro.MayUnwind + } + return facts, err + }, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == original.ssa.Func("Yield") { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + assertUnchanged("after analysis") + for name, fn := range map[string]*ssa.Function{ + "original init": originalInit, + "public patch init": publicInit, + "importer init": importerInit, + } { + functionPlan, present := plan.FunctionPlan(fn) + if !present || functionPlan.Emission != coro.EmitCoroutine || functionPlan.FuncRep != coro.DirectCoro || functionPlan.Demand != coro.AsyncDemand { + t.Fatalf("%s plan = %+v, present=%t; want async-only direct coroutine", name, functionPlan, present) + } + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + tracking := NewCallerTracking() + patchedLL, _, err := NewPackageExWithEmbedOptions( + prog, tracking, patches, nil, original.ssa, patchedFiles, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile patched package: %v", err) + } + importerLL, _, err := NewPackageExWithEmbedOptions( + prog, tracking, patches, nil, importer.ssa, []*ast.File{importer.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile importer package: %v", err) + } + assertUnchanged("after compilation") + + patchedModule := patchedLL.Module() + defer patchedModule.Dispose() + importerModule := importerLL.Module() + defer importerModule.Dispose() + for name, module := range map[string]llvm.Module{"patched": patchedModule, "importer": importerModule} { + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify %s module: %v\n%s", name, err, module.String()) + } + } + + publicSymbol := patchedPath + ".init$coro" + privateSymbol := patchedPath + ".init$hasPatch$coro" + importerSymbol := importerPath + ".init$coro" + public := patchedModule.NamedFunction(publicSymbol) + private := patchedModule.NamedFunction(privateSymbol) + if public.IsNil() || public.FirstBasicBlock().IsNil() || private.IsNil() || private.FirstBasicBlock().IsNil() { + t.Fatalf("patch init definitions = public %v, private %v; want bodyful %q and %q\n%s", public, private, publicSymbol, privateSymbol, patchedModule.String()) + } + importerEntry := importerModule.NamedFunction(importerSymbol) + if importerEntry.IsNil() || importerEntry.FirstBasicBlock().IsNil() { + t.Fatalf("importer init definition %q is absent:\n%s", importerSymbol, importerModule.String()) + } + + importerIR := importerEntry.String() + publicIR := public.String() + privateIR := private.String() + requirePatchInitDirectCall(t, "importer init", importerIR, publicSymbol) + requirePatchInitDirectCall(t, "public patch init", publicIR, privateSymbol) + for _, target := range []string{importerPath + ".init", importerSymbol, privateSymbol} { + forbidPatchInitDirectCall(t, "importer init", importerIR, target) + } + for _, target := range []string{patchedPath + ".init", publicSymbol} { + forbidPatchInitDirectCall(t, "public patch init", publicIR, target) + } + for _, target := range []string{patchedPath + ".init$hasPatch", privateSymbol, publicSymbol} { + forbidPatchInitDirectCall(t, "private original init", privateIR, target) + } +} diff --git a/cl/coro_physical_plan.go b/cl/coro_physical_plan.go new file mode 100644 index 0000000000..6a0645078d --- /dev/null +++ b/cl/coro_physical_plan.go @@ -0,0 +1,1088 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// coroPhysicalInstructionRecipe is the closed code-generation choice for one +// source SSA instruction. Ordinary means that physical preflight accepted the +// legacy value recipe without a coroutine-specific fault branch. Every other +// value is selected and observed through the physical SitePlan; codegen may +// not rediscover it from SSA, target types, or frame-retention maps. +type coroPhysicalInstructionRecipe uint8 + +const ( + coroPhysicalInstructionOrdinary coroPhysicalInstructionRecipe = iota + coroPhysicalInstructionFieldAddr + coroPhysicalInstructionDeref + coroPhysicalInstructionIndexAddr + coroPhysicalInstructionIndex + coroPhysicalInstructionSlice + coroPhysicalInstructionSliceToArrayPointer + coroPhysicalInstructionBuiltinNilGuard + coroPhysicalInstructionSyntheticSelectNoCaseBox + coroPhysicalInstructionUnsafeString + coroPhysicalInstructionUnsafeSlice + coroPhysicalInstructionInterfaceNilCompare + coroPhysicalInstructionTerminalResultAllocation + coroPhysicalInstructionFrameAllocation + coroPhysicalInstructionFrameBitcastAllocation +) + +func (recipe coroPhysicalInstructionRecipe) String() string { + switch recipe { + case coroPhysicalInstructionOrdinary: + return "ordinary" + case coroPhysicalInstructionFieldAddr: + return "fieldaddr" + case coroPhysicalInstructionDeref: + return "deref" + case coroPhysicalInstructionIndexAddr: + return "indexaddr" + case coroPhysicalInstructionIndex: + return "index" + case coroPhysicalInstructionSlice: + return "slice" + case coroPhysicalInstructionSliceToArrayPointer: + return "slice-to-array-pointer" + case coroPhysicalInstructionBuiltinNilGuard: + return "builtin-nil-guard" + case coroPhysicalInstructionSyntheticSelectNoCaseBox: + return "synthetic-select-no-case-box" + case coroPhysicalInstructionUnsafeString: + return "unsafe-string" + case coroPhysicalInstructionUnsafeSlice: + return "unsafe-slice" + case coroPhysicalInstructionInterfaceNilCompare: + return "interface-nil-compare" + case coroPhysicalInstructionTerminalResultAllocation: + return "terminal-result-allocation" + case coroPhysicalInstructionFrameAllocation: + return "frame-allocation" + case coroPhysicalInstructionFrameBitcastAllocation: + return "frame-bitcast-allocation" + default: + return fmt.Sprintf("physical-recipe(%d)", uint8(recipe)) + } +} + +type coroPhysicalContainerKind uint8 + +const ( + coroPhysicalContainerNone coroPhysicalContainerKind = iota + coroPhysicalContainerString + coroPhysicalContainerArray + coroPhysicalContainerSlice + coroPhysicalContainerArrayPointer +) + +// coroPhysicalControlRecipe is the frozen coroutine control operation selected +// for one source instruction. It is deliberately orthogonal to the value/fault +// recipe above: an awaited call can still carry an implicit guard plan, while +// ordinary calls and values keep the zero control recipe. +type coroPhysicalControlRecipe uint8 + +const ( + coroPhysicalControlNone coroPhysicalControlRecipe = iota + coroPhysicalControlDirectAwait + coroPhysicalControlDispatchAwait + coroPhysicalControlClosedInterfaceAwait + coroPhysicalControlManagedInterfaceAwait + coroPhysicalControlPlainDispatch + coroPhysicalControlDirectSpawn + coroPhysicalControlDispatchSpawn +) + +func (recipe coroPhysicalControlRecipe) String() string { + switch recipe { + case coroPhysicalControlNone: + return "none" + case coroPhysicalControlDirectAwait: + return "direct-await" + case coroPhysicalControlDispatchAwait: + return "dispatch-await" + case coroPhysicalControlClosedInterfaceAwait: + return "closed-interface-await" + case coroPhysicalControlManagedInterfaceAwait: + return "managed-interface-await" + case coroPhysicalControlPlainDispatch: + return "plain-dispatch" + case coroPhysicalControlDirectSpawn: + return "direct-spawn" + case coroPhysicalControlDispatchSpawn: + return "dispatch-spawn" + default: + return fmt.Sprintf("physical-control-recipe(%d)", uint8(recipe)) + } +} + +type coroPhysicalOperationRecipe uint8 + +const ( + coroPhysicalOperationNone coroPhysicalOperationRecipe = iota + coroPhysicalOperationChannelSend + coroPhysicalOperationChannelReceive + coroPhysicalOperationChannelClose + coroPhysicalOperationChannelSelectPark + coroPhysicalOperationChannelSelectTry + coroPhysicalOperationWorkerSyscall + coroPhysicalOperationWorkerForeign +) + +func (recipe coroPhysicalOperationRecipe) String() string { + switch recipe { + case coroPhysicalOperationNone: + return "none" + case coroPhysicalOperationChannelSend: + return "channel-send" + case coroPhysicalOperationChannelReceive: + return "channel-receive" + case coroPhysicalOperationChannelClose: + return "channel-close" + case coroPhysicalOperationChannelSelectPark: + return "channel-select-park" + case coroPhysicalOperationChannelSelectTry: + return "channel-select-try" + case coroPhysicalOperationWorkerSyscall: + return "worker-syscall" + case coroPhysicalOperationWorkerForeign: + return "worker-foreign" + default: + return fmt.Sprintf("physical-operation-recipe(%d)", uint8(recipe)) + } +} + +// coroPhysicalOutcomeRecipe freezes source instructions that enter or inspect +// the Go completion/cleanup protocol. It is orthogonal to value/fault, +// call/spawn control, and blocking operation recipes: no outcome site may be +// rediscovered from a compilation-wide feature flag during emission. +type coroPhysicalOutcomeRecipe uint8 + +const ( + coroPhysicalOutcomeNone coroPhysicalOutcomeRecipe = iota + coroPhysicalOutcomeReturn + coroPhysicalOutcomeDeferRegister + coroPhysicalOutcomeRunDefers + coroPhysicalOutcomePanic + coroPhysicalOutcomeRecover + coroPhysicalOutcomeSyntheticSelectTrap +) + +func (recipe coroPhysicalOutcomeRecipe) String() string { + switch recipe { + case coroPhysicalOutcomeNone: + return "none" + case coroPhysicalOutcomeReturn: + return "return" + case coroPhysicalOutcomeDeferRegister: + return "defer-register" + case coroPhysicalOutcomeRunDefers: + return "run-defers" + case coroPhysicalOutcomePanic: + return "panic" + case coroPhysicalOutcomeRecover: + return "recover" + case coroPhysicalOutcomeSyntheticSelectTrap: + return "synthetic-select-trap" + default: + return fmt.Sprintf("physical-outcome-recipe(%d)", uint8(recipe)) + } +} + +type coroPhysicalLoweringCapabilities struct { + childAwait bool + staticSpawn bool + managedDispatch bool + explicitPanic bool + channel bool + worker bool + interfacePlain *coroClosedInterfacePlainPlan + managedInterface *coroManagedInterfaceDispatchPlan +} + +// coroPhysicalInstructionPlan contains only information that changes emitted +// control flow. container and bound make the guarded Index/Slice recipes +// target-independent; nilGuard and boundsGuard are independent because a +// frozen safe array index removes only the range edge, never a nullable +// pointer-to-array dereference. +type coroPhysicalInstructionPlan struct { + semantic coroSemanticInstructionPlan + recipe coroPhysicalInstructionRecipe + control coroPhysicalControlRecipe + controlTarget *ssa.Function + controlTargetID coro.FunctionID + controlInterface *coroInterfaceDispatchPlan + controlSignature *types.Signature + controlFailure string + controlFailureHard bool + operation coroPhysicalOperationRecipe + operationFailure string + operationWorker *coroWorkerForeignCallShape + outcome coroPhysicalOutcomeRecipe + outcomeFailure string + valueOperand ssa.Value + container coroPhysicalContainerKind + bound int64 + nilGuard bool + boundsGuard bool +} + +func (plan coroPhysicalInstructionPlan) mayFault() bool { + return plan.nilGuard || plan.boundsGuard || + plan.recipe == coroPhysicalInstructionFieldAddr || + plan.recipe == coroPhysicalInstructionDeref || + plan.recipe == coroPhysicalInstructionBuiltinNilGuard +} + +// elidesRuntimeHelper reports whether this frozen physical recipe replaces one +// logical LLSSA helper with compiler-owned structured control flow. Emission +// must use this projection rather than reclassifying the raw SSA instruction: +// the helper inventory remains part of effect/closure planning, while the +// selected recipe is the sole authority for whether a call is physically +// emitted in the live coroutine frame. +func (plan coroPhysicalInstructionPlan) elidesRuntimeHelper(helper string) bool { + switch plan.recipe { + case coroPhysicalInstructionFieldAddr, coroPhysicalInstructionDeref: + if helper == "AssertNilDeref" || helper == "AssertNilDerefPtr" { + return true + } + case coroPhysicalInstructionIndexAddr, coroPhysicalInstructionIndex: + if helper == "CheckIndexRange" || helper == "AssertNilDeref" || helper == "AssertNilDerefPtr" { + return true + } + case coroPhysicalInstructionSlice: + if helper == "StringSlice2" || helper == "NewSlice2" || helper == "NewSlice3Bounds" || + helper == "AssertNilDeref" { + return true + } + case coroPhysicalInstructionSliceToArrayPointer: + if helper == "PanicSliceConvert" { + return true + } + case coroPhysicalInstructionBuiltinNilGuard: + if helper == "PanicWrapNilPointer" { + return true + } + case coroPhysicalInstructionUnsafeString, coroPhysicalInstructionUnsafeSlice: + if helper == "AssertRuntimeError" { + return true + } + case coroPhysicalInstructionInterfaceNilCompare: + if helper == "EfaceEqual" || helper == "IfaceType" { + return true + } + case coroPhysicalInstructionFrameAllocation: + if helper == "AllocZ" { + return true + } + } + switch plan.outcome { + case coroPhysicalOutcomePanic: + return helper == "Panic" + case coroPhysicalOutcomeRecover: + return helper == "Recover" + default: + return false + } +} + +// coroPhysicalFunctionPlan is the post-analysis physical projection of one +// exact function emission. It owns every proof and per-instruction choice used +// after preflight. The pointed-to proofs are built to completion before this +// object is frozen and are read-only thereafter. +type coroPhysicalFunctionPlan struct { + function *ssa.Function + owner *preparedEmissionPackage + frameRetention *coroFrameRetentionProof + critical *coroCriticalProof + cleanup *coroStaticCleanupPlan + frameRetentionABI string + instructions map[ssa.Instruction]coroPhysicalInstructionPlan +} + +func prepareCoroPhysicalFunctionPlan( + audit *coroPhysicalPureSSAAudit, + owner *preparedEmissionPackage, + whole *coro.SSAPlan, + cleanup *coroStaticCleanupPlan, + critical *coroCriticalProof, + explicitPanic bool, + capabilities coroPhysicalLoweringCapabilities, +) (*coroPhysicalFunctionPlan, error) { + if audit == nil || audit.fn == nil { + return nil, fmt.Errorf("physical function planning requires one exact pure-SSA audit") + } + plan := &coroPhysicalFunctionPlan{ + function: audit.fn, + owner: owner, + frameRetention: audit.currentFrameRetentionProof(), + critical: critical, + cleanup: cleanup, + frameRetentionABI: audit.frameRetentionABI, + instructions: make(map[ssa.Instruction]coroPhysicalInstructionPlan), + } + var exactBitcastAllocation *ssa.Alloc + if proof, exact := coro.ProveSSAExactScalarBitcast(audit.fn); exact { + exactBitcastAllocation = proof.Allocation + } + for _, block := range audit.fn.Blocks { + for _, instruction := range block.Instrs { + instructionPlan, err := planCoroPhysicalInstruction( + audit, owner, whole, cleanup, exactBitcastAllocation, instruction, explicitPanic, capabilities, + ) + if err != nil { + return nil, fmt.Errorf("block %d instruction %T: %w", block.Index, instruction, err) + } + plan.instructions[instruction] = instructionPlan + } + } + return plan, nil +} + +func (plan *coroPhysicalFunctionPlan) instructionPlan(instruction ssa.Instruction) (coroPhysicalInstructionPlan, error) { + if plan == nil || plan.function == nil || plan.owner == nil || instruction == nil || instruction.Parent() != plan.function { + return coroPhysicalInstructionPlan{}, fmt.Errorf("physical instruction plan requires one exact frozen function owner and source instruction") + } + physical, ok := plan.instructions[instruction] + if !ok { + return coroPhysicalInstructionPlan{}, fmt.Errorf("source instruction %q is absent from its frozen physical plan", instruction.String()) + } + return physical, nil +} + +type coroPhysicalPlanStage struct { + plans map[emissionFunctionOwnerKey]*coroPhysicalFunctionPlan +} + +func newCoroPhysicalPlanStage() *coroPhysicalPlanStage { + return &coroPhysicalPlanStage{plans: make(map[emissionFunctionOwnerKey]*coroPhysicalFunctionPlan)} +} + +func (stage *coroPhysicalPlanStage) freezePhysicalFunctionPlan(plan *coroPhysicalFunctionPlan) error { + if stage == nil || plan == nil || plan.function == nil || plan.owner == nil || plan.instructions == nil { + return fmt.Errorf("physical plan freeze requires one complete function-owner projection") + } + key := emissionFunctionOwnerKey{function: plan.function, owner: plan.owner} + if _, exists := stage.plans[key]; exists { + return fmt.Errorf("physical plan for function %q owner %q was frozen more than once", plan.function.Name(), plan.owner.identity) + } + for _, block := range plan.function.Blocks { + for _, instruction := range block.Instrs { + if _, ok := plan.instructions[instruction]; !ok { + return fmt.Errorf("physical plan for function %q omitted source instruction %q", plan.function.Name(), instruction.String()) + } + } + } + stage.plans[key] = plan + return nil +} + +func (ir *coroProgramIR) commitPhysicalFunctionPlans(stage *coroPhysicalPlanStage, expected map[emissionFunctionOwnerKey]none) error { + if ir == nil || !ir.callsFrozen { + return fmt.Errorf("physical plan commit requires the call SitePlan stage") + } + if ir.physicalPlansSealed { + return fmt.Errorf("physical plans were committed more than once") + } + if stage == nil { + return fmt.Errorf("physical plan commit requires one complete staging transaction") + } + if len(stage.plans) != len(expected) { + return fmt.Errorf("physical plan stage has %d function owners, want %d", len(stage.plans), len(expected)) + } + for key := range expected { + if stage.plans[key] == nil { + name, owner := "", "" + if key.function != nil { + name = key.function.Name() + } + if key.owner != nil { + owner = key.owner.identity + } + return fmt.Errorf("physical plan stage omitted function %q owner %q", name, owner) + } + } + for key := range stage.plans { + if _, ok := expected[key]; !ok { + return fmt.Errorf("physical plan stage retained an unexpected function %q owner %q", key.function.Name(), key.owner.identity) + } + } + committed := make(map[emissionFunctionOwnerKey]*coroPhysicalFunctionPlan, len(stage.plans)) + for key, plan := range stage.plans { + committed[key] = plan + } + ir.physicalPlans = committed + ir.physicalPlansSealed = true + return nil +} + +func (ir *coroProgramIR) physicalFunctionPlan(function *ssa.Function, owner *preparedEmissionPackage) (*coroPhysicalFunctionPlan, error) { + if ir == nil || !ir.physicalPlansSealed { + return nil, fmt.Errorf("coroutine physical plans are not sealed") + } + if function == nil || owner == nil { + return nil, fmt.Errorf("physical plan lookup requires one exact function and owner") + } + plan := ir.physicalPlans[emissionFunctionOwnerKey{function: function, owner: owner}] + if plan == nil { + return nil, fmt.Errorf("function %q owner %q has no frozen physical plan", function.Name(), owner.identity) + } + return plan, nil +} + +// physicalFunctionPlanForEmission resolves the frozen definition projection +// used by one physical emission. Ordinary functions require the exact current +// package owner. A syntax-free generated wrapper is the sole exception: ABI +// method tables may materialize its linkonce body from another package, so the +// already-validated shared symbol may borrow the corresponding frozen plan. +func (index emissionCanonicalIndex) physicalFunctionPlanForEmission( + function *ssa.Function, + requestedOwner *preparedEmissionPackage, +) (*coroPhysicalFunctionPlan, error) { + u := index.universe + if u == nil || u.coroProgramIR == nil || !u.coroProgramIR.physicalPlansSealed { + return nil, fmt.Errorf("coroutine physical plans are not sealed") + } + if function == nil || requestedOwner == nil { + return nil, fmt.Errorf("physical emission plan lookup requires one exact function and requested owner") + } + function = u.canonicalAlias(function) + if function == nil { + return nil, fmt.Errorf("physical emission plan lookup found cyclic function aliases") + } + if plan := u.coroProgramIR.physicalPlans[emissionFunctionOwnerKey{ + function: function, + owner: requestedOwner, + }]; plan != nil { + return plan, nil + } + if !isEmissionGeneratedWrapper(function) { + return nil, fmt.Errorf("function %q owner %q has no frozen physical plan", function.Name(), requestedOwner.identity) + } + + shared, available := index.sharedGeneratedWrapperPhysicalName(function) + if shared == "" { + return nil, fmt.Errorf( + "generated wrapper %q owner %q has no unambiguous frozen physical symbol; frozen owners: %v", + function.Name(), requestedOwner.identity, available, + ) + } + for _, owner := range u.sortedUseOwners(function) { + key := emissionFunctionOwnerKey{function: function, owner: owner} + if u.physicalNames[key] != shared { + continue + } + if plan := u.coroProgramIR.physicalPlans[key]; plan != nil { + return plan, nil + } + } + return nil, fmt.Errorf( + "generated wrapper %q shared physical symbol %q has no corresponding frozen physical plan; frozen owners: %v", + function.Name(), shared, available, + ) +} + +func planCoroPhysicalInstruction( + audit *coroPhysicalPureSSAAudit, + owner *preparedEmissionPackage, + whole *coro.SSAPlan, + cleanup *coroStaticCleanupPlan, + exactBitcastAllocation *ssa.Alloc, + instruction ssa.Instruction, + explicitPanic bool, + capabilities coroPhysicalLoweringCapabilities, +) (coroPhysicalInstructionPlan, error) { + result := coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionOrdinary} + if audit == nil || instruction == nil || instruction.Parent() != audit.fn { + return result, fmt.Errorf("physical instruction planning requires one exact audit and source instruction") + } + semantic, err := planCoroSemanticInstruction(instruction) + if audit.universe != nil && audit.universe.coroProgramIR != nil && owner != nil { + semantic, err = audit.universe.coroProgramIR.semanticInstructionPlan(audit.fn, owner, instruction) + } + if err != nil { + return result, fmt.Errorf("load semantic instruction recipe: %w", err) + } + result.semantic = semantic + planCoroPhysicalControlInstruction(audit, whole, instruction, capabilities, &result) + planCoroPhysicalOperationInstruction(audit, whole, instruction, capabilities, &result) + planCoroPhysicalOutcomeInstruction(audit, cleanup, instruction, capabilities, &result) + switch instruction := instruction.(type) { + case *ssa.Alloc: + switch { + case coroPhysicalCleanupContainsTerminalAllocation(cleanup, instruction): + result.recipe = coroPhysicalInstructionTerminalResultAllocation + case instruction == exactBitcastAllocation: + result.recipe = coroPhysicalInstructionFrameBitcastAllocation + case !instruction.Heap || audit.frameRetainsAllocation(instruction): + result.recipe = coroPhysicalInstructionFrameAllocation + } + case *ssa.FieldAddr: + requiresGuard, reason := audit.fieldAddrRequiresImplicitNilFault(instruction) + if reason != "" { + return result, fmt.Errorf("FieldAddr frozen helper plan: %s", reason) + } + if requiresGuard { + result.recipe = coroPhysicalInstructionFieldAddr + result.nilGuard = true + } + case *ssa.UnOp: + if instruction.Op == token.MUL && audit.derefRequiresImplicitNilFault(instruction) { + result.recipe = coroPhysicalInstructionDeref + result.nilGuard = true + } + case *ssa.IndexAddr: + if audit.ctx != nil && emissionIsVargsAlloc(audit.ctx, instruction.X) { + break + } + if !explicitPanic { + break + } + result.recipe = coroPhysicalInstructionIndexAddr + container, bound, err := coroPhysicalContainerPlan(audit, instruction.X) + if err != nil { + return result, err + } + if container != coroPhysicalContainerSlice && container != coroPhysicalContainerArrayPointer { + return result, fmt.Errorf("IndexAddr has unsupported physical container %d", container) + } + result.container, result.bound = container, bound + result.nilGuard = container == coroPhysicalContainerArrayPointer && + !emissionKnownNonNilArrayBase(instruction.X) && !ssaValueProvenNonNilAt(instruction.X, instruction) + safe, err := coroPhysicalSafeFixedArrayIndex(audit, whole, instruction, instruction.X, instruction.Index) + if err != nil { + return result, err + } + result.boundsGuard = !safe + case *ssa.Index: + if !explicitPanic { + break + } + result.recipe = coroPhysicalInstructionIndex + container, bound, err := coroPhysicalContainerPlan(audit, instruction.X) + if err != nil { + return result, err + } + result.container, result.bound = container, bound + result.nilGuard = container == coroPhysicalContainerArrayPointer && + !emissionKnownNonNilArrayBase(instruction.X) && !ssaValueProvenNonNilAt(instruction.X, instruction) + safe, err := coroPhysicalSafeFixedArrayIndex(audit, whole, instruction, instruction.X, instruction.Index) + if err != nil { + return result, err + } + result.boundsGuard = !safe + case *ssa.Slice: + if audit.ctx != nil && emissionIsVargsAlloc(audit.ctx, instruction.X) { + break + } + if !explicitPanic { + break + } + result.recipe = coroPhysicalInstructionSlice + container, bound, err := coroPhysicalContainerPlan(audit, instruction.X) + if err != nil { + return result, err + } + if container != coroPhysicalContainerString && container != coroPhysicalContainerSlice && + container != coroPhysicalContainerArrayPointer { + return result, fmt.Errorf("Slice has unsupported physical container %d", container) + } + result.container, result.bound = container, bound + result.nilGuard = container == coroPhysicalContainerArrayPointer && + !isKnownNonNilAddr(instruction.X) && !ssaValueProvenNonNilAt(instruction.X, instruction) + result.boundsGuard = true + case *ssa.SliceToArrayPointer: + length, exact := coroSliceToArrayPointerLen(instruction, audit.typeOf) + if !exact || length < 0 { + return result, fmt.Errorf("slice-to-array-pointer has no exact physical length") + } + result.recipe = coroPhysicalInstructionSliceToArrayPointer + result.bound = length + result.boundsGuard = length != 0 + case *ssa.Call: + if explicitPanic && isWrapNilCheckCall(instruction) { + result.recipe = coroPhysicalInstructionBuiltinNilGuard + result.nilGuard = true + } + if builtin, ok := instruction.Common().Value.(*ssa.Builtin); ok && explicitPanic { + switch builtin.Name() { + case "String": + result.recipe = coroPhysicalInstructionUnsafeString + case "Slice": + result.recipe = coroPhysicalInstructionUnsafeSlice + } + } + case *ssa.MakeInterface: + if coroSyntheticSelectNoCaseBox(instruction) { + result.recipe = coroPhysicalInstructionSyntheticSelectNoCaseBox + } + case *ssa.BinOp: + if (instruction.Op == token.EQL || instruction.Op == token.NEQ) && + (isUntypedNilConst(instruction.X) || isUntypedNilConst(instruction.Y)) { + value := instruction.X + if isUntypedNilConst(value) { + value = instruction.Y + } + if _, ok := types.Unalias(audit.typeOf(value.Type())).Underlying().(*types.Interface); ok { + result.recipe = coroPhysicalInstructionInterfaceNilCompare + result.valueOperand = value + } + } + } + return result, nil +} + +func planCoroPhysicalOutcomeInstruction( + audit *coroPhysicalPureSSAAudit, + cleanup *coroStaticCleanupPlan, + instruction ssa.Instruction, + capabilities coroPhysicalLoweringCapabilities, + result *coroPhysicalInstructionPlan, +) { + if audit == nil || result == nil || instruction == nil || instruction.Parent() != audit.fn { + panic("physical outcome planning requires one exact instruction plan") + } + switch instruction := instruction.(type) { + case *ssa.Return: + result.outcome = coroPhysicalOutcomeReturn + case *ssa.Defer: + if !coroPhysicalCleanupContainsDefer(cleanup, instruction) { + result.outcomeFailure = "defer registration is absent from the frozen cleanup plan" + return + } + result.outcome = coroPhysicalOutcomeDeferRegister + case *ssa.RunDefers: + if cleanup == nil || len(cleanup.sites) == 0 { + result.outcomeFailure = "RunDefers has no frozen cleanup plan" + return + } + result.outcome = coroPhysicalOutcomeRunDefers + case *ssa.Panic: + if coroSyntheticSelectNoCasePanic(instruction) { + result.outcome = coroPhysicalOutcomeSyntheticSelectTrap + return + } + if !capabilities.explicitPanic { + result.outcomeFailure = "explicit panic requires the explicit-status panic ABI" + return + } + if reason := validateCoroExplicitStatusPanic(audit, instruction); reason != "" { + result.outcomeFailure = reason + return + } + result.outcome = coroPhysicalOutcomePanic + case *ssa.Call: + if !isCoroRecoverBuiltinCall(instruction) { + return + } + if !capabilities.explicitPanic { + result.outcomeFailure = "recover builtin requires the explicit-status panic ABI" + return + } + result.outcome = coroPhysicalOutcomeRecover + } +} + +func coroPhysicalCleanupContainsDefer(cleanup *coroStaticCleanupPlan, instruction *ssa.Defer) bool { + if cleanup == nil || instruction == nil { + return false + } + for _, site := range cleanup.sites { + if site != nil && site.instruction == instruction { + return true + } + } + return false +} + +func coroPhysicalCleanupContainsTerminalAllocation(cleanup *coroStaticCleanupPlan, allocation *ssa.Alloc) bool { + if cleanup == nil || allocation == nil { + return false + } + for _, candidate := range cleanup.terminalResultAllocations { + if candidate == allocation { + return true + } + } + return false +} + +func planCoroPhysicalOperationInstruction( + audit *coroPhysicalPureSSAAudit, + whole *coro.SSAPlan, + instruction ssa.Instruction, + capabilities coroPhysicalLoweringCapabilities, + result *coroPhysicalInstructionPlan, +) { + if audit == nil || result == nil || instruction == nil || instruction.Parent() != audit.fn { + panic("physical operation planning requires one exact instruction plan") + } + failChannel := func(operation string) bool { + if capabilities.channel { + return false + } + result.operationFailure = operation + " requires the channel scheduler capability" + return true + } + switch instruction := instruction.(type) { + case *ssa.Send: + if failChannel("channel send") { + return + } + if err := validateCoroPhysicalChannelType(instruction.Chan.Type()); err != nil { + result.operationFailure = "channel send type: " + err.Error() + return + } + result.operation = coroPhysicalOperationChannelSend + case *ssa.UnOp: + if instruction.Op != token.ARROW { + return + } + if failChannel("channel receive") { + return + } + if err := validateCoroPhysicalChannelType(instruction.X.Type()); err != nil { + result.operationFailure = "channel receive type: " + err.Error() + return + } + result.operation = coroPhysicalOperationChannelReceive + case *ssa.Select: + if failChannel("channel select") { + return + } + for index, state := range instruction.States { + if state == nil { + result.operationFailure = fmt.Sprintf("channel select case %d is nil", index) + return + } + if state.Chan == nil { + result.operationFailure = fmt.Sprintf("channel select case %d channel is nil", index) + return + } + if err := validateCoroPhysicalChannelType(state.Chan.Type()); err != nil { + result.operationFailure = fmt.Sprintf("channel select case %d type: %v", index, err) + return + } + } + if instruction.Blocking { + result.operation = coroPhysicalOperationChannelSelectPark + } else { + result.operation = coroPhysicalOperationChannelSelectTry + } + case *ssa.Call: + if isCoroCloseBuiltinCall(instruction) { + if failChannel("channel close") { + return + } + if !capabilities.explicitPanic { + result.operationFailure = "channel close requires the explicit-status panic ABI" + return + } + if len(instruction.Common().Args) != 1 { + result.operationFailure = "channel close requires one exact channel operand" + return + } + if err := validateCoroPhysicalChannelType(instruction.Common().Args[0].Type()); err != nil { + result.operationFailure = "channel close type: " + err.Error() + return + } + result.operation = coroPhysicalOperationChannelClose + return + } + if audit.universe != nil && audit.universe.coroProgramIR != nil { + frozen, found, err := audit.universe.coroProgramIR.callSitePlan(instruction) + if err != nil { + result.operationFailure = "load worker syscall SitePlan: " + err.Error() + return + } + if found && frozen.plan.Intrinsic && isLLGoSyscallIntrinsic(frozen.opcode) && + frozen.plan.IntrinsicSemantics == CoroIntrinsicCallInlineSuspend { + if !capabilities.worker { + result.operationFailure = "worker llgo.syscall requires the bounded worker capability" + return + } + if err := validateCoroWorkerSyscallCall(whole, audit.universe, instruction); err != nil { + result.operationFailure = "invalid worker llgo.syscall capability: " + err.Error() + return + } + result.operation = coroPhysicalOperationWorkerSyscall + return + } + } + shape, recognized, err := validateCoroWorkerForeignCall( + whole, audit.universe, instruction, coroWorkerTargetPointerSize(audit.universe), + ) + if !recognized { + return + } + if !capabilities.worker { + result.operationFailure = "blocking foreign call requires the bounded worker capability" + return + } + if err != nil { + result.operationFailure = "invalid bounded worker foreign call: " + err.Error() + return + } + result.operation = coroPhysicalOperationWorkerForeign + result.operationWorker = &shape + } +} + +// planCoroPhysicalControlInstruction is the sole post-analysis selector for +// direct child-await and source goroutine-spawn control recipes. It records a +// non-hard await mismatch so the validator can still consider the established +// direct-plain path; once an await target was recognized, ABI failures are hard +// and cannot silently fall back. Every spawn mismatch is hard because a source +// Go instruction has no ordinary synchronous lowering. +func planCoroPhysicalControlInstruction( + audit *coroPhysicalPureSSAAudit, + whole *coro.SSAPlan, + instruction ssa.Instruction, + capabilities coroPhysicalLoweringCapabilities, + result *coroPhysicalInstructionPlan, +) { + if audit == nil || result == nil || instruction == nil || instruction.Parent() != audit.fn { + panic("physical control planning requires one exact instruction plan") + } + switch instruction := instruction.(type) { + case *ssa.Call: + if !capabilities.childAwait || whole == nil { + return + } + common := instruction.Common() + callPlan, callPlanned := whole.CallPlan(instruction) + if common != nil && common.IsInvoke() { + result.controlFailureHard = true + if capabilities.managedInterface.acceptsCall(instruction) { + if !callPlanned || callPlan.Rep != coro.Dispatch { + result.controlFailure = "managed interface descriptor call lost its frozen Dispatch CallPlan" + return + } + if callPlan.Open { + if err := validateCoroManagedInterfaceDispatchCall( + whole, audit.universe, audit.fn, instruction, callPlan, + ); err != nil { + result.controlFailure = "invalid managed interface await: " + err.Error() + return + } + } + signature, err := coroInterfaceDispatchSourceSignature(common) + if err != nil { + result.controlFailure = "managed interface await signature: " + err.Error() + return + } + result.control = coroPhysicalControlManagedInterfaceAwait + result.controlSignature = signature + return + } + if !capabilities.explicitPanic { + if _, err := resolveCoroClosedInterfacePlainCall(whole, instruction); err == nil { + result.controlFailureHard = false + return + } + } + if capabilities.interfacePlain.acceptsCall(instruction) { + result.controlFailureHard = false + return + } + dispatch, err := resolveCoroInterfaceDispatchPlan(whole, audit.universe, instruction) + if err != nil { + result.controlFailure = "unsupported interface invoke: " + err.Error() + return + } + if !coroInterfaceDispatchNeedsAwait(dispatch) { + result.controlFailure = "unsupported interface invoke: closed interface dispatch has no coroutine target" + return + } + result.control = coroPhysicalControlClosedInterfaceAwait + result.controlInterface = dispatch + return + } + if callPlanned && callPlan.Rep == coro.Dispatch && common != nil && common.StaticCallee() == nil { + result.controlFailureHard = true + if !capabilities.managedDispatch { + result.controlFailure = "managed descriptor call requires the v1 descriptor dispatch capability" + return + } + if callPlan.SyncDispatch { + if err := validateCoroPlainDispatchCall(whole, audit.fn, instruction, callPlan, audit.universe); err != nil { + result.controlFailure = "invalid synchronous descriptor call: " + err.Error() + return + } + result.control = coroPhysicalControlPlainDispatch + return + } + if err := validateCoroManagedDispatchCall(whole, audit.fn, instruction, callPlan, audit.universe); err != nil { + result.controlFailure = "invalid managed descriptor await: " + err.Error() + return + } + if err := validateCoroManagedDispatchAwaitShape(whole, audit.fn, instruction, callPlan); err != nil { + result.controlFailure = "invalid managed descriptor await: " + err.Error() + return + } + result.control = coroPhysicalControlDispatchAwait + return + } + callerPlan, found := whole.FunctionPlan(audit.fn) + if !found { + result.controlFailure = "current function has no compilation plan" + return + } + callee, targetPlan, err := resolveCoroStaticAwait(whole, callerPlan, instruction, audit.universe) + if err != nil { + result.controlFailure = err.Error() + return + } + calleeSignature := coroPhysicalNormalizeSourceSignature(callee.Signature) + if audit.universe != nil { + calleeSignature, err = audit.universe.coroPhysicalSourceSignature(callee) + } + if err == nil { + err = validateCoroLeafPhysicalSignature(targetPlan, calleeSignature) + } + if err != nil { + result.controlFailure = "child await signature: " + err.Error() + result.controlFailureHard = true + return + } + result.control = coroPhysicalControlDirectAwait + result.controlTarget = callee + result.controlTargetID = targetPlan.ID + case *ssa.Go: + result.controlFailureHard = true + if !capabilities.staticSpawn { + result.controlFailure = "goroutine spawn requires the closed-static scheduler capability" + return + } + if whole == nil { + result.controlFailure = "goroutine spawn requires one compilation plan" + return + } + callPlan, found := whole.CallPlan(instruction) + if !found { + result.controlFailure = "goroutine spawn has no compilation CallPlan" + return + } + switch callPlan.Rep { + case coro.DirectCoro: + target, targetPlan, err := resolveCoroDirectStaticSpawn(whole, instruction, capabilities.managedDispatch) + if err != nil { + result.controlFailure = "unsupported closed static spawn: " + err.Error() + return + } + targetSignature := coroPhysicalNormalizeSourceSignature(target.Signature) + if audit.universe != nil { + targetSignature, err = audit.universe.coroPhysicalSourceSignature(target) + } + if err == nil { + err = validateCoroLeafPhysicalSignature(targetPlan, targetSignature) + } + if err != nil { + result.controlFailure = "spawn target signature: " + err.Error() + return + } + result.control = coroPhysicalControlDirectSpawn + result.controlTarget = target + result.controlTargetID = targetPlan.ID + case coro.Dispatch: + if !capabilities.managedDispatch { + result.controlFailure = "managed descriptor spawn requires the v1 descriptor dispatch capability" + return + } + if _, err := whole.ResolveManagedDispatchSpawn(instruction); err != nil { + result.controlFailure = "unsupported managed descriptor spawn: " + err.Error() + return + } + if err := validateCoroManagedDispatchSignatureShape(instruction.Common().Signature()); err != nil { + result.controlFailure = "managed descriptor spawn signature: " + err.Error() + return + } + result.control = coroPhysicalControlDispatchSpawn + default: + result.controlFailure = "goroutine spawn has unsupported representation " + callPlan.Rep.String() + } + } +} + +func coroPhysicalContainerPlan(audit *coroPhysicalPureSSAAudit, value ssa.Value) (coroPhysicalContainerKind, int64, error) { + if audit == nil || value == nil || value.Type() == nil { + return coroPhysicalContainerNone, 0, fmt.Errorf("physical container has no exact type") + } + switch container := types.Unalias(audit.typeOf(value.Type())).Underlying().(type) { + case *types.Basic: + if coroPhysicalStringBasic(container) { + return coroPhysicalContainerString, 0, nil + } + case *types.Array: + return coroPhysicalContainerArray, container.Len(), nil + case *types.Slice: + return coroPhysicalContainerSlice, 0, nil + case *types.Pointer: + if array, ok := types.Unalias(container.Elem()).Underlying().(*types.Array); ok { + return coroPhysicalContainerArrayPointer, array.Len(), nil + } + } + return coroPhysicalContainerNone, 0, fmt.Errorf("unsupported physical container type %s", audit.typeOf(value.Type())) +} + +func coroPhysicalSafeFixedArrayIndex( + audit *coroPhysicalPureSSAAudit, + whole *coro.SSAPlan, + operation ssa.Instruction, + collection, index ssa.Value, +) (bool, error) { + if audit == nil || operation == nil || collection == nil || index == nil || operation.Parent() != audit.fn { + return false, fmt.Errorf("safe fixed-array plan requires exact source operands") + } + if whole == nil { + // Structural validators have no frozen optimization fact. Preserve the + // checked recipe; active Compilation paths always provide the SSAPlan. + return false, nil + } + bound, fixed := coroPhysicalFixedArrayBound(audit, collection) + recomputed := fixed && coro.ProveSSAExactSafeFixedArrayIndex(operation.Parent(), index, bound, operation) + plannedBound, planned := whole.ExactSafeFixedArrayIndex(operation) + if planned != recomputed || planned && plannedBound != bound { + return false, fmt.Errorf( + "safe fixed-array index disagrees between CoroPlan and physical projection (planned=%t bound=%d recomputed=%t bound=%d)", + planned, plannedBound, recomputed, bound, + ) + } + return planned, nil +} + +func coroPhysicalFixedArrayBound(audit *coroPhysicalPureSSAAudit, collection ssa.Value) (int64, bool) { + if audit == nil || collection == nil || collection.Type() == nil { + return 0, false + } + switch container := types.Unalias(audit.typeOf(collection.Type())).Underlying().(type) { + case *types.Array: + return container.Len(), true + case *types.Pointer: + if array, ok := types.Unalias(container.Elem()).Underlying().(*types.Array); ok { + return array.Len(), true + } + } + return 0, false +} diff --git a/cl/coro_physical_plan_test.go b/cl/coro_physical_plan_test.go new file mode 100644 index 0000000000..b11d4d0afd --- /dev/null +++ b/cl/coro_physical_plan_test.go @@ -0,0 +1,384 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + "golang.org/x/tools/go/ssa" +) + +func TestCoroPhysicalPlanRuntimeHelperElisionIsRecipeOwned(t *testing.T) { + tests := []struct { + name string + plan coroPhysicalInstructionPlan + helper string + want bool + }{ + {name: "ordinary keeps helper", helper: "NewSlice2"}, + {name: "slice two index", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionSlice}, helper: "NewSlice2", want: true}, + {name: "slice three index", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionSlice}, helper: "NewSlice3Bounds", want: true}, + {name: "slice keeps unrelated", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionSlice}, helper: "AllocU"}, + {name: "index range", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionIndex}, helper: "CheckIndexRange", want: true}, + {name: "deref nil", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionDeref}, helper: "AssertNilDeref", want: true}, + {name: "slice conversion", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionSliceToArrayPointer}, helper: "PanicSliceConvert", want: true}, + {name: "wrapper nil", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionBuiltinNilGuard}, helper: "PanicWrapNilPointer", want: true}, + {name: "unsafe slice", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionUnsafeSlice}, helper: "AssertRuntimeError", want: true}, + {name: "interface nil", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionInterfaceNilCompare}, helper: "EfaceEqual", want: true}, + {name: "frame allocation", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionFrameAllocation}, helper: "AllocZ", want: true}, + {name: "frame allocation keeps unrelated", plan: coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionFrameAllocation}, helper: "AllocU"}, + {name: "panic outcome", plan: coroPhysicalInstructionPlan{outcome: coroPhysicalOutcomePanic}, helper: "Panic", want: true}, + {name: "recover outcome", plan: coroPhysicalInstructionPlan{outcome: coroPhysicalOutcomeRecover}, helper: "Recover", want: true}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + if got := test.plan.elidesRuntimeHelper(test.helper); got != test.want { + t.Fatalf("elidesRuntimeHelper(%q) = %t, want %t", test.helper, got, test.want) + } + }) + } +} + +func TestCoroPhysicalPlanStageIsAtomicExactAndSingleCommit(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func Root(value int) int { return value + 1 } +`) + root := ssaPkg.Func("Root") + owner := &preparedEmissionPackage{identity: "foo"} + physical := &coroPhysicalFunctionPlan{ + function: root, + owner: owner, + instructions: make(map[ssa.Instruction]coroPhysicalInstructionPlan), + } + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + physical.instructions[instruction] = coroPhysicalInstructionPlan{recipe: coroPhysicalInstructionOrdinary} + } + } + key := emissionFunctionOwnerKey{function: root, owner: owner} + expected := map[emissionFunctionOwnerKey]none{key: {}} + + stage := newCoroPhysicalPlanStage() + if err := stage.freezePhysicalFunctionPlan(physical); err != nil { + t.Fatal(err) + } + if err := stage.freezePhysicalFunctionPlan(physical); err == nil || !strings.Contains(err.Error(), "frozen more than once") { + t.Fatalf("duplicate physical freeze = %v", err) + } + + missing := newCoroProgramIR() + missing.callsFrozen = true + if err := missing.commitPhysicalFunctionPlans(newCoroPhysicalPlanStage(), expected); err == nil || + !strings.Contains(err.Error(), "has 0 function owners, want 1") { + t.Fatalf("incomplete physical commit = %v", err) + } + if missing.physicalPlansSealed || len(missing.physicalPlans) != 0 { + t.Fatal("failed physical commit mutated ProgramIR") + } + + ir := newCoroProgramIR() + ir.callsFrozen = true + if err := ir.commitPhysicalFunctionPlans(stage, expected); err != nil { + t.Fatal(err) + } + if loaded, err := ir.physicalFunctionPlan(root, owner); err != nil || loaded != physical { + t.Fatalf("frozen physical lookup = %p, %v; want %p", loaded, err, physical) + } + if err := ir.commitPhysicalFunctionPlans(stage, expected); err == nil || !strings.Contains(err.Error(), "committed more than once") { + t.Fatalf("second physical commit = %v", err) + } +} + +func TestCoroPhysicalEmissionGeneratedWrapperBorrowsSharedFrozenOwner(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func Root(value int) int { return value + 1 } +`) + wrapper := ssaPkg.Func("Root") + wrapper.Pkg = nil + wrapper.Synthetic = "wrapper for test" + instruction := wrapper.Blocks[0].Instrs[0] + declaring := &preparedEmissionPackage{identity: "declaring"} + consumer := &preparedEmissionPackage{identity: "consumer"} + physical := &coroPhysicalFunctionPlan{ + function: wrapper, + owner: declaring, + } + key := emissionFunctionOwnerKey{function: wrapper, owner: declaring} + ir := newCoroProgramIR() + ir.physicalPlans = map[emissionFunctionOwnerKey]*coroPhysicalFunctionPlan{key: physical} + ir.physicalPlansSealed = true + ir.siteOwners[key] = none{} + ir.sitePlans[key] = map[ssa.Instruction]coroEmissionSitePlan{ + instruction: {}, + } + universe := &EmissionUniverse{ + aliases: make(map[*ssa.Function]*ssa.Function), + physicalNames: map[emissionFunctionOwnerKey]string{key: "shared.wrapper"}, + useOwners: map[*ssa.Function]map[*preparedEmissionPackage]none{ + wrapper: {declaring: {}}, + }, + coroProgramIR: ir, + } + + loaded, err := (emissionCanonicalIndex{universe: universe}).physicalFunctionPlanForEmission(wrapper, consumer) + if err != nil || loaded != physical { + t.Fatalf("cross-owner wrapper physical plan = %p, %v; want %p", loaded, err, physical) + } + ctx := &context{ + emissionOwner: consumer, + coroEmission: &coroPhysicalEmissionSession{ + phase: coroPhysicalEmissionPrologue, + plan: loaded, + }, + } + if _, err := ir.sitePlan(ctx, instruction); err != nil { + t.Fatalf("physical-session SitePlan lookup = %v", err) + } + + ordinary := *wrapper + ordinary.Synthetic = "" + if _, err := (emissionCanonicalIndex{universe: universe}).physicalFunctionPlanForEmission(&ordinary, consumer); err == nil || + !strings.Contains(err.Error(), "has no frozen physical plan") { + t.Fatalf("ordinary cross-owner lookup = %v", err) + } +} + +func TestCoroPhysicalFieldAddrRecipeCoversConstantUnreachableCodegen(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +type Box struct { Value int } +func Address(box *Box) *int { return &box.Value } +`) + function := ssaPkg.Func("Address") + var field *ssa.FieldAddr + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.FieldAddr); ok { + field = candidate + } + } + } + if field == nil { + t.Fatal("fixture has no FieldAddr") + } + owner := &preparedEmissionPackage{identity: "foo"} + key := emissionFunctionOwnerKey{function: function, owner: owner} + ir := newCoroProgramIR() + ir.siteOwners[key] = none{} + ir.sitePlans[key] = map[ssa.Instruction]coroEmissionSitePlan{ + field: { + managedRuntimeHelpers: []coroPlannedRuntimeHelper{{ + name: "AssertNilDeref", + placement: coroRuntimeHelperAtSource, + }}, + }, + } + audit := &coroPhysicalPureSSAAudit{ + universe: &EmissionUniverse{coroProgramIR: ir}, + ctx: &context{emissionOwner: owner}, + fn: function, + reachableBlocks: map[*ssa.BasicBlock]bool{field.Block(): false}, + } + guard, reason := audit.fieldAddrRequiresImplicitNilFault(field) + if reason != "" || !guard { + t.Fatalf("constant-unreachable FieldAddr guard = %t, %q; want true", guard, reason) + } +} + +func TestCoroPhysicalRecipeObserverRejectsMissingAndMismatch(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func Root(value int) int { return value + 1 } +`) + root := ssaPkg.Func("Root") + instruction := root.Blocks[0].Instrs[0] + owner := &preparedEmissionPackage{identity: "foo"} + physical := &coroPhysicalFunctionPlan{ + function: root, + owner: owner, + instructions: map[ssa.Instruction]coroPhysicalInstructionPlan{ + instruction: { + semantic: coroSemanticInstructionPlan{recipe: coro.RecipeID("test.semantic.v0")}, + recipe: coroPhysicalInstructionDeref, + nilGuard: true, + }, + }, + } + ctx := &context{ + compilation: &Compilation{}, + emissionUniverse: &EmissionUniverse{coroProgramIR: newCoroProgramIR()}, + coroEmission: &coroPhysicalEmissionSession{ + phase: coroPhysicalEmissionPrologue, + plan: physical, + }, + } + + missingSemantic := captureCoroSitePlanPanic(func() { ctx.beginCoroSiteEmission(instruction)() }) + if !strings.Contains(missingSemantic, "omitted frozen semantic recipe test.semantic.v0") { + t.Fatalf("missing semantic observation = %q", missingSemantic) + } + missing := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + }) + if !strings.Contains(missing, "omitted frozen physical recipe deref") { + t.Fatalf("missing physical observation = %q", missing) + } + mismatch := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalInstruction(instruction, coroPhysicalInstructionIndex) + }) + if !strings.Contains(mismatch, "emitted physical recipe index, frozen SitePlan requires deref") { + t.Fatalf("mismatched physical observation = %q", mismatch) + } + missingGuard := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalInstruction(instruction, coroPhysicalInstructionDeref) + }) + if !strings.Contains(missingGuard, "physical nil-guard emission=false, frozen SitePlan requires true") { + t.Fatalf("missing physical guard observation = %q", missingGuard) + } + func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalInstruction(instruction, coroPhysicalInstructionDeref) + ctx.observeCoroPhysicalNilGuard(instruction) + }() + + physical.instructions[instruction] = coroPhysicalInstructionPlan{ + semantic: coroSemanticInstructionPlan{recipe: coro.RecipeID("test.control.v0")}, + control: coroPhysicalControlDirectAwait, + } + missingControl := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + }) + if !strings.Contains(missingControl, "omitted frozen physical control recipe direct-await") { + t.Fatalf("missing physical control observation = %q", missingControl) + } + mismatchedControl := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalControl(instruction, coroPhysicalControlDispatchSpawn) + }) + if !strings.Contains(mismatchedControl, "emitted physical control recipe dispatch-spawn, frozen SitePlan requires direct-await") { + t.Fatalf("mismatched physical control observation = %q", mismatchedControl) + } + func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalControl(instruction, coroPhysicalControlDirectAwait) + }() + + physical.instructions[instruction] = coroPhysicalInstructionPlan{ + semantic: coroSemanticInstructionPlan{recipe: coro.RecipeID("test.operation.v0")}, + operation: coroPhysicalOperationChannelSelectPark, + } + missingOperation := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + }) + if !strings.Contains(missingOperation, "omitted frozen physical operation recipe channel-select-park") { + t.Fatalf("missing physical operation observation = %q", missingOperation) + } + mismatchedOperation := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalOperation(instruction, coroPhysicalOperationChannelSend) + }) + if !strings.Contains(mismatchedOperation, "emitted physical operation recipe channel-send, frozen SitePlan requires channel-select-park") { + t.Fatalf("mismatched physical operation observation = %q", mismatchedOperation) + } + func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalOperation(instruction, coroPhysicalOperationChannelSelectPark) + }() + + physical.instructions[instruction] = coroPhysicalInstructionPlan{ + semantic: coroSemanticInstructionPlan{recipe: coro.RecipeID("test.outcome.v0")}, + outcome: coroPhysicalOutcomePanic, + } + missingOutcome := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + }) + if !strings.Contains(missingOutcome, "omitted frozen physical outcome recipe panic") { + t.Fatalf("missing physical outcome observation = %q", missingOutcome) + } + mismatchedOutcome := captureCoroSitePlanPanic(func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalOutcome(instruction, coroPhysicalOutcomeReturn) + }) + if !strings.Contains(mismatchedOutcome, "emitted physical outcome recipe return, frozen SitePlan requires panic") { + t.Fatalf("mismatched physical outcome observation = %q", mismatchedOutcome) + } + func() { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSemanticInstruction(instruction) + ctx.observeCoroPhysicalOutcome(instruction, coroPhysicalOutcomePanic) + }() +} + +func TestCoroPhysicalCodegenRejectsMissingCommittedPlan(t *testing.T) { + prog, ssaPkg, files, universe, plan := prepareCoroPhysicalValueTransportABI(t, nil) + defer prog.Dispose() + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.FuncRepABI = coro.FuncRepABIV1 + if err := compilation.preflightCoroPlan(); err != nil { + t.Fatal(err) + } + for key := range universe.coroProgramIR.physicalPlans { + delete(universe.coroProgramIR.physicalPlans, key) + } + message := captureCoroSitePlanPanic(func() { + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if pkg != nil { + pkg.Module().Dispose() + } + if err != nil { + panic(err) + } + }) + if !strings.Contains(message, "has no frozen physical plan") { + t.Fatalf("missing physical plan codegen failure = %q", message) + } +} diff --git a/cl/coro_physical_transport_test.go b/cl/coro_physical_transport_test.go new file mode 100644 index 0000000000..3e0b990a13 --- /dev/null +++ b/cl/coro_physical_transport_test.go @@ -0,0 +1,94 @@ +//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/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" +) + +func TestCoroPhysicalTransportTypeSeparatesRawCAndManagedFunctions(t *testing.T) { + const source = `package foo + +//llgo:type C +type CFunc func(int) int + +type RawBox struct { Callback CFunc } + +func Root(callback CFunc, box RawBox) {} +func Managed(callback func(int) int) {} +` + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + defer prog.Dispose() + ParsePkgSyntax(prog, ssaPkg.Pkg, files) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + + root := ssaPkg.Func("Root") + managedRoot := ssaPkg.Func("Managed") + rawType := root.Signature.Params().At(0).Type() + rawBoxType := root.Signature.Params().At(1).Type() + managedType := managedRoot.Signature.Params().At(0).Type() + pointerType := types.Typ[types.UnsafePointer] + + rawKey := coroPhysicalTransportTypeKey(universe, rawType) + managedKey := coroPhysicalTransportTypeKey(universe, managedType) + pointerKey := coroPhysicalTransportTypeKey(universe, pointerType) + if rawKey == managedKey { + t.Fatalf("raw C and managed function transports share key %q", rawKey) + } + if rawKey != pointerKey { + t.Fatalf("raw C transport key = %q, opaque pointer key = %q; want the same one-word ABI", rawKey, pointerKey) + } + + managedBoxType := types.NewStruct( + []*types.Var{types.NewField(token.NoPos, nil, "Callback", managedType, false)}, + []string{""}, + ) + if rawBoxKey, managedBoxKey := coroPhysicalTransportTypeKey(universe, rawBoxType), coroPhysicalTransportTypeKey(universe, managedBoxType); rawBoxKey == managedBoxKey { + t.Fatalf("nested raw C and managed function transports share key %q", rawBoxKey) + } + + signature := func(params ...types.Type) *types.Signature { + variables := make([]*types.Var, len(params)) + for index, typ := range params { + variables[index] = types.NewParam(token.NoPos, nil, "", typ) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(variables...), root.Signature.Results(), false) + } + plan := coro.FunctionPlan{ID: "foo.Root"} + if err := validateCoroPhysicalSSAParameterShape(plan, root, signature(pointerType, rawBoxType), universe); err != nil { + t.Fatalf("exact raw-C-to-pointer physical alias was rejected: %v", err) + } + if err := validateCoroPhysicalSSAParameterShape(plan, root, signature(managedType, rawBoxType), universe); err == nil || + !strings.Contains(err.Error(), "effective parameter 0") { + t.Fatalf("raw-C-to-managed descriptor mismatch = %v, want parameter 0 rejection", err) + } + if err := validateCoroPhysicalSSAParameterShape(plan, root, signature(rawType, managedBoxType), universe); err == nil || + !strings.Contains(err.Error(), "effective parameter 1") { + t.Fatalf("nested raw-C-to-managed descriptor mismatch = %v, want parameter 1 rejection", err) + } +} diff --git a/cl/coro_poll_wait.go b/cl/coro_poll_wait.go new file mode 100644 index 0000000000..a9d0a1ff25 --- /dev/null +++ b/cl/coro_poll_wait.go @@ -0,0 +1,122 @@ +/* + * 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" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroPollParkHookV2 = "__llgo_coro_poll_park_v2" + coroPollResumeHookV2 = "__llgo_coro_poll_resume_v2" +) + +const ( + coroPollResumeReadyV2 uint64 = iota + 1 + coroPollResumeClosingV2 + coroPollResumeTimeoutV2 + coroPollResumeOperationCanceledV2 + coroPollResumeTaskAbortV2 + coroPollResumeShutdownV2 +) + +func coroPollParkSignatureV2() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + types.NewParam(token.NoPos, nil, "header", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + types.NewParam(token.NoPos, nil, "context", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, nil, "fd", types.Typ[types.Int32]), + types.NewParam(token.NoPos, nil, "interest", types.Typ[types.Uint32]), + types.NewParam(token.NoPos, nil, "deadline", types.Typ[types.Int64]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroPollResumeSignatureV2() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + ) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "status", types.Typ[types.Uint32])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func (p *context) requireCoroPollWaitBody(b llssa.Builder) *coroBodyContext { + return p.requireCoroParkV2Body(b, "poll wait") +} + +// compileCoroPollWait lowers one synchronous source-style descriptor wait into +// a compiler-owned PollParkV2 transaction. Only the copied scalar descriptor +// identity crosses the stack cut. The opaque source, WaitSet, lease, and +// cancellation state stay in fixed typed storage that LLVM spills into the +// stackless coroutine frame. +func (p *context) compileCoroPollWait(b llssa.Builder, args []ssa.Value) llssa.Expr { + body := p.requireCoroPollWaitBody(b) + if len(args) != 4 { + panic("llgo.coroPollWait requires exactly (uintptr, int32, uint32, int64) arguments") + } + context := p.compileValue(b, args[0]) + fd := p.compileValue(b, args[1]) + interest := p.compileValue(b, args[2]) + deadline := p.compileValue(b, args[3]) + state := b.Alloc(p.prog.RuntimeType("CoroPollParkV2"), false) + result := b.Alloc(p.prog.Uint32(), false) + + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.Prog.BoolVal(true), + park: func(suspend llssa.Builder) { + park := p.pkg.NewFunc(coroPollParkHookV2, coroPollParkSignatureV2(), llssa.InC) + suspend.Call( + park.Expr, + body.task, + body.coro.Handle(), + suspend.Convert(suspend.Prog.VoidPtr(), body.header), + suspend.Convert(suspend.Prog.VoidPtr(), state), + context, + fd, + interest, + deadline, + ) + }, + resume: func(resume llssa.Builder) llssa.Expr { + resumeHook := p.pkg.NewFunc(coroPollResumeHookV2, coroPollResumeSignatureV2(), llssa.InC) + status := resume.Call( + resumeHook.Expr, + body.task, + resume.Convert(resume.Prog.VoidPtr(), state), + ) + resume.Store(result, status) + return status + }, + normal: []uint64{ + coroPollResumeReadyV2, + coroPollResumeClosingV2, + coroPollResumeTimeoutV2, + }, + abort: coroPollResumeTaskAbortV2, + shutdown: coroPollResumeShutdownV2, + }) + return b.Load(result) +} diff --git a/cl/coro_poll_wait_test.go b/cl/coro_poll_wait_test.go new file mode 100644 index 0000000000..906caf6ab4 --- /dev/null +++ b/cl/coro_poll_wait_test.go @@ -0,0 +1,305 @@ +//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 ( + "bytes" + "go/ast" + "go/importer" + "go/token" + "go/types" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroPollWaitTestSource = `package foo + +import _ "unsafe" + +//go:linkname wait llgo.coroPollWait +func wait(context uintptr, fd int32, interest uint32, deadline int64) uint32 + +func Root(context uintptr, fd int32, interest uint32, deadline int64) uint32 { + return wait(context, fd, interest, deadline) +} +` + +func TestCoroPollWaitIntrinsicRejectsNonCanonicalShape(t *testing.T) { + for _, test := range []struct { + name string + source string + }{ + { + name: "fd", + source: `package pollwaitbadfd +//llgo:link Wait llgo.coroPollWait +func Wait(uintptr, uint32, uint32, int64) uint32 +func Use(context uintptr, fd uint32, interest uint32, deadline int64) uint32 { return Wait(context, fd, interest, deadline) } +`, + }, + { + name: "result", + source: `package pollwaitbadresult +//llgo:link Wait llgo.coroPollWait +func Wait(uintptr, int32, uint32, int64) uint64 +func Use(context uintptr, fd int32, interest uint32, deadline int64) uint64 { return Wait(context, fd, interest, deadline) } +`, + }, + { + name: "arity", + source: `package pollwaitbadarity +//llgo:link Wait llgo.coroPollWait +func Wait(uintptr, int32, uint32) uint32 +func Use(context uintptr, fd int32, interest uint32) uint32 { return Wait(context, fd, interest) } +`, + }, + } { + t.Run(test.name, func(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/coropollwaitbad"+test.name, test.source) + testProg.ssa.Build() + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse( + prog, nil, []EmissionPackage{{SSA: pkg.ssa, Files: []*ast.File{pkg.file}}}, + ) + if err != nil { + t.Fatal(err) + } + calls := allocaCStrTestCalls(pkg.ssa.Func("Use")) + if len(calls) != 1 { + t.Fatalf("bad poll wait fixture calls = %d, want 1", len(calls)) + } + if _, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(calls[0]); err == nil || !intrinsic || !strings.Contains(err.Error(), "coroPollWait") { + t.Fatalf("bad coroPollWait semantics = _, %v, %v; want exact-shape error", intrinsic, err) + } + }) + } +} + +func TestCoroPollWaitCurrentFrameNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root, waitCall := compileCoroPollWaitFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.FuncRep != coro.DirectCoro || + !rootPlan.DeclaredEffect.Contains(coro.MayPark) || !rootPlan.LocalEffect.Contains(coro.MayPark) || + !rootPlan.Effect.Contains(coro.MayPark) { + t.Fatalf("Root plan = %+v, present=%t; want one local poll-park coroutine", rootPlan, ok) + } + if !plan.ElidesCall(waitCall) { + t.Fatal("coroPollWait declaration call is not frozen as a frontend-elided intrinsic site") + } + if _, retained := plan.CallPlan(waitCall); retained { + t.Fatal("coroPollWait declaration unexpectedly retained a managed CallPlan") + } + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify poll wait coroutine before CoroSplit: %v\n%s", err, module.String()) + } + physical := requireCoroPhysicalFunction(t, module, "foo.Root") + body := physical.String() + assertCoroCancellationTerminalStatusPublication(t, physical) + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("Root coro.suspend calls = %d, want initial + poll + final:\n%s", got, body) + } + for _, symbol := range []string{coroPollParkHookV2, coroPollResumeHookV2} { + if got := strings.Count(body, "@"+symbol); got != 1 { + t.Fatalf("Root references to %q = %d, want 1:\n%s", symbol, got, body) + } + } + for _, forbidden := range []string{ + "@foo.wait", "@llgo.coroPollWait", "runtime.AllocZ", + "__llgo_coro_poll_prepare_or_abort_v1", "__llgo_coro_poll_retire_completed_or_abort_v1", + "__llgo_coro_park_prepare_v1", + } { + if strings.Contains(body, forbidden) { + t.Fatalf("Poll V2 lowering retained forbidden V1 call/allocation %q:\n%s", forbidden, body) + } + } + stateAndPark := regexp.MustCompile( + `(?s)store i16 4,.*store i16 3,.*store i32 1,.*call void @` + regexp.QuoteMeta(coroPollParkHookV2) + + `\(ptr [^,]+, ptr [^,]+, ptr [^,]+, ptr [^,]+, i(?:32|64) [^,]+, i32 [^,]+, i32 [^,]+, i64 [^)]+\)`, + ) + if !stateAndPark.MatchString(body) { + t.Fatalf("Root does not publish Park/Suspended/stateID=1 before Poll V2 park:\n%s", body) + } + park := strings.Index(body, "call void @"+coroPollParkHookV2) + suspendRelative := strings.Index(body[park:], "call i8 @llvm.coro.suspend") + resumeRelative := strings.Index(body[park:], "call i32 @"+coroPollResumeHookV2) + if park < 0 || suspendRelative < 0 || resumeRelative < 0 || suspendRelative >= resumeRelative { + t.Fatalf("Root does not park, suspend, then consume Poll V2 status in order:\n%s", body) + } + dispatch := regexp.MustCompile( + `(?s)call i32 @` + regexp.QuoteMeta(coroPollResumeHookV2) + `\([^\n]+\).*?switch i32 [^\[]+\[(.*?)\]`, + ).FindStringSubmatch(body) + if len(dispatch) != 2 { + t.Fatalf("Root has no isolated Poll V2 resume switch:\n%s", body) + } + for _, status := range []uint64{ + coroPollResumeReadyV2, + coroPollResumeClosingV2, + coroPollResumeTimeoutV2, + coroPollResumeTaskAbortV2, + coroPollResumeShutdownV2, + } { + if !regexp.MustCompile(`(?m)^\s+i32 ` + strconv.FormatUint(status, 10) + `, label `).MatchString(dispatch[1]) { + t.Fatalf("Root Poll V2 resume switch lacks status %d:\n%s", status, dispatch[0]) + } + } + if regexp.MustCompile(`(?m)^\s+i32 ` + strconv.FormatUint(coroPollResumeOperationCanceledV2, 10) + `, label `).MatchString(dispatch[1]) { + t.Fatalf("ordinary poll wait silently accepts operation-only cancellation:\n%s", dispatch[0]) + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), "call i32 @"+coroPollResumeHookV2) { + t.Fatalf("CoroSplit lost Poll V2 resume dispatch:\n%s", module.String()) + } + assertCoroCancellationTerminalStatusPublication(t, resume) + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit poll wait object: %v\n%s", err, module.String()) + } + defer object.Dispose() + for _, symbol := range []string{coroPollParkHookV2, coroPollResumeHookV2} { + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(symbol)) { + t.Fatalf("post-CoroSplit object lost Poll V2 ABI symbol %q", symbol) + } + } + }) + } +} + +func compileCoroPollWaitFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, *ssa.Call, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroPollWaitTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + prog.SetRuntime(func() *types.Package { + runtimePackage, err := importer.For("source", nil).Import(llssa.PkgRuntime) + if err != nil { + t.Fatal("load runtime failed:", err) + } + if runtimePackage.Scope().Lookup("CoroPollParkV2") == nil { + name := types.NewTypeName(token.NoPos, runtimePackage, "CoroPollParkV2", nil) + types.NewNamed(name, types.NewArray(types.Typ[types.Uintptr], 32), nil) + if previous := runtimePackage.Scope().Insert(name); previous != nil { + t.Fatalf("install Poll V2 test runtime type: duplicate %v", previous) + } + } + return runtimePackage + }) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + var waitCall *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Call.StaticCallee() != nil && call.Call.StaticCallee().Name() == "wait" { + waitCall = call + } + } + } + if waitCall == nil { + prog.Dispose() + t.Fatal("fixture has no direct coroPollWait call") + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(waitCall) + if err != nil || !intrinsic || semantics != CoroIntrinsicCallInlineSuspend { + prog.Dispose() + t.Fatalf("coroPollWait semantics = %v, %t, %v; want InlineSuspend, true, nil", semantics, intrinsic, err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.MayPark}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + CoroFrameRetentionABI: CoroFrameRetentionParkABIV2, + } + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root, waitCall +} diff --git a/cl/coro_print_builtin_test.go b/cl/coro_print_builtin_test.go new file mode 100644 index 0000000000..998dc02c28 --- /dev/null +++ b/cl/coro_print_builtin_test.go @@ -0,0 +1,366 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroPrintRuntimeFixture = `package runtime +import "unsafe" + +type String struct { + Data unsafe.Pointer + Len int +} + +func PrintByte(byte) {} +func PrintInt(int64) {} +func PrintFloat(float64) {} +func PrintString(String) {} +` + +const coroPrintFixture = `package foo + +import "unsafe" + +func scalarBitcast32(value int32) float32 { + return *(*float32)(unsafe.Pointer(&value)) +} + +func scalarBitcast64(value int64) float64 { + return *(*float64)(unsafe.Pointer(&value)) +} + +var escapedScalar float32 + +func returnTransformed(pointer unsafe.Pointer) float32 { + return scalarBitcast32(int32(uintptr(pointer))) +} + +func storeTransformed(pointer unsafe.Pointer) { + escapedScalar = scalarBitcast32(int32(uintptr(pointer))) +} + +func arithmeticTransformed(pointer unsafe.Pointer) float32 { + return scalarBitcast32(int32(uintptr(pointer))) + 1 +} + +func Root(number int, text string, pointer unsafe.Pointer) { + print( + "value=", number, int64(uintptr(pointer)), + scalarBitcast32(int32(uintptr(pointer))), + scalarBitcast64(int64(uintptr(pointer))), + ) + println(text) +} +` + +type coroPrintTestPlan struct { + prog llssa.Program + runtimePkg emissionTestPackage + fooPkg emissionTestPackage + universe *EmissionUniverse + plan *coro.SSAPlan + root *ssa.Function + calls map[string]*ssa.Call +} + +func TestCoroPointerDerivedScalarTransformResultsRemainFailClosed(t *testing.T) { + fixture := prepareCoroPrintTestPlan(t, nil, true, false) + defer fixture.prog.Dispose() + for _, name := range []string{"returnTransformed", "storeTransformed", "arithmeticTransformed"} { + function := fixture.fooPkg.ssa.Func(name) + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, function, "") + if err != nil { + t.Fatal(err) + } + found := false + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + conversion, ok := instruction.(*ssa.Convert) + if !ok || !coroFrameRetentionPointerToUintptr(conversion) { + continue + } + found = true + if reason := audit.validateConvert(conversion); !strings.Contains(reason, "not bound to an exact managed-child/worker") { + t.Fatalf("%s pointer conversion rejection = %q", name, reason) + } + } + } + if !found { + t.Fatalf("%s has no pointer-to-uintptr conversion", name) + } + } +} + +func TestCoroPrintBuiltinManagedHelpersNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, target := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(target.name, func(t *testing.T) { + fixture := prepareCoroPrintTestPlan(t, target.target, true, false) + defer fixture.prog.Dispose() + + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + proof := audit.currentFrameRetentionProof() + var pointerWords, integerAliases, transformResults []ssa.Value + for _, block := range fixture.root.Blocks { + for _, instruction := range block.Instrs { + if handled, reason := audit.validate(instruction); handled && reason != "" { + t.Fatalf("%T %q rejected: %s", instruction, instruction, reason) + } + switch instruction := instruction.(type) { + case *ssa.Convert: + if coroFrameRetentionPointerToUintptr(instruction) { + pointerWords = append(pointerWords, instruction) + } else if instruction.X != nil && coroFrameRetentionUintptrLike(instruction.X.Type()) && + coroFrameRetentionIntegerLike(instruction.Type()) { + integerAliases = append(integerAliases, instruction) + } + case *ssa.Call: + if instruction.Common() != nil && instruction.Common().StaticCallee() != nil && + strings.HasPrefix(instruction.Common().StaticCallee().Name(), "scalarBitcast") { + transformResults = append(transformResults, instruction) + } + } + } + } + if len(pointerWords) != 3 || len(integerAliases) != 3 || len(transformResults) != 2 { + t.Fatalf("pointer print chain values = %d words/%d integer aliases/%d transforms, want 3/3/2", + len(pointerWords), len(integerAliases), len(transformResults)) + } + for _, value := range append(append(pointerWords, integerAliases...), transformResults...) { + if !proof.provesTraceableUintptr(value) { + t.Fatalf("pointer-derived print value %q has no frozen provenance", value) + } + } + if got := strings.Join(rootNames(proof.exactCallKeepaliveRoots(fixture.calls["print"])), ","); got != "pointer" { + t.Fatalf("print keepalive roots = %q, want pointer", got) + } + + for _, name := range []string{"PrintByte", "PrintFloat", "PrintInt", "PrintString"} { + helper := fixture.runtimePkg.ssa.Func(name) + plan, ok := fixture.plan.FunctionPlan(helper) + if !ok || plan.External != coro.Defined || plan.Emission != coro.EmitCoroutine || + plan.Primary != coro.PrimaryCoroutine || plan.FuncRep != coro.DirectCoro || + !plan.Demand.Contains(coro.AsyncDemand) || !plan.Effect.MaySuspend() { + t.Fatalf("%s plan = %+v, present=%t; want demanded managed helper", name, plan, ok) + } + } + + compilation := &Compilation{CoroPlan: fixture.plan, EmissionUniverse: fixture.universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + runtimeLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.runtimePkg.ssa, []*ast.File{fixture.runtimePkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile print helpers: %v", err) + } + runtimeModule := runtimeLL.Module() + defer runtimeModule.Dispose() + fooLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.fooPkg.ssa, []*ast.File{fixture.fooPkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile print owner: %v", err) + } + fooModule := fooLL.Module() + defer fooModule.Dispose() + for name, module := range map[string]llvm.Module{"runtime": runtimeModule, "foo": fooModule} { + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify %s before CoroSplit: %v\n%s", name, err, module.String()) + } + } + + body := requireCoroPhysicalFunction(t, fooModule, "foo.Root").String() + for _, helper := range []string{"runtime.PrintByte$coro", "runtime.PrintFloat$coro", "runtime.PrintInt$coro", "runtime.PrintString$coro"} { + if !strings.Contains(body, helper) { + t.Fatalf("print owner lacks managed helper %q:\n%s", helper, body) + } + } + if got := strings.Count(body, "runtime.PrintString$coro"); got != 2 { + t.Fatalf("PrintString calls = %d, want 2:\n%s", got, body) + } + if !strings.Contains(body, "ptrtoint") { + t.Fatalf("print owner lost pointer-to-integer transport:\n%s", body) + } + for _, transform := range []string{"foo.scalarBitcast32", "foo.scalarBitcast64"} { + plain := fooModule.NamedFunction(transform) + if plain.IsNil() || !fooModule.NamedFunction(transform+"$coro").IsNil() || + strings.Contains(plain.String(), "call ") || strings.Contains(plain.String(), "llvm.coro.suspend") { + t.Fatalf("%s is not one call-free plain scalar transform:\n%s", transform, plain.String()) + } + } + if got := strings.Count(body, "call void @"+coroAwaitPrepareHookV1); got != 7 { + t.Fatalf("print helper awaits = %d, want 7:\n%s", got, body) + } + + for _, module := range []llvm.Module{runtimeModule, fooModule} { + runCoroABITestPipeline(t, fixture.prog, module) + } + }) + } +} + +func TestCoroPrintBuiltinFailsClosedForBlockingPlainHelper(t *testing.T) { + fixture := prepareCoroPrintTestPlan(t, nil, true, true) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + if reason := audit.validatePrintBuiltin(fixture.calls["print"], "print"); !strings.Contains(reason, "not one non-suspending, non-unwinding direct plain body") { + t.Fatalf("blocking plain print helper rejection = %q", reason) + } +} + +func TestCoroPrintBuiltinRequiresExactLoweredFacts(t *testing.T) { + fixture := prepareCoroPrintTestPlan(t, nil, false, false) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + if reason := audit.validatePrintBuiltin(fixture.calls["println"], "println"); !strings.Contains(reason, "lacks an exact non-elided lowered-call fact") { + t.Fatalf("missing print lowered-fact rejection = %q", reason) + } +} + +func prepareCoroPrintTestPlan(t *testing.T, target *llssa.Target, loweredCalls, blockingPlain bool) coroPrintTestPlan { + t.Helper() + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, coroPrintRuntimeFixture) + fooPkg := testProg.addPackage(t, "foo", coroPrintFixture) + testProg.ssa.Build() + + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + prog.SetRuntime(runtimePkg.types) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: fooPkg.ssa, Files: []*ast.File{fooPkg.file}}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(fooPkg.ssa.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := fooPkg.ssa.Func("Root") + helpers := map[*ssa.Function]bool{ + runtimePkg.ssa.Func("PrintByte"): true, + runtimePkg.ssa.Func("PrintFloat"): true, + runtimePkg.ssa.Func("PrintInt"): true, + runtimePkg.ssa.Func("PrintString"): true, + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + config := coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if !helpers[function] { + return coro.SSAFunctionPolicy{}, nil + } + if blockingPlain { + return coro.SSAFunctionPolicy{Exec: coro.BlockForeign}, nil + } + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + }, + } + if loweredCalls { + config.ClassifyLoweredCalls = universe.CoroLoweredCalls + } + plan, err := coro.AnalyzeSSA(fooPkg.ssa.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, config) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return coroPrintTestPlan{ + prog: prog, + runtimePkg: runtimePkg, + fooPkg: fooPkg, + universe: universe, + plan: plan, + root: root, + calls: coroPrintBuiltinCalls(t, root), + } +} + +func coroPrintBuiltinCalls(t *testing.T, function *ssa.Function) map[string]*ssa.Call { + t.Helper() + found := make(map[string]*ssa.Call) + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "print" && builtin.Name() != "println" { + continue + } + if found[builtin.Name()] != nil { + t.Fatalf("%s has multiple %s builtins", function, builtin.Name()) + } + found[builtin.Name()] = call + } + } + if found["print"] == nil || found["println"] == nil { + t.Fatalf("%s print calls = %v, want print and println", function, found) + } + return found +} diff --git a/cl/coro_profile_test.go b/cl/coro_profile_test.go new file mode 100644 index 0000000000..7e1268467f --- /dev/null +++ b/cl/coro_profile_test.go @@ -0,0 +1,34 @@ +/* + * 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 llssa "github.com/goplus/llgo/ssa" + +func prepareStacklessEmissionUniverse( + prog llssa.Program, patches Patches, inputs []EmissionPackage, +) (*EmissionUniverse, error) { + return PrepareEmissionUniverseWithOptions(prog, patches, inputs, EmissionUniverseOptions{ + CoroProfile: CoroProfileStackless, + }) +} + +func prepareStacklessEmissionUniverseWithOptions( + prog llssa.Program, patches Patches, inputs []EmissionPackage, options EmissionUniverseOptions, +) (*EmissionUniverse, error) { + options.CoroProfile = CoroProfileStackless + return PrepareEmissionUniverseWithOptions(prog, patches, inputs, options) +} diff --git a/cl/coro_program_ir.go b/cl/coro_program_ir.go new file mode 100644 index 0000000000..069a740768 --- /dev/null +++ b/cl/coro_program_ir.go @@ -0,0 +1,250 @@ +/* + * 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 ( + "fmt" + "slices" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// coroProgramIR owns production SitePlan state. Later replacement cohorts add +// global summaries, physical control and storage projections to this same +// object rather than creating independently-versioned lowering documents. +type coroProgramIR struct { + sitePlans map[emissionFunctionOwnerKey]map[ssa.Instruction]coroEmissionSitePlan + siteOwners map[emissionFunctionOwnerKey]none + semanticPlans map[emissionFunctionOwnerKey]map[ssa.Instruction]coroSemanticInstructionPlan + localBodyFacts map[*ssa.Function]coro.SSAFunctionBodyFacts + callPlans map[ssa.CallInstruction]coroFrozenCallSitePlan + callsFrozen bool + physicalPlans map[emissionFunctionOwnerKey]*coroPhysicalFunctionPlan + physicalPlansSealed bool +} + +func newCoroProgramIR() *coroProgramIR { + return &coroProgramIR{ + sitePlans: make(map[emissionFunctionOwnerKey]map[ssa.Instruction]coroEmissionSitePlan), + siteOwners: make(map[emissionFunctionOwnerKey]none), + semanticPlans: make(map[emissionFunctionOwnerKey]map[ssa.Instruction]coroSemanticInstructionPlan), + localBodyFacts: make(map[*ssa.Function]coro.SSAFunctionBodyFacts), + callPlans: make(map[ssa.CallInstruction]coroFrozenCallSitePlan), + physicalPlans: make(map[emissionFunctionOwnerKey]*coroPhysicalFunctionPlan), + } +} + +func (ir *coroProgramIR) freezeSemanticInstruction( + function *ssa.Function, + owner *preparedEmissionPackage, + instruction ssa.Instruction, +) error { + if ir == nil || function == nil || owner == nil || instruction == nil || instruction.Parent() != function { + return fmt.Errorf("semantic SitePlan requires one exact program IR, owner, and source instruction") + } + key := emissionFunctionOwnerKey{function: function, owner: owner} + if _, sealed := ir.siteOwners[key]; sealed { + return fmt.Errorf("semantic SitePlan for function %q owner %q was added after owner freeze", function.Name(), owner.identity) + } + plan, err := planCoroSemanticInstruction(instruction) + if err != nil { + return err + } + byInstruction := ir.semanticPlans[key] + if byInstruction == nil { + byInstruction = make(map[ssa.Instruction]coroSemanticInstructionPlan) + ir.semanticPlans[key] = byInstruction + } + if previous, exists := byInstruction[instruction]; exists { + if previous != plan { + return fmt.Errorf("source instruction acquired conflicting semantic recipes") + } + return nil + } + byInstruction[instruction] = plan + return nil +} + +func (ir *coroProgramIR) freezeSite(function *ssa.Function, owner *preparedEmissionPackage, instruction ssa.Instruction, plan coroEmissionSitePlan) error { + if ir == nil || function == nil || owner == nil || instruction == nil || instruction.Parent() != function { + return fmt.Errorf("requires one exact program IR, owner, and source instruction") + } + plan = cloneCoroEmissionSitePlan(plan) + key := emissionFunctionOwnerKey{function: function, owner: owner} + byInstruction := ir.sitePlans[key] + if byInstruction == nil { + byInstruction = make(map[ssa.Instruction]coroEmissionSitePlan) + ir.sitePlans[key] = byInstruction + } + if previous, exists := byInstruction[instruction]; exists { + if !sameCoroEmissionSitePlan(previous, plan) { + return fmt.Errorf("source instruction acquired conflicting helper plans") + } + return nil + } + if len(plan.managedRuntimeHelpers) != 0 || len(plan.plainRuntimeHelpers) != 0 { + byInstruction[instruction] = plan + } + return nil +} + +func (ir *coroProgramIR) freezeSiteOwner(function *ssa.Function, owner *preparedEmissionPackage) error { + if ir == nil || function == nil || owner == nil { + return fmt.Errorf("coroutine site plan owner requires an exact program IR, function, and owner") + } + key := emissionFunctionOwnerKey{function: function, owner: owner} + if _, frozen := ir.siteOwners[key]; frozen { + return fmt.Errorf("coroutine site plan owner %q was frozen more than once", function.Name()) + } + semantic := ir.semanticPlans[key] + facts := coro.SSAFunctionBodyFacts{Effect: coro.NoSuspend} + if function.Blocks != nil { + facts.Exec = coro.MayUnwind + } + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + plan, ok := semantic[instruction] + if !ok { + return fmt.Errorf("coroutine semantic SitePlan owner %q omitted source instruction %q", function.Name(), instruction.String()) + } + facts.Effect = facts.Effect.Join(plan.effect) + facts.Exec = facts.Exec.Join(plan.exec) + if !plan.debug { + facts.InstructionCount++ + } + } + } + facts.Effect = facts.Effect.Normalize() + facts.HasCycle = coroSemanticCFGHasCycle(function.Blocks) + if previous, exists := ir.localBodyFacts[function]; exists && previous != facts { + return fmt.Errorf("function %q acquired owner-dependent local semantic facts", function.Name()) + } + ir.localBodyFacts[function] = facts + ir.siteOwners[key] = none{} + return nil +} + +func (ir *coroProgramIR) semanticInstructionPlan( + function *ssa.Function, + owner *preparedEmissionPackage, + instruction ssa.Instruction, +) (coroSemanticInstructionPlan, error) { + if ir == nil || function == nil || owner == nil || instruction == nil || instruction.Parent() != function { + return coroSemanticInstructionPlan{}, fmt.Errorf("semantic SitePlan lookup requires one exact function owner and source instruction") + } + key := emissionFunctionOwnerKey{function: function, owner: owner} + if _, frozen := ir.siteOwners[key]; !frozen { + return coroSemanticInstructionPlan{}, fmt.Errorf("semantic SitePlan owner is not frozen") + } + plan, ok := ir.semanticPlans[key][instruction] + if !ok { + return coroSemanticInstructionPlan{}, fmt.Errorf("source instruction %q has no frozen semantic recipe", instruction.String()) + } + return plan, nil +} + +func (ir *coroProgramIR) functionLocalBodyFacts(function *ssa.Function) (coro.SSAFunctionBodyFacts, error) { + if ir == nil || function == nil { + return coro.SSAFunctionBodyFacts{}, fmt.Errorf("local body facts require one exact function") + } + facts, ok := ir.localBodyFacts[function] + if !ok { + return coro.SSAFunctionBodyFacts{}, fmt.Errorf("function %q has no frozen local body facts", function.Name()) + } + return facts, nil +} + +func (ir *coroProgramIR) sitePlan(ctx *context, instruction ssa.Instruction) (coroEmissionSitePlan, error) { + if ir == nil || ctx == nil || ctx.emissionOwner == nil || instruction == nil || instruction.Parent() == nil { + return coroEmissionSitePlan{}, fmt.Errorf("coroutine site plan lookup requires an exact program IR, owner context, and source instruction") + } + owner := ctx.emissionOwner + if physical := ctx.coroEmissionPlan(); physical != nil { + if physical.function != instruction.Parent() || physical.owner == nil { + return coroEmissionSitePlan{}, fmt.Errorf("coroutine physical emission plan does not own the requested source instruction") + } + owner = physical.owner + } + key := emissionFunctionOwnerKey{function: instruction.Parent(), owner: owner} + if _, frozen := ir.siteOwners[key]; !frozen { + return coroEmissionSitePlan{}, fmt.Errorf("coroutine source instruction has no frozen site-plan owner") + } + return cloneCoroEmissionSitePlan(ir.sitePlans[key][instruction]), nil +} + +func (ir *coroProgramIR) plannedRuntimeHelpers(ctx *context, instruction ssa.Instruction) ([]string, error) { + plan, err := ir.sitePlan(ctx, instruction) + if err != nil { + return nil, err + } + helpers := make([]string, len(plan.managedRuntimeHelpers)) + for index, helper := range plan.managedRuntimeHelpers { + helpers[index] = helper.name + } + return helpers, nil +} + +func (ir *coroProgramIR) callSitePlan(call ssa.CallInstruction) (coroFrozenCallSitePlan, bool, error) { + if ir == nil || !ir.callsFrozen { + return coroFrozenCallSitePlan{}, false, fmt.Errorf("coroutine call SitePlan is not frozen") + } + if call == nil || call.Common() == nil || call.Parent() == nil { + return coroFrozenCallSitePlan{}, false, fmt.Errorf("coroutine call SitePlan lookup requires an exact SSA call") + } + plan, ok := ir.callPlans[call] + return plan, ok, nil +} + +func (plan coroEmissionSitePlan) managedRuntimeHelperNames() []string { + helpers := make([]string, len(plan.managedRuntimeHelpers)) + for index, helper := range plan.managedRuntimeHelpers { + helpers[index] = helper.name + } + return helpers +} + +func (plan coroEmissionSitePlan) managedRuntimeHelpersAt(placement coroRuntimeHelperPlacement) []string { + helpers := make([]string, 0, len(plan.managedRuntimeHelpers)) + for _, helper := range plan.managedRuntimeHelpers { + if helper.placement == placement { + helpers = append(helpers, helper.name) + } + } + return helpers +} + +func (plan coroEmissionSitePlan) hasManagedRuntimeHelper(name string) bool { + for _, helper := range plan.managedRuntimeHelpers { + if helper.name == name { + return true + } + } + return false +} + +func cloneCoroEmissionSitePlan(plan coroEmissionSitePlan) coroEmissionSitePlan { + plan.managedRuntimeHelpers = append([]coroPlannedRuntimeHelper(nil), plan.managedRuntimeHelpers...) + plan.plainRuntimeHelpers = append([]string(nil), plan.plainRuntimeHelpers...) + return plan +} + +func sameCoroEmissionSitePlan(first, second coroEmissionSitePlan) bool { + return slices.Equal(first.managedRuntimeHelpers, second.managedRuntimeHelpers) && + slices.Equal(first.plainRuntimeHelpers, second.plainRuntimeHelpers) && + first.hasCallPlan == second.hasCallPlan && sameCoroFrozenCallSitePlan(first.callPlan, second.callPlan) +} diff --git a/cl/coro_pure_ssa.go b/cl/coro_pure_ssa.go new file mode 100644 index 0000000000..d8f78323ef --- /dev/null +++ b/cl/coro_pure_ssa.go @@ -0,0 +1,3161 @@ +/* + * 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 ( + "fmt" + "go/constant" + "go/token" + "go/types" + "sort" + "strings" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// coroPhysicalPureSSAAudit is the deliberately small proof boundary for SSA +// operations that remain ordinary LLVM values across a coro suspend. It is not +// a general instruction allowlist. Every accepted case below mirrors the +// corresponding compileInstr/compileInstrOrValue and LLSSA Builder lowering. +// An operation that can perform dynamic dispatch or introduce a new panic edge +// is rejected here. A hidden runtime helper is accepted only when the immutable +// whole-program plan binds that exact logical helper to either one demanded +// non-unwind NoSuspend plain body, or an explicitly capability-gated +// structured-outcome coroutine. +// +// PhysicalABIV1's current frame allocator profiles are conservative or +// non-collecting. Pointer/interface/slice values may therefore live in the LLVM +// coroutine frame, but this slice does not claim a precise frame root map or a +// moving-GC write barrier. A future precise collector must add those two ABI +// capabilities before enabling the same local-frame operations for that +// profile. +type coroPhysicalPureSSAAudit struct { + universe *EmissionUniverse + plan *coro.SSAPlan + ctx *context + fn *ssa.Function + reachableBlocks map[*ssa.BasicBlock]bool + frameRetentionABI string + frameRetentionBuilt bool + frameRetentionProofCache *coroFrameRetentionProof + // allowImplicitNilFault is enabled only by PhysicalABIV1 preflight after + // the target-wide explicit-status panic identity has been selected. It + // never weakens transport/root validation; it lets implicit nil and bounds + // faults rely on compiler-owned terminal edges instead of stackful helpers. + allowImplicitNilFault bool + // Recover is an independent structured capability even though the first + // explicit-status identity enables both gates together. + allowExplicitRecover bool +} + +func newCoroPhysicalPureSSAAudit( + universe *EmissionUniverse, + plan *coro.SSAPlan, + fn *ssa.Function, + frameRetentionABI string, +) (*coroPhysicalPureSSAAudit, error) { + return newCoroPhysicalPureSSAAuditForOwner(universe, plan, fn, nil, frameRetentionABI) +} + +func newCoroPhysicalPureSSAAuditForOwner( + universe *EmissionUniverse, + plan *coro.SSAPlan, + fn *ssa.Function, + owner *preparedEmissionPackage, + frameRetentionABI string, +) (*coroPhysicalPureSSAAudit, error) { + audit := &coroPhysicalPureSSAAudit{ + universe: universe, + plan: plan, + fn: fn, + frameRetentionABI: frameRetentionABI, + reachableBlocks: coroPhysicalConstantReachableBlocks(fn), + } + if universe == nil { + // Structural unit tests may call the validator directly. Active + // Compilation paths always supply their prepared emission universe. + return audit, nil + } + if fn == nil { + return nil, fmt.Errorf("nil function") + } + if canonical := universe.canonicalAlias(fn); canonical == nil || canonical != fn { + return nil, fmt.Errorf("function %q is not the exact canonical emission owner", fn.Name()) + } + if _, frozen := universe.required[fn]; !frozen { + return nil, fmt.Errorf("function %q is outside the prepared emission universe", fn.Name()) + } + if owner == nil { + owner = universe.ownerOf(fn) + } + if owner == nil { + return nil, fmt.Errorf("function %q has no exact emission owner", fn.Name()) + } + owned := false + for _, candidate := range universe.sortedUseOwners(fn) { + owned = owned || candidate == owner + } + if !owned { + return nil, fmt.Errorf("function %q is not materialized for emission owner %q", fn.Name(), owner.identity) + } + ctx, err := universe.functionABIContext(fn, owner) + if err != nil { + return nil, err + } + audit.ctx = ctx + return audit, nil +} + +func (a *coroPhysicalPureSSAAudit) validate(instr ssa.Instruction) (handled bool, reason string) { + if instr != nil && a != nil && a.ctx != nil { + if _, unevaluated := a.ctx.unevaluatedSSA[instr]; unevaluated { + return true, "" + } + } + if instr != nil && a != nil && len(a.reachableBlocks) != 0 && !a.reachableBlocks[instr.Block()] { + return true, "" + } + switch instr := instr.(type) { + case *ssa.Alloc: + return true, a.validateAlloc(instr) + case *ssa.FieldAddr: + return true, a.validateFieldAddr(instr) + case *ssa.IndexAddr: + return true, a.validateIndexAddr(instr) + case *ssa.Index: + return true, a.validateIndex(instr) + case *ssa.Slice: + return true, a.validateSlice(instr) + case *ssa.SliceToArrayPointer: + return true, a.validateSliceToArrayPointer(instr) + case *ssa.Extract: + return true, a.validateExtract(instr) + case *ssa.Field: + return true, a.validateField(instr) + case *ssa.MakeInterface: + return true, a.validateMakeInterface(instr) + case *ssa.ChangeInterface: + return true, a.validateChangeInterface(instr) + case *ssa.TypeAssert: + return true, a.validateTypeAssert(instr) + case *ssa.MakeSlice: + return true, a.validateMakeSlice(instr) + case *ssa.MakeMap: + return true, a.validateMakeMap(instr) + case *ssa.MakeChan: + return true, a.validateMakeChan(instr) + case *ssa.Lookup: + return true, a.validateLookup(instr) + case *ssa.MapUpdate: + return true, a.validateMapUpdate(instr) + case *ssa.Range: + return true, a.validateRange(instr) + case *ssa.Next: + return true, a.validateNext(instr) + case *ssa.MakeClosure: + return true, a.validateMakeClosure(instr) + case *ssa.ChangeType: + return true, a.validateChangeType(instr) + case *ssa.Convert: + return true, a.validateConvert(instr) + case *ssa.Phi: + return true, a.validatePhi(instr) + case *ssa.BinOp: + return true, a.validateBinOp(instr) + case *ssa.UnOp: + if instr.Op == token.MUL || instr.Op == token.SUB || instr.Op == token.XOR || instr.Op == token.NOT { + return true, a.validateUnOp(instr) + } + case *ssa.Store: + return true, a.validateStore(instr) + case *ssa.Call: + if _, builtin := instr.Call.Value.(*ssa.Builtin); builtin { + return true, a.validateBuiltin(instr) + } + } + return false, "" +} + +func (a *coroPhysicalPureSSAAudit) validateMakeClosure(closure *ssa.MakeClosure) string { + if closure == nil { + return "incomplete closure construction" + } + target, ok := closure.Fn.(*ssa.Function) + if !ok || target == nil || a.plan == nil { + return "closure has no exact function target" + } + if len(closure.Bindings) != len(target.FreeVars) { + return fmt.Sprintf("closure bindings=%d do not match target free variables=%d", len(closure.Bindings), len(target.FreeVars)) + } + for index, binding := range closure.Bindings { + if binding == nil || target.FreeVars[index] == nil || !types.Identical(binding.Type(), target.FreeVars[index].Type()) { + return fmt.Sprintf("closure binding %d does not match its target free variable", index) + } + } + if a.universe != nil { + resolved, frozen := a.universe.Resolve(target) + if !frozen || resolved == nil { + return "closure target is outside the frozen emission universe" + } + target = resolved + } + targetID, ok := a.plan.FunctionID(target) + if !ok { + return "closure target has no FunctionID" + } + value, ok := a.plan.ValuePlan(closure) + if !ok || len(value.Funcs) != 1 || len(value.Funcs[0].Path) != 0 { + return "closure has no exact scalar callable representation" + } + leaf := value.Funcs[0] + // MakeClosure itself is an exact non-nil producer even when its value later + // joins a nil or another callable through Phi/storage flow. ValuePlan carries + // the representation required by that complete flow, so its target and nil + // sets may be conservative here. The source SSA operand still fixes this + // producer's target; require that the plan contains it, then use only the + // frozen representation below. + targetPresent := false + for _, candidate := range leaf.Targets { + if candidate == targetID { + targetPresent = true + break + } + } + if !targetPresent { + return "closure exact target is absent from its scalar callable representation" + } + if leaf.Transport != coro.ManagedTransport { + return fmt.Sprintf("closure has non-managed callable transport %s", leaf.Transport) + } + if len(target.FreeVars) == 0 && (leaf.Rep == coro.DirectPlain || leaf.Rep == coro.DirectCoro) { + return a.requireNoRuntimeHelpers(closure) + } + if len(target.FreeVars) != 0 && leaf.Rep == coro.DirectCoro { + targetPlan, planned := a.plan.FunctionPlan(target) + if !planned || targetPlan.ID != targetID || targetPlan.External != coro.Defined || + targetPlan.Emission != coro.EmitCoroutine || targetPlan.Primary != coro.PrimaryCoroutine || + (targetPlan.FuncRep != coro.DirectCoro && targetPlan.FuncRep != coro.Dispatch) { + return "captured direct coroutine target has no canonical physical context plan" + } + return a.requireFrozenCoroSafeRuntimeHelpers(closure, "AllocU") + } + if leaf.Rep != coro.Dispatch { + return "captured or descriptor-backed closure has no exact Dispatch representation" + } + targetPlan, planned := a.plan.FunctionPlan(target) + if !planned || targetPlan.ID != targetID { + return "descriptor-backed closure target has no canonical function plan" + } + if err := validateCoroDynamicDispatchTarget(target, targetPlan, a.universe); err != nil { + return "descriptor-backed closure target: " + err.Error() + } + if len(target.FreeVars) != 0 { + return a.requireFrozenCoroSafeRuntimeHelpers(closure, "AllocU") + } + return a.requireNoRuntimeHelpers(closure) +} + +func coroPhysicalConstantReachableBlocks(fn *ssa.Function) map[*ssa.BasicBlock]bool { + reachable := make(map[*ssa.BasicBlock]bool) + if fn == nil || len(fn.Blocks) == 0 || fn.Blocks[0] == nil { + return reachable + } + queue := []*ssa.BasicBlock{fn.Blocks[0]} + for len(queue) != 0 { + block := queue[0] + queue = queue[1:] + if block == nil || reachable[block] { + continue + } + reachable[block] = true + successors := block.Succs + if len(block.Instrs) != 0 && len(successors) == 2 { + if branch, ok := block.Instrs[len(block.Instrs)-1].(*ssa.If); ok { + if condition, ok := branch.Cond.(*ssa.Const); ok && condition.Value != nil && condition.Value.Kind() == constant.Bool { + if constant.BoolVal(condition.Value) { + successors = successors[:1] + } else { + successors = successors[1:] + } + } + } + } + for _, successor := range successors { + if successor != nil && !reachable[successor] { + queue = append(queue, successor) + } + } + } + return reachable +} + +func (a *coroPhysicalPureSSAAudit) validateAlloc(alloc *ssa.Alloc) string { + if alloc == nil { + return "heap allocation requires managed allocation and coroutine GC-root lowering" + } + if a.ctx != nil && isEmissionVargsAlloc(a.ctx, alloc) { + // The ordinary compiler materializes this synthetic array only in its + // vargs side table. Individual stores evaluate their unboxed operands and + // the variadic call consumes those values directly; no address or backing + // allocation crosses a suspension boundary. + return "" + } + if alloc.Heap { + if a.frameRetainsAllocation(alloc) { + // The complete address-use proof changes this exact lowering from + // runtime.AllocZ to an LLVM alloca in the current coroutine frame. Do + // not consult the ordinary Heap helper-demand table for that allocation. + return "" + } + if a.frameRetainsManagedHeapAllocation(alloc) { + // This remains an ordinary Go heap allocation. The capability proves + // both its exact AllocZ lowering and that a live pointer spilled by + // CoroSplit is scanned from the current non-moving/no-GC frame profile. + return "" + } + _, reason := a.managedHeapAllocationCapability(alloc) + if reason == "" { + reason = "allocation is absent from the immutable managed-heap root proof" + } + return "heap allocation requires managed allocation and coroutine GC-root lowering: " + reason + } + if a.ctx != nil && a.ctx.skipSyntheticMakeSliceAlloc(alloc) { + return "synthetic slice/varargs allocation belongs to a non-pure enclosing lowering" + } + pointer, ok := types.Unalias(a.typeOf(alloc.Type())).Underlying().(*types.Pointer) + if !ok { + return "local allocation does not have a pointer type" + } + if err := validateCoroPhysicalSSAValueType(pointer.Elem()); err != nil { + return "local allocation has unsupported value type: " + err.Error() + } + return a.requireNoRuntimeHelpers(alloc) +} + +// managedHeapAllocationCapability proves one exact x/tools Heap Alloc without +// changing its lowering or escape identity. Non-zero objects must lower only +// through the owner-scoped frozen AllocZ edge. Zero-sized objects use LLGo's +// module sentinel and therefore must have no hidden allocator helper at all. +// The proof is intentionally unavailable under the legacy shadow-stack mode: +// a precise or moving collector needs typed coroutine-frame maps and barriers, +// neither of which this capability claims. +func (a *coroPhysicalPureSSAAudit) managedHeapAllocationCapability(alloc *ssa.Alloc) (coroFrameRetentionManagedHeapAllocation, string) { + fact := coroFrameRetentionManagedHeapAllocation{} + if a == nil || a.ctx == nil || a.universe == nil || a.plan == nil || a.fn == nil { + return fact, "requires an owned body, complete emission universe, and whole-build plan" + } + if emitShadowStackInstrumentation { + return fact, "requires the non-moving conservative-or-no-GC coroutine frame root profile" + } + if !a.universe.CompleteRuntimeABI() { + return fact, "requires a complete frozen runtime ABI" + } + if alloc == nil || alloc.Parent() != a.fn || !alloc.Heap { + return fact, "is not one exact owned escaping SSA allocation" + } + if a.ctx.skipSyntheticMakeSliceAlloc(alloc) || isEmissionVargsAlloc(a.ctx, alloc) { + return fact, "synthetic slice/varargs storage has no standalone managed-allocation capability" + } + pointer, ok := types.Unalias(a.typeOf(alloc.Type())).Underlying().(*types.Pointer) + if !ok { + return fact, "allocation result does not have pointer type" + } + if err := validateCoroPhysicalSSAValueType(pointer.Elem()); err != nil { + return fact, "allocation element has unsupported physical type: " + err.Error() + } + physical := a.ctx.type_(pointer.Elem(), llssa.InGo) + helpers, helperReason := a.plannedRuntimeHelpers(alloc) + if helperReason != "" { + return fact, helperReason + } + if a.ctx.prog.SizeOf(physical) == 0 { + if len(helpers) != 0 { + return fact, "zero-sized module-sentinel allocation unexpectedly lowers through " + strings.Join(helpers, ", ") + } + fact.zeroSized = true + return fact, "" + } + if len(helpers) != 1 || helpers[0] != "AllocZ" { + return fact, "non-zero allocation does not lower through exactly one AllocZ helper" + } + if reason := a.requireFrozenCoroSafeRuntimeHelpers(alloc, "AllocZ"); reason != "" { + return fact, reason + } + target, planned := a.plan.ResolveLoweredCall(a.fn, "AllocZ") + if !planned || target == nil { + return fact, "AllocZ lacks one exact owner-scoped lowered-call target" + } + targetPlan, planned := a.plan.FunctionPlan(target) + if !planned || targetPlan.ID == "" { + return fact, "AllocZ target lacks one canonical function plan" + } + fact.helper = "AllocZ" + fact.helperTarget = targetPlan.ID + fact.helperEmission = targetPlan.Emission + return fact, "" +} + +func (a *coroPhysicalPureSSAAudit) validateFieldAddr(field *ssa.FieldAddr) string { + if field == nil { + return "nil field address" + } + if _, reason := a.stableAddressAt(field, field, make(map[ssa.Value]bool)); reason != "" { + return reason + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(field.Type())); err != nil { + return "field address has unsupported type: " + err.Error() + } + return a.requireNoRuntimeHelpersExcept(field, "AssertNilDeref") +} + +func (a *coroPhysicalPureSSAAudit) fieldAddrRequiresImplicitNilFault(field *ssa.FieldAddr) (bool, string) { + if a == nil || field == nil { + return false, "" + } + helpers, reason := a.plannedRuntimeHelpers(field) + if reason != "" { + return false, reason + } + for _, helper := range helpers { + if helper == "AssertNilDeref" { + // The frozen logical SitePlan is the lowering input. A stronger frame + // proof may later justify a dedicated proved-non-null recipe, but it + // cannot silently turn an expected helper into ordinary codegen. + return true, "" + } + } + // A value field load is represented as FieldAddr followed by UnOp. Its + // logical helper belongs to the load, while the physical FieldAddr recipe + // must still own the earlier base-pointer guard so no GEP is formed from a + // nil address. Preserve that frame-proof projection in addition to the + // direct helper-owned address-of case above. + if ssaAddressValueProvenNonNilAt(field.X, field) { + return false, "" + } + proof := a.currentFrameRetentionProof() + return proof != nil && proof.requiresImplicitNilFault(field, field), "" +} + +func (a *coroPhysicalPureSSAAudit) derefRequiresImplicitNilFault(deref *ssa.UnOp) bool { + if a == nil || deref == nil || deref.Op != token.MUL { + return false + } + if ssaValueProvenNonNilAt(deref.X, deref) { + return false + } + if _, _, synthetic := coroSliceToArrayValueDeref(deref, a.typeOf); synthetic { + // The conversion owns the N>0 length fault. N==0 array-value + // conversion is the zero value and must remain legal for a nil slice. + return false + } + proof := a.currentFrameRetentionProof() + if proof == nil { + return false + } + if field, ok := deref.X.(*ssa.FieldAddr); ok && proof.requiresImplicitNilFault(field, field) { + // The FieldAddr recipe owns this base guard before constructing the GEP. + return false + } + if _, indexed := deref.X.(*ssa.IndexAddr); indexed { + // The IndexAddr recipe owns its bounds and possible *array nil guards. + return false + } + return proof.requiresImplicitNilFault(deref.X, deref) +} + +func (a *coroPhysicalPureSSAAudit) validateIndexAddr(index *ssa.IndexAddr) string { + if index == nil { + return "nil index address" + } + if a.ctx != nil && emissionIsVargsAlloc(a.ctx, index.X) { + return "" + } + if _, reason := a.stableAddressAt(index, index, make(map[ssa.Value]bool)); reason != "" { + detail := "" + if add, ok := index.Index.(*ssa.BinOp); ok { + detail = fmt.Sprintf(", operands=(%T %s, %T %s)", add.X, add.X, add.Y, add.Y) + if phi, ok := add.X.(*ssa.Phi); ok { + detail += fmt.Sprintf(", phi-edges=%v", phi.Edges) + } + } + return fmt.Sprintf("%s (base=%T %s, index=%T %s%s)", reason, index.X, index.X, index.Index, index.Index, detail) + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(index.Type())); err != nil { + return "index address has unsupported type: " + err.Error() + } + if a.allowImplicitNilFault { + proof := a.currentFrameRetentionProof() + if proof != nil && proof.provesGuardableStableAddress(index, index) { + // ExplicitStatus codegen replaces CheckIndexRange (and a possible + // *array nil helper) with compiler-owned terminal branches before the + // unchecked address is formed. + return a.requireOnlyCompilerElidedRuntimeHelpers(index, "CheckIndexRange", "AssertNilDeref") + } + } + return a.requireNoRuntimeHelpersExcept(index, "CheckIndexRange", "AssertNilDeref") +} + +func (a *coroPhysicalPureSSAAudit) validateIndex(index *ssa.Index) string { + if index == nil || index.X == nil || index.Index == nil { + return "incomplete index operation" + } + if a.allowImplicitNilFault { + switch container := types.Unalias(a.typeOf(index.X.Type())).Underlying().(type) { + case *types.Basic: + if !coroPhysicalStringBasic(container) { + return "index has unsupported basic container type" + } + case *types.Array, *types.Slice: + case *types.Pointer: + if _, ok := types.Unalias(container.Elem()).Underlying().(*types.Array); !ok { + return "index pointer base is not a fixed array" + } + default: + return fmt.Sprintf("index has unsupported container type %T", container) + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(index.Type())); err != nil { + return "index has unsupported result type: " + err.Error() + } + // ExplicitStatus codegen consumes these logical helper edges by emitting + // a terminal bounds branch (and, for *array, a terminal nil branch) + // before an unchecked load. + return a.requireOnlyCompilerElidedRuntimeHelpers(index, "CheckIndexRange", "AssertNilDeref") + } + array, ok := types.Unalias(a.typeOf(index.X.Type())).Underlying().(*types.Array) + if !ok || !coroConstantIndexInBounds(index.Index, array.Len()) { + return "index may panic; pure coroutine indexing requires a compile-time in-range fixed-array index" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(index.Type())); err != nil { + return "array index has unsupported result type: " + err.Error() + } + return a.requireNoRuntimeHelpers(index) +} + +func coroPhysicalStringBasic(basic *types.Basic) bool { + return basic != nil && (basic.Kind() == types.String || basic.Kind() == types.UntypedString) +} + +func (a *coroPhysicalPureSSAAudit) validateSlice(slice *ssa.Slice) string { + if slice == nil || slice.X == nil || slice.Type() == nil { + return "incomplete slice operation" + } + if a.ctx != nil && emissionIsVargsAlloc(a.ctx, slice.X) { + return "" + } + if a.ctx != nil { + if _, synthetic := a.ctx.syntheticMakeSliceCap(slice); synthetic { + return "synthetic make-slice lowering is outside structured slice bounds" + } + } + + baseType := a.typeOf(slice.X.Type()) + resultType := a.typeOf(slice.Type()) + var helper string + switch base := types.Unalias(baseType).Underlying().(type) { + case *types.Basic: + if !coroPhysicalStringBasic(base) || slice.Max != nil { + return "slice basic base must be a two-index string" + } + result, ok := types.Unalias(resultType).Underlying().(*types.Basic) + if !ok || result.Kind() != types.String || + (base.Kind() == types.String && !types.Identical(baseType, resultType)) || + (base.Kind() == types.UntypedString && !types.Identical(resultType, types.Typ[types.String])) { + return "string slice result does not preserve its source type" + } + helper = "StringSlice2" + case *types.Slice: + if !types.Identical(baseType, resultType) { + return "slice expression result does not preserve its source slice type" + } + if slice.Max == nil { + helper = "NewSlice2" + } else { + helper = "NewSlice3Bounds" + } + case *types.Pointer: + array, ok := types.Unalias(base.Elem()).Underlying().(*types.Array) + if !ok { + return "slice pointer base is not a fixed array" + } + result, ok := types.Unalias(resultType).Underlying().(*types.Slice) + if !ok || !types.Identical(a.typeOf(array.Elem()), a.typeOf(result.Elem())) { + return "pointer-to-array slice result has a different element type" + } + if _, reason := a.stableAddressAt(slice.X, slice, make(map[ssa.Value]bool)); reason != "" { + return "slice base: " + reason + } + if slice.Low == nil && slice.High == nil && slice.Max == nil { + helper = "" + } else if slice.Max == nil { + helper = "NewSlice2" + } else { + helper = "NewSlice3Bounds" + } + default: + return fmt.Sprintf("slice has unsupported base type %T", base) + } + if slice.Max != nil && slice.High == nil { + return "three-index slice requires explicit high and max bounds" + } + for name, bound := range map[string]ssa.Value{ + "low": slice.Low, "high": slice.High, "max": slice.Max, + } { + if bound == nil { + continue + } + basic, ok := types.Unalias(a.typeOf(bound.Type())).Underlying().(*types.Basic) + if !ok || basic.Info()&types.IsInteger == 0 { + return "slice " + name + " bound is not an integer" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(bound.Type())); err != nil { + return "slice " + name + " bound has unsupported type: " + err.Error() + } + } + physicalBaseType := baseType + if base, ok := types.Unalias(baseType).Underlying().(*types.Basic); ok && base.Kind() == types.UntypedString { + // SSA retains the untyped kind on a string constant even when a dynamic + // slice defaults that operand to the concrete string representation. + // compileValue applies the same types.Default conversion before emission. + physicalBaseType = types.Typ[types.String] + } + for _, typ := range []types.Type{physicalBaseType, resultType} { + if err := validateCoroPhysicalSSAValueType(typ); err != nil { + return "slice has unsupported physical value type: " + err.Error() + } + } + if !a.allowImplicitNilFault { + if slice.Low != nil || slice.High != nil || slice.Max != nil { + return "slice bounds require the explicit-status panic ABI" + } + if _, pointer := types.Unalias(baseType).Underlying().(*types.Pointer); !pointer { + return "dynamic slice bounds require the explicit-status panic ABI" + } + return a.requireNoRuntimeHelpers(slice) + } + if helper == "" { + return a.requireOnlyCompilerElidedRuntimeHelpers(slice) + } + if err := validateCoroPhysicalSSAValueType(resultType); err != nil { + return "slice view has unsupported type: " + err.Error() + } + // ExplicitStatus codegen owns the bounds predicate and constructs the + // aggregate only in the normal continuation; the logical helper remains in + // the frozen inventory solely for effect/outcome propagation. + return a.requireOnlyCompilerElidedRuntimeHelpers(slice, helper) +} + +func (a *coroPhysicalPureSSAAudit) validateSliceToArrayPointer(conversion *ssa.SliceToArrayPointer) string { + if conversion == nil || conversion.X == nil || conversion.Type() == nil { + return "incomplete slice-to-array-pointer conversion" + } + source, result := a.typeOf(conversion.X.Type()), a.typeOf(conversion.Type()) + array, reason := coroSliceToArrayPointerShape(source, result) + if reason != "" { + return "invalid slice-to-array-pointer conversion: " + reason + } + for _, typ := range []types.Type{source, result} { + if err := validateCoroPhysicalSSAValueType(typ); err != nil { + return "slice-to-array-pointer conversion has unsupported physical type: " + err.Error() + } + } + if array.Len() == 0 { + // This is a pure data-word projection. It must preserve nil rather than + // manufacture a non-nil sentinel, and has no PanicSliceConvert edge. + return a.requireOnlyCompilerElidedRuntimeHelpers(conversion) + } + if !a.allowImplicitNilFault { + return "slice-to-array-pointer length fault requires the explicit-status panic ABI" + } + return a.requireOnlyCompilerElidedRuntimeHelpers(conversion, "PanicSliceConvert") +} + +func (a *coroPhysicalPureSSAAudit) validateExtract(extract *ssa.Extract) string { + if extract == nil || extract.Tuple == nil { + return "incomplete tuple extract" + } + tuple, ok := types.Unalias(a.typeOf(extract.Tuple.Type())).Underlying().(*types.Tuple) + if !ok || extract.Index < 0 || extract.Index >= tuple.Len() { + return "tuple extract index is outside its frozen aggregate shape" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(extract.Type())); err != nil { + return "tuple extract has unsupported result type: " + err.Error() + } + return a.requireNoRuntimeHelpers(extract) +} + +func (a *coroPhysicalPureSSAAudit) validateField(field *ssa.Field) string { + if field == nil || field.X == nil { + return "incomplete aggregate field extraction" + } + structure, ok := types.Unalias(a.typeOf(field.X.Type())).Underlying().(*types.Struct) + if !ok || field.Field < 0 || field.Field >= structure.NumFields() { + return "aggregate field index is outside its frozen struct shape" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(field.Type())); err != nil { + return "aggregate field has unsupported result type: " + err.Error() + } + return a.requireNoRuntimeHelpers(field) +} + +func (a *coroPhysicalPureSSAAudit) validateMakeInterface(box *ssa.MakeInterface) string { + if box == nil || box.X == nil { + return "incomplete interface construction" + } + target, ok := types.Unalias(a.typeOf(box.Type())).Underlying().(*types.Interface) + if !ok { + return "MakeInterface target is not an interface" + } + target.Complete() + source := a.typeOf(box.X.Type()) + emitsABIType := true + if a.universe != nil { + emitsABIType = a.universe.makeInterfaceEmitsABIType(box, a.ctx) + } + if coroPhysicalTypeContainsFunctionValue(source, make(map[types.Type]bool)) { + if !emitsABIType { + return a.validateCompilerElidedFunctionInterface(box) + } + if err := validateCoroCallableTransportValue(a.plan, a.fn, box.X, a.universe); err != nil { + return "function-valued interface payload: " + err.Error() + } + } + if err := validateCoroPhysicalSSAValueType(source); err != nil { + return "interface payload has unsupported type: " + err.Error() + } + if !emitsABIType { + // Varargs and compiler ABI inspection sites consume the concrete operand + // directly and emit no interface helper or physical interface value. + return "" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(box.Type())); err != nil { + return "interface result has unsupported type: " + err.Error() + } + if a == nil || a.ctx == nil { + return "interface construction requires a frozen emission context" + } + + // Mirror the complete LLSSA MakeInterface recipe independently of the + // frozen helper inventory. Integer and aggregate payloads need stable + // backing storage, non-empty interfaces additionally need an itab, and the + // large/zero-sized dereference recipe owns its explicit nil check and typed + // copy. Every emitted call is then checked against the owner-scoped plan by + // the same structured helper gate used for maps and future composite + // lowerings. This admits ordinary `return errno` error paths without + // granting a symbol-name exception to syscall or to error itself. + physical := a.ctx.type_(box.X.Type(), llssa.InGo) + needsAlloc := !emissionDirectIfaceType(physical.RawType()) + needsNilCheck := false + needsTypedMove := false + if unop, ok := box.X.(*ssa.UnOp); ok && unop.Op == token.MUL && + (a.ctx.isLargeNonPointerValue(physical) || a.ctx.isZeroSizedValue(physical)) { + needsAlloc = true + needsNilCheck = !isKnownNonNilAddr(unop.X) && !ssaValueProvenNonNilAt(unop.X, box) + needsTypedMove = true + } + + // loweredRuntimeHelpers is sorted, so keep the independently-derived exact + // inventory in lexical order as well. The structured gate compares sets and + // cardinality and therefore also rejects duplicate or newly-added helpers. + expected := make([]string, 0, 4) + if needsAlloc { + expected = append(expected, "AllocU") + } + if needsNilCheck { + expected = append(expected, "AssertNilDeref") + } + if !target.Empty() { + expected = append(expected, "NewItab") + } + if needsTypedMove { + expected = append(expected, "Typedmemmove") + } + if len(expected) == 0 { + return a.requireNoRuntimeHelpers(box) + } + return a.requireFrozenStructuredRuntimeHelpers(box, expected...) +} + +func (a *coroPhysicalPureSSAAudit) validateChangeInterface(change *ssa.ChangeInterface) string { + if change == nil || change.X == nil { + return "incomplete interface conversion" + } + sourceType := a.typeOf(change.X.Type()) + targetType := a.typeOf(change.Type()) + source, ok := types.Unalias(sourceType).Underlying().(*types.Interface) + if !ok { + return "ChangeInterface source is not an interface" + } + target, ok := types.Unalias(targetType).Underlying().(*types.Interface) + if !ok { + return "ChangeInterface target is not an interface" + } + source.Complete() + target.Complete() + if err := validateCoroPhysicalSSAValueType(sourceType); err != nil { + return "interface conversion has unsupported source type: " + err.Error() + } + if err := validateCoroPhysicalSSAValueType(targetType); err != nil { + return "interface conversion has unsupported target type: " + err.Error() + } + + // LLSSA extracts the dynamic ABI type through IfaceType when the source is + // non-empty, then constructs a fresh itab when the destination is non-empty. + // Empty-interface sides need only aggregate extract/insert operations. Bind + // exactly that recipe to the frozen owner-scoped helper plan. + expected := make([]string, 0, 2) + if !source.Empty() { + expected = append(expected, "IfaceType") + } + if !target.Empty() { + expected = append(expected, "NewItab") + } + if len(expected) == 0 { + return a.requireNoRuntimeHelpers(change) + } + return a.requireFrozenStructuredRuntimeHelpers(change, expected...) +} + +func (a *coroPhysicalPureSSAAudit) validateTypeAssert(assertion *ssa.TypeAssert) string { + if assertion == nil || assertion.X == nil || assertion.AssertedType == nil { + return "incomplete type assertion" + } + sourceType := a.typeOf(assertion.X.Type()) + assertedType := a.typeOf(assertion.AssertedType) + resultType := a.typeOf(assertion.Type()) + source, ok := types.Unalias(sourceType).Underlying().(*types.Interface) + if !ok { + return "type assertion source is not an interface" + } + source.Complete() + if err := validateCoroPhysicalSSAValueType(sourceType); err != nil { + return "type assertion has unsupported source type: " + err.Error() + } + if err := validateCoroPhysicalSSAValueType(assertedType); err != nil { + return "type assertion has unsupported asserted type: " + err.Error() + } + if err := validateCoroPhysicalSSAValueType(resultType); err != nil { + return "type assertion has unsupported result type: " + err.Error() + } + if assertion.CommaOk { + tuple, ok := types.Unalias(resultType).Underlying().(*types.Tuple) + if !ok || tuple.Len() != 2 || !types.Identical(a.typeOf(tuple.At(0).Type()), assertedType) || + !types.Identical(a.typeOf(tuple.At(1).Type()), types.Typ[types.Bool]) { + return "comma-ok type assertion has an incompatible result tuple" + } + } else if !types.Identical(resultType, assertedType) { + return "single-value type assertion result does not match its asserted type" + } + if coroPhysicalTypeContainsFunctionValue(assertedType, make(map[types.Type]bool)) { + // Builder.TypeAssert copies the concrete callable's canonical physical + // bytes. The frozen result ValuePlan proves whether each leaf is a managed + // {descriptor,env} closure or an exact raw C code pointer; neither + // transport may be reinterpreted as the other at this boundary. + if err := validateCoroCallableTransportValue(a.plan, a.fn, assertion, a.universe); err != nil { + return "function-valued type assertion result: " + err.Error() + } + } + + // Mirror Builder.TypeAssert independently. A non-empty source needs its + // dynamic ABI type; assertions to another interface use Implements and, for + // a non-empty result, NewItab; managed function assertions use MatchesClosure + // after the result's descriptor ValuePlan is certified. Raw C function + // assertions copy their direct pointer payload without that helper. A single-value + // assertion additionally has the exact PanicTypeAssert terminal edge. Every + // helper is then bound to the frozen owner-scoped plan so a newly suspending + // or unwinding helper cannot hide beneath a live LLVM coroutine frame. + expected := make([]string, 0, 4) + if !types.Identical(sourceType, assertedType) { + switch asserted := types.Unalias(assertedType).Underlying().(type) { + case *types.Interface: + asserted.Complete() + expected = append(expected, "Implements") + if !asserted.Empty() { + expected = append(expected, "NewItab") + } + case *types.Signature: + if !coroTypeAssertUsesManagedClosure(a.ctx, assertion) { + break + } + expected = append(expected, "MatchesClosure") + } + } + if !source.Empty() { + expected = append(expected, "IfaceType") + } + if !assertion.CommaOk { + expected = append(expected, "PanicTypeAssert") + } + return a.requireFrozenTypeAssertRuntimeHelpers(assertion, expected...) +} + +// coroTypeAssertUsesManagedClosure mirrors Builder.TypeAssert's physical +// branch, rather than inferring the representation from the logical Go +// signature. Exact //llgo:type C functions remain one raw code pointer and +// must never enter MatchesClosure, whose payload contract is the managed +// two-pointer closure aggregate. +func coroTypeAssertUsesManagedClosure(ctx *context, assertion *ssa.TypeAssert) bool { + if ctx == nil || assertion == nil || assertion.AssertedType == nil { + return false + } + physical := ctx.type_(assertion.AssertedType, llssa.InGo) + closure, ok := types.Unalias(physical.RawType()).Underlying().(*types.Struct) + return ok && llssa.IsClosure(closure) +} + +// validateCompilerElidedFunctionInterface accepts no ordinary function box. +// It certifies the transient MakeInterface node that x/tools SSA inserts for +// the exact func(any) operand of llgo.funcAddr/llgo.funcPCABI0. Those +// intrinsics inspect the static SSA function and emit its address/PC directly; +// compileValue never materializes the interface representation. +func (a *coroPhysicalPureSSAAudit) validateCompilerElidedFunctionInterface(box *ssa.MakeInterface) string { + if a == nil || a.universe == nil || a.ctx == nil || box == nil || + !a.universe.makeInterfaceConsumedByFuncAddress(box, a.ctx) { + return "function interface is not an exact compiler-elided address operand" + } + refs := box.Referrers() + if refs == nil || len(*refs) != 1 { + return "compiler-elided function interface does not have one exact consumer" + } + call, ok := (*refs)[0].(*ssa.Call) + if !ok || call.Parent() != a.fn || call.Common() == nil || len(call.Common().Args) != 1 || call.Common().Args[0] != box { + return "compiler-elided function interface is not the sole argument of its owning direct call" + } + semantics, intrinsic, err := coroIntrinsicCallSiteSemantics(a.universe, call) + if err != nil { + return "compiler-elided function address intrinsic: " + err.Error() + } + if !intrinsic || semantics != CoroIntrinsicCallInlineNoSuspend { + return "compiler-elided function interface consumer is not one exact inline no-suspend address intrinsic" + } + return a.requireNoRuntimeHelpers(box) +} + +func (a *coroPhysicalPureSSAAudit) validateMakeSlice(makeSlice *ssa.MakeSlice) string { + if makeSlice == nil || makeSlice.Len == nil || makeSlice.Cap == nil { + return "incomplete slice allocation" + } + if _, ok := types.Unalias(a.typeOf(makeSlice.Type())).Underlying().(*types.Slice); !ok { + return "MakeSlice result is not a slice" + } + for _, size := range []ssa.Value{makeSlice.Len, makeSlice.Cap} { + basic, ok := types.Unalias(a.typeOf(size.Type())).Underlying().(*types.Basic) + if !ok || basic.Info()&types.IsInteger == 0 { + return "MakeSlice length and capacity must be integer values" + } + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(makeSlice.Type())); err != nil { + return "MakeSlice result has unsupported type: " + err.Error() + } + return a.requireFrozenOutcomeRuntimeHelper(makeSlice, "MakeSlice") +} + +func (a *coroPhysicalPureSSAAudit) validateMakeMap(makeMap *ssa.MakeMap) string { + if makeMap == nil || makeMap.Type() == nil { + return "incomplete map allocation" + } + if _, ok := types.Unalias(a.typeOf(makeMap.Type())).Underlying().(*types.Map); !ok { + return "MakeMap result is not a map" + } + if makeMap.Reserve != nil { + reserve, ok := types.Unalias(a.typeOf(makeMap.Reserve.Type())).Underlying().(*types.Basic) + if !ok || reserve.Info()&types.IsInteger == 0 { + return "MakeMap reserve is not an integer" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(makeMap.Reserve.Type())); err != nil { + return "MakeMap reserve has unsupported type: " + err.Error() + } + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(makeMap.Type())); err != nil { + return "MakeMap result has unsupported type: " + err.Error() + } + return a.requireFrozenStructuredRuntimeHelpers(makeMap, "MakeMap") +} + +func (a *coroPhysicalPureSSAAudit) validateMakeChan(makeChan *ssa.MakeChan) string { + if makeChan == nil || makeChan.Size == nil || makeChan.Type() == nil { + return "incomplete channel allocation" + } + if _, ok := types.Unalias(a.typeOf(makeChan.Type())).Underlying().(*types.Chan); !ok { + return "MakeChan result is not a channel" + } + size, ok := types.Unalias(a.typeOf(makeChan.Size.Type())).Underlying().(*types.Basic) + if !ok || size.Info()&types.IsInteger == 0 || size.Info()&types.IsUntyped != 0 { + return "MakeChan capacity is not a concrete integer" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(makeChan.Size.Type())); err != nil { + return "MakeChan capacity has unsupported type: " + err.Error() + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(makeChan.Type())); err != nil { + return "MakeChan result has unsupported type: " + err.Error() + } + // NewChan rejects negative or overflowing capacities. Its exact managed + // helper therefore returns through the same ExplicitStatus child-await path + // as make([]T, n), never by unwinding across the live LLVM coroutine frame. + return a.requireFrozenOutcomeRuntimeHelper(makeChan, "NewChan") +} + +func (a *coroPhysicalPureSSAAudit) validateLookup(lookup *ssa.Lookup) string { + if lookup == nil || lookup.X == nil || lookup.Index == nil || lookup.Type() == nil { + return "incomplete map lookup" + } + mapType, ok := types.Unalias(a.typeOf(lookup.X.Type())).Underlying().(*types.Map) + if !ok { + return "Lookup source is not a map" + } + if !types.Identical(a.typeOf(lookup.Index.Type()), a.typeOf(mapType.Key())) { + return "Lookup key does not match the map key type" + } + if lookup.CommaOk { + result, ok := types.Unalias(a.typeOf(lookup.Type())).Underlying().(*types.Tuple) + if !ok || result.Len() != 2 || + !types.Identical(a.typeOf(result.At(0).Type()), a.typeOf(mapType.Elem())) || + !coroPhysicalBoolType(a.typeOf(result.At(1).Type())) { + return "comma-ok Lookup result is not the exact (element, bool) tuple" + } + } else if !types.Identical(a.typeOf(lookup.Type()), a.typeOf(mapType.Elem())) { + return "Lookup result does not match the map element type" + } + for name, typ := range map[string]types.Type{ + "map": lookup.X.Type(), "key": lookup.Index.Type(), "result": lookup.Type(), + } { + if err := validateCoroPhysicalSSAValueType(a.typeOf(typ)); err != nil { + return "Lookup " + name + " has unsupported type: " + err.Error() + } + } + helper := "MapAccess1" + if lookup.CommaOk { + helper = "MapAccess2" + } + return a.requireFrozenStructuredRuntimeHelpers(lookup, "AllocU", helper) +} + +func (a *coroPhysicalPureSSAAudit) validateMapUpdate(update *ssa.MapUpdate) string { + if update == nil || update.Map == nil || update.Key == nil || update.Value == nil { + return "incomplete map update" + } + mapType, ok := types.Unalias(a.typeOf(update.Map.Type())).Underlying().(*types.Map) + if !ok { + return "MapUpdate target is not a map" + } + if !types.Identical(a.typeOf(update.Key.Type()), a.typeOf(mapType.Key())) { + return "MapUpdate key does not match the map key type" + } + if !types.Identical(a.typeOf(update.Value.Type()), a.typeOf(mapType.Elem())) { + return "MapUpdate value does not match the map element type" + } + for name, typ := range map[string]types.Type{ + "map": update.Map.Type(), "key": update.Key.Type(), "value": update.Value.Type(), + } { + if err := validateCoroPhysicalSSAValueType(a.typeOf(typ)); err != nil { + return "MapUpdate " + name + " has unsupported type: " + err.Error() + } + } + return a.requireFrozenStructuredRuntimeHelpers(update, "AllocU", "MapAssign") +} + +func (a *coroPhysicalPureSSAAudit) validateRange(rng *ssa.Range) string { + if rng == nil || rng.X == nil { + return "incomplete range iterator construction" + } + var helper string + sourceType := a.typeOf(rng.X.Type()) + if physicalString, stringSource := coroPhysicalRangeStringType(sourceType); stringSource { + helper = "NewStringIter" + sourceType = physicalString + } else { + switch source := types.Unalias(sourceType).Underlying().(type) { + case *types.Basic: + return "Range basic source is not a string" + case *types.Map: + helper = "NewMapIter" + default: + return fmt.Sprintf("Range has unsupported source type %T", source) + } + } + if err := validateCoroPhysicalSSAValueType(sourceType); err != nil { + return "Range source has unsupported type: " + err.Error() + } + // x/tools intentionally gives Range an opaque iterator type. The exact + // helper result supplies the physical pointer representation; Next below + // proves that the opaque value never escapes that pair of lowerings. + refs := rng.Referrers() + if refs == nil { + return "Range iterator has no frozen use list" + } + for _, ref := range *refs { + next, ok := ref.(*ssa.Next) + if !ok || next.Iter != rng || next.Parent() != rng.Parent() { + return "Range iterator escapes its exact Next lowering" + } + } + return a.requireFrozenStructuredRuntimeHelpers(rng, helper) +} + +func (a *coroPhysicalPureSSAAudit) validateNext(next *ssa.Next) string { + if next == nil || next.Iter == nil || next.Type() == nil { + return "incomplete range iterator advance" + } + rng, ok := next.Iter.(*ssa.Range) + if !ok || rng.X == nil || rng.Parent() != next.Parent() { + return "Next does not consume one exact local Range iterator" + } + result, ok := types.Unalias(a.typeOf(next.Type())).Underlying().(*types.Tuple) + if !ok || result.Len() != 3 || !coroPhysicalBoolType(a.typeOf(result.At(0).Type())) { + return "Next result is not an exact (bool, key, value) tuple" + } + var helper string + var keyType, valueType types.Type + sourceType := a.typeOf(rng.X.Type()) + if _, stringSource := coroPhysicalRangeStringType(sourceType); stringSource { + if !next.IsString { + return "Next string marker disagrees with its Range source" + } + helper = "StringIterNext" + keyType, valueType = types.Typ[types.Int], types.Typ[types.Rune] + } else { + switch source := types.Unalias(sourceType).Underlying().(type) { + case *types.Basic: + return "Next string marker disagrees with its Range source" + case *types.Map: + if next.IsString { + return "Next map iterator is marked as a string iterator" + } + helper = "MapIterNext" + keyType, valueType = source.Key(), source.Elem() + default: + return fmt.Sprintf("Next has unsupported Range source type %T", source) + } + } + for index, expected := range []types.Type{keyType, valueType} { + actual := a.typeOf(result.At(index + 1).Type()) + if coroPhysicalInvalidType(actual) { + continue + } + if !types.Identical(actual, a.typeOf(expected)) { + return fmt.Sprintf("Next tuple field %d does not match the range source", index+1) + } + if err := validateCoroPhysicalSSAValueType(actual); err != nil { + return fmt.Sprintf("Next tuple field %d has unsupported type: %v", index+1, err) + } + } + return a.requireFrozenStructuredRuntimeHelpers(next, helper) +} + +// coroPhysicalRangeStringType gives an untyped string constant the concrete +// string representation that Builder.Range already emits. x/tools retains the +// constant's untyped basic kind in Range.X even though Go default typing at +// this operation is string; rejecting it would make a valid standard-library +// range depend on an incidental SSA type spelling. +func coroPhysicalRangeStringType(typ types.Type) (types.Type, bool) { + if typ == nil { + return nil, false + } + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + if !ok || basic.Kind() != types.String && basic.Kind() != types.UntypedString { + return nil, false + } + if basic.Kind() == types.UntypedString { + return types.Typ[types.String], true + } + return typ, true +} + +func coroPhysicalBoolType(typ types.Type) bool { + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + return ok && basic.Kind() == types.Bool +} + +func coroPhysicalInvalidType(typ types.Type) bool { + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + return ok && basic.Kind() == types.Invalid +} + +func (a *coroPhysicalPureSSAAudit) validateChangeType(change *ssa.ChangeType) string { + if change == nil || change.X == nil { + return "incomplete value-preserving type change" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(change.X.Type())); err != nil { + return "type-change source is unsupported: " + err.Error() + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(change.Type())); err != nil { + return "type-change result is unsupported: " + err.Error() + } + return a.requireNoRuntimeHelpers(change) +} + +func (a *coroPhysicalPureSSAAudit) validateConvert(convert *ssa.Convert) string { + if convert == nil || convert.X == nil { + return "incomplete conversion" + } + source, target := a.typeOf(convert.X.Type()), a.typeOf(convert.Type()) + if !coroPureConversion(source, target) { + helper := coroRuntimeConversionHelper(source, target) + if helper == "" { + return "conversion may allocate or call the runtime; pure coroutine conversion supports only numeric and pointer/unsafe-pointer representations" + } + if err := validateCoroPhysicalSSAValueType(source); err != nil { + return "conversion source is unsupported: " + err.Error() + } + if err := validateCoroPhysicalSSAValueType(target); err != nil { + return "conversion result is unsupported: " + err.Error() + } + // LLSSA lowers every supported string conversion through exactly one + // named runtime helper. Bind that independently-derived recipe to the + // owner-scoped lowered-call plan; allocation remains legal only when the + // helper is a demanded no-suspend/no-unwind plain body (or an explicitly + // structured coroutine helper under the same shared gate). + return a.requireFrozenExactRuntimeHelper(convert, helper) + } + proof := a.currentFrameRetentionProof() + if coroFrameRetentionPointerLike(source) && coroFrameRetentionUintptrLike(target) && + (proof == nil || !proof.provesTraceableUintptr(convert)) && + !a.coroPointerUintptrScalarTerminal(convert) && + (a.universe == nil || !a.universe.coroRuntimeCodeAddressType(source)) { + reason := "pointer-to-uintptr conversion is not bound to an exact managed-child/worker uintptrkeepalive source or scalar terminal" + if coroPointerUintptrScalarTerminal(convert) && a != nil && a.plan != nil && a.fn != nil { + plan, planned := a.plan.FunctionPlan(a.fn) + reason += fmt.Sprintf(" (structural scalar terminal; planned=%t effect=%s exec=%s)", planned, plan.Effect, plan.Exec) + } + return reason + } + if coroFrameRetentionUintptrLike(source) && coroFrameRetentionPointerLike(target) && + (proof == nil || !proof.provesTraceableUintptr(convert.X)) && + !a.provesWorkerForeignPointerResult(convert.X) { + return "uintptr-to-pointer conversion has no traceable exact pointer provenance" + } + if err := validateCoroPhysicalSSAValueType(source); err != nil { + return "conversion source is unsupported: " + err.Error() + } + if err := validateCoroPhysicalSSAValueType(target); err != nil { + return "conversion result is unsupported: " + err.Error() + } + return a.requireNoRuntimeHelpers(convert) +} + +// coroPointerUintptrScalarTerminal binds the structural scalar-observation +// recipe below to the immutable whole-function suspension plan. A same-block +// chain by itself is insufficient: an ordinary budget poll could split a +// NeedsPreempt body. OutcomeStructured is allowed because it describes only +// terminal Return/Panic transport; every actual suspension effect and explicit +// preemption requirement remains rejected. The conservative/no-GC frame +// profile is still the authority for LLVM motion inside this bounded body. +func (a *coroPhysicalPureSSAAudit) coroPointerUintptrScalarTerminal(value ssa.Value) bool { + if a == nil || a.plan == nil || a.fn == nil || !coroPointerUintptrScalarTerminal(value) { + return false + } + plan, planned := a.plan.FunctionPlan(a.fn) + return planned && !plan.Exec.Contains(coro.NeedsPreempt) && + plan.Effect&^coro.OutcomeStructured == coro.NoSuspend +} + +// provesWorkerForeignPointerResult accepts one exact producer-injected result +// fact from a certified worker call. It deliberately recognizes only a direct +// tuple extract: integer arithmetic, storage, Phi merging, and arbitrary +// uintptr parameters cannot acquire pointer provenance after the fact. The +// callable shadow carries this metadata forward from FuncPCABI0 formation, +// and validateCoroWorkerSyscallCall joins it with the immutable whole-program +// plan before physical lowering may consume it. +func (a *coroPhysicalPureSSAAudit) provesWorkerForeignPointerResult(value ssa.Value) bool { + if a == nil || a.plan == nil || a.universe == nil || value == nil { + return false + } + extract, ok := value.(*ssa.Extract) + if !ok || extract.Index < 0 || extract.Index >= 8 { + return false + } + call, ok := extract.Tuple.(*ssa.Call) + if !ok || call == nil || call.Parent() != a.fn { + return false + } + if validateCoroWorkerSyscallCall(a.plan, a.universe, call) == nil { + certificate, certified, err := a.universe.CoroWorkerSyscallCertificate(call) + return err == nil && certified && certificate.ID != "" && + certificate.ForeignPointerResultMask&(uint8(1)<=/!= only invert or swap its pure boolean result. The helper + // must remain a demanded no-suspend/no-unwind body in the frozen plan. + return a.requireFrozenExactRuntimeHelper(op, helper) + } + } + if (op.Op == token.EQL || op.Op == token.NEQ) && + (coroInterfaceType(a.typeOf(op.X.Type())) && coroFrameRetentionNilConst(op.Y) || + coroInterfaceType(a.typeOf(op.Y.Type())) && coroFrameRetentionNilConst(op.X)) { + if err := validateCoroPhysicalSSAValueType(a.typeOf(op.Type())); err != nil { + return "empty-interface nil comparison has unsupported result type: " + err.Error() + } + // Physical codegen compares the empty-interface type word directly. The + // ordinary helper inventory still records LLSSA's EfaceEqual recipe (and + // permits IfaceType for future interface normalization), but neither call + // is emitted by this exact instruction. + return a.requireOnlyCompilerElidedRuntimeHelpers(op, "EfaceEqual", "IfaceType") + } + if op.Op == token.EQL || op.Op == token.NEQ { + leftInterface, leftOK := types.Unalias(a.typeOf(op.X.Type())).Underlying().(*types.Interface) + rightInterface, rightOK := types.Unalias(a.typeOf(op.Y.Type())).Underlying().(*types.Interface) + if leftOK || rightOK { + for _, typ := range []types.Type{a.typeOf(op.X.Type()), a.typeOf(op.Y.Type()), a.typeOf(op.Type())} { + if err := validateCoroPhysicalSSAValueType(typ); err != nil { + return "interface equality has unsupported physical value type: " + err.Error() + } + } + helpers := []string{"EfaceEqual"} + if leftOK && !leftInterface.Empty() || rightOK && !rightInterface.Empty() { + helpers = append(helpers, "IfaceType") + } + // LLSSA normalizes non-empty interfaces through IfaceType and then + // compares the two dynamic values through EfaceEqual. EfaceEqual may + // panic for an uncomparable dynamic type, so every helper must use its + // exact owner-scoped plain/child-await lowering and a MayUnwind helper + // must return through ExplicitStatus. This preserves ordinary Go + // interface comparison semantics without native-stack unwinding across + // the live LLVM coroutine frame. + return a.requireFrozenStructuredRuntimeHelpers(op, helpers...) + } + } + if (op.Op == token.EQL || op.Op == token.NEQ) && + ((coroPureNilComparableType(a.typeOf(op.X.Type())) && coroFrameRetentionNilConst(op.Y)) || + (coroPureNilComparableType(a.typeOf(op.Y.Type())) && coroFrameRetentionNilConst(op.X))) { + return a.requireNoRuntimeHelpers(op) + } + if !coroPureBasicScalar(a.typeOf(op.Type())) || !coroPureBasicScalar(a.typeOf(op.X.Type())) || !coroPureBasicScalar(a.typeOf(op.Y.Type())) { + return "potentially panicking or non-scalar binary operation" + } + switch op.Op { + case token.QUO, token.REM: + operand, _ := types.Unalias(a.typeOf(op.X.Type())).Underlying().(*types.Basic) + // Only integer division and remainder can panic on a zero divisor. + // Floating-point division follows Go's IEEE-754 semantics and produces + // infinities or NaNs, so it requires neither a panic helper nor a + // non-zero dominance proof. + if operand != nil && operand.Info()&types.IsInteger != 0 && !ssaIntegerValueProvenNonZeroAt(op.Y, op) { + return a.requireFrozenOutcomeRuntimeHelper(op, "AssertDivideByZero") + } + case token.SHL, token.SHR: + if signedIntegerMayBeNegative(op.Y) { + // Builder.BinOp emits exactly one AssertNegativeShift predicate + // before the LLVM shift. Under ExplicitStatus that potentially + // panicking helper must be a managed outcome child, so a negative + // count enters the parent's ordinary panic/cleanup path without + // unwinding a native stack through the live coroutine frame. + return a.requireFrozenOutcomeRuntimeHelper(op, "AssertNegativeShift") + } + } + return a.requireNoRuntimeHelpers(op) +} + +func coroEmptyInterfaceType(typ types.Type) bool { + if typ == nil { + return false + } + iface, ok := types.Unalias(typ).Underlying().(*types.Interface) + if !ok { + return false + } + iface.Complete() + return iface.Empty() +} + +func coroInterfaceType(typ types.Type) bool { + if typ == nil { + return false + } + _, ok := types.Unalias(typ).Underlying().(*types.Interface) + return ok +} + +func coroPureStringType(typ types.Type) bool { + if typ == nil { + return false + } + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + return ok && basic.Kind() == types.String +} + +func coroPureAggregateType(typ types.Type) bool { + if typ == nil { + return false + } + switch types.Unalias(typ).Underlying().(type) { + case *types.Array, *types.Struct: + return true + default: + return false + } +} + +// coroPureAggregateEqualityType mirrors the helper-free recursive cases in +// LLSSA Builder.BinOp. It is intentionally narrower than Go comparability: +// strings need StringEqual and interfaces need EfaceEqual (and may panic for a +// dynamically uncomparable payload), so neither can enter a PhysicalABIV1 +// coroutine through this certificate. Blank struct fields are not compared by +// Go or LLSSA and therefore contribute no leaf requirement. +func coroPureAggregateEqualityType(typ types.Type, visiting map[types.Type]bool) bool { + if typ == nil || visiting[typ] { + return false + } + visiting[typ] = true + defer delete(visiting, typ) + switch underlying := types.Unalias(typ).Underlying().(type) { + case *types.Basic: + return underlying.Kind() == types.UnsafePointer || + underlying.Info()&(types.IsBoolean|types.IsInteger|types.IsFloat|types.IsComplex) != 0 + case *types.Pointer, *types.Chan: + return true + case *types.Array: + return coroPureAggregateEqualityType(underlying.Elem(), visiting) + case *types.Struct: + for index := 0; index < underlying.NumFields(); index++ { + field := underlying.Field(index) + if field.Name() == "_" { + continue + } + if !coroPureAggregateEqualityType(field.Type(), visiting) { + return false + } + } + return true + default: + return false + } +} + +// coroPureDirectEqualityType is deliberately narrower than Go's comparable +// set. These representations lower to target-local scalar comparisons and +// cannot invoke user/runtime code or panic. Interfaces and aggregate values +// remain outside this gate; map, slice, and function values remain legal only +// through the existing comparison-to-nil path. +func coroPureDirectEqualityType(typ types.Type) bool { + if typ == nil { + return false + } + switch underlying := types.Unalias(typ).Underlying().(type) { + case *types.Pointer, *types.Chan: + return true + case *types.Basic: + return underlying.Kind() == types.UnsafePointer || underlying.Info()&types.IsComplex != 0 + default: + return false + } +} + +func coroPureNilComparableType(typ types.Type) bool { + if typ == nil { + return false + } + switch underlying := types.Unalias(typ).Underlying().(type) { + case *types.Pointer, *types.Slice, *types.Map, *types.Chan, *types.Signature, *types.Interface: + return true + case *types.Basic: + return underlying.Kind() == types.UnsafePointer + default: + return false + } +} + +func (a *coroPhysicalPureSSAAudit) validateUnOp(op *ssa.UnOp) string { + if op == nil || op.X == nil { + return "incomplete unary operation" + } + if op.Op != token.MUL { + if !coroPureBasicScalar(a.typeOf(op.Type())) { + return "unsupported unary operation" + } + return a.requireNoRuntimeHelpers(op) + } + if _, reason := a.stableAddressAt(op.X, op, make(map[ssa.Value]bool)); reason != "" { + return "typed load: " + reason + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(op.Type())); err != nil { + return "typed load has unsupported value type: " + err.Error() + } + // A zero-sized load still has Go's nil-dereference semantics. Physical + // coroutine code emits the same explicit-status nil guard as an ordinary + // load and then materializes the zero value without touching memory. The + // legacy AssertNilDeref inventory entry is therefore compiler-elided by the + // independently validated frame-retention/fault proof below. + if a.allowImplicitNilFault { + proof := a.currentFrameRetentionProof() + if proof != nil && proof.requiresImplicitNilFault(op.X, op) { + // Value-receiver calls use AssertNilDerefPtr on the native stack so + // the checked pointer remains available to the subsequent load. The + // physical coroutine recipe preserves that same base value across its + // explicit-status branch and therefore elides either helper spelling. + return a.requireOnlyCompilerElidedRuntimeHelpers(op, "AssertNilDeref", "AssertNilDerefPtr") + } + } + return a.requireNoRuntimeHelpersExcept(op, "AssertNilDeref", "AssertNilDerefPtr") +} + +func (a *coroPhysicalPureSSAAudit) validateStore(store *ssa.Store) string { + if store == nil || store.Addr == nil || store.Val == nil { + return "incomplete typed store" + } + if index, ok := store.Addr.(*ssa.IndexAddr); ok && a.ctx != nil && emissionIsVargsAlloc(a.ctx, index.X) { + value := store.Val + if boxed, ok := value.(*ssa.MakeInterface); ok { + value = boxed.X + } + if value == nil { + return "synthetic varargs store has no concrete operand" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(value.Type())); err != nil { + return "synthetic varargs operand has unsupported type: " + err.Error() + } + return "" + } + root, reason := a.stableAddressAt(store.Addr, store, make(map[ssa.Value]bool)) + if reason != "" { + return "typed store: " + reason + } + pointer, ok := types.Unalias(a.typeOf(store.Addr.Type())).Underlying().(*types.Pointer) + if !ok || !types.Identical(pointer.Elem(), a.typeOf(store.Val.Type())) { + return "typed store address/value types do not match" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(store.Val.Type())); err != nil { + return "typed store has unsupported value type: " + err.Error() + } + if root == coroPhysicalAddressGlobal && + coroTypeContainsGCPointer(a.typeOf(store.Val.Type()), make(map[types.Type]bool)) && + !a.coroBarrierFreeGlobalStoreProfile() { + return "global typed store of a pointer-containing value requires explicit write-barrier lowering" + } + // A pointer-containing frame-local, exact managed-heap, or certified global + // store is accepted only under PhysicalABIV1's current non-moving + // conservative/non-collecting profile. BDWGC and tinygogc rescan globals; + // nogc targets retain them for the process lifetime. This is not evidence + // that precise frame maps, relocation, or write barriers are implemented. + return a.requireNoRuntimeHelpers(store) +} + +func (a *coroPhysicalPureSSAAudit) coroBarrierFreeGlobalStoreProfile() bool { + if a == nil || emitShadowStackInstrumentation { + return false + } + switch a.frameRetentionABI { + case CoroFrameRetentionParkABIV2: + // These are the only active identities backed by the frozen + // physical-v1.nonmoving-conservative-or-none root profile. A future + // precise or moving collector must use a new identity and remains + // rejected above until it supplies real global write barriers. + return true + default: + return false + } +} + +func (a *coroPhysicalPureSSAAudit) validateBuiltin(call *ssa.Call) string { + if call == nil || call.Call.Value == nil { + return "unsupported builtin call in pure coroutine body" + } + builtin, ok := call.Call.Value.(*ssa.Builtin) + if !ok { + return "dynamic/non-builtin call is outside pure SSA lowering" + } + switch builtin.Name() { + case "Sizeof", "Alignof": + if len(call.Call.Args) != 1 || call.Type() == nil || + !types.Identical(a.typeOf(call.Type()), types.Typ[types.Uintptr]) { + return builtin.Name() + " builtin requires one type operand and a uintptr result" + } + operand := a.typeOf(call.Call.Args[0].Type()) + if operand == nil || coroTypeContainsUnresolvedTypeParam(operand, make(map[types.Type]bool)) { + return builtin.Name() + " builtin has no concrete physical operand type" + } + // The operand is deliberately not validated as a live SSA value: + // collectUnsafeSizeAlignUnevaluatedSSA removes its type-only producer + // graph, and compileUnsafeSizeAlignBuiltin emits one target-derived + // integer constant without a runtime edge. + return a.requireNoRuntimeHelpers(call) + case "ssa:wrapnilchk": + if len(call.Call.Args) != 3 || call.Type() == nil || + !types.Identical(a.typeOf(call.Type()), a.typeOf(call.Call.Args[0].Type())) { + return "ssa:wrapnilchk builtin has an invalid receiver/result shape" + } + if _, ok := types.Unalias(a.typeOf(call.Call.Args[0].Type())).Underlying().(*types.Pointer); !ok { + return "ssa:wrapnilchk receiver is not pointer-shaped" + } + for _, index := range []int{1, 2} { + basic, ok := types.Unalias(a.typeOf(call.Call.Args[index].Type())).Underlying().(*types.Basic) + if !ok || basic.Kind() != types.String { + return "ssa:wrapnilchk metadata is not string-shaped" + } + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(call.Type())); err != nil { + return "builtin result has unsupported type: " + err.Error() + } + if a.allowImplicitNilFault { + // ExplicitStatus codegen owns this exact synthetic guard: it emits an + // inline pointer test and publishes the nil branch through the same + // structured panic handoff as an implicit dereference fault. The legacy + // PanicWrapNilPointer helper is therefore not called from this physical + // coroutine body and needs no hidden unwind contract here. + return "" + } + // LLSSA lowers this synthetic wrapper guard to a pointer comparison and + // the same terminal PanicWrapNilPointer edge used by ordinary checked + // dereferences. The helper cannot return to the live coroutine frame. + return a.requireFrozenTerminalRuntimeHelpers(call, "PanicWrapNilPointer") + case "len": + return a.validateLenBuiltin(call) + case "cap": + return a.validateCapBuiltin(call) + case "append": + return a.validateAppendBuiltin(call) + case "copy": + return a.validateCopyBuiltin(call) + case "real", "imag": + if reason := a.validateComplexComponentBuiltin(call, builtin.Name()); reason != "" { + return reason + } + case "min", "max": + return a.validateMinMaxBuiltin(call, builtin.Name()) + case "print", "println": + return a.validatePrintBuiltin(call, builtin.Name()) + case "delete": + return a.validateDeleteBuiltin(call) + case "clear": + return a.validateClearBuiltin(call) + case "close": + return a.validateCloseBuiltin(call) + case "recover": + if !a.allowExplicitRecover || len(call.Call.Args) != 0 || call.Type() == nil { + return "recover builtin requires the explicit-status physical ABI and zero arguments" + } + result, ok := types.Unalias(a.typeOf(call.Type())).Underlying().(*types.Interface) + if !ok || !result.Empty() { + return "recover builtin result is not one empty interface" + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(call.Type())); err != nil { + return "recover builtin result has unsupported type: " + err.Error() + } + return a.requireOnlyCompilerElidedRuntimeHelpers(call, "Recover") + case "Add": + if !coroPhysicalUnsafeAddCall(call, a.typeOf) { + return "unsafe.Add builtin has an invalid frozen pointer/integer shape" + } + case "String": + return a.validateUnsafeStringBuiltin(call) + case "Slice": + return a.validateUnsafeSliceBuiltin(call) + case "StringData", "SliceData": + return a.validateUnsafeDataBuiltin(call, builtin.Name()) + default: + return fmt.Sprintf("builtin %q is outside the pure coroutine lowering slice", builtin.Name()) + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(call.Type())); err != nil { + return "builtin result has unsupported type: " + err.Error() + } + return a.requireNoRuntimeHelpers(call) +} + +type coroPhysicalLenOperandKind uint8 + +const ( + coroPhysicalLenUnsupported coroPhysicalLenOperandKind = iota + coroPhysicalLenInline + coroPhysicalLenMap + coroPhysicalLenChan +) + +// coroPhysicalLenKind accepts only one concrete lowering selected from the +// Go type of the SSA operand. A map whose key or element remains parameterized +// is still exact: len observes only the map header. A bare type parameter or +// interface is deliberately rejected because its type set may require +// different string/slice/map/channel lowerings at different instantiations. +func coroPhysicalLenKind(typ types.Type) coroPhysicalLenOperandKind { + if typ == nil { + return coroPhysicalLenUnsupported + } + switch operand := types.Unalias(typ).Underlying().(type) { + case *types.Slice: + return coroPhysicalLenInline + case *types.Map: + return coroPhysicalLenMap + case *types.Chan: + return coroPhysicalLenChan + case *types.Basic: + if operand.Kind() == types.String { + return coroPhysicalLenInline + } + } + return coroPhysicalLenUnsupported +} + +func (a *coroPhysicalPureSSAAudit) validateLenBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 1 || call.Type() == nil || + !types.Identical(a.typeOf(call.Type()), types.Typ[types.Int]) { + return "len builtin has an invalid argument/result shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "len" { + return "len validation requires the exact builtin call" + } + argument := call.Common().Args[0] + if argument == nil { + return "len builtin has a nil operand" + } + switch coroPhysicalLenKind(a.typeOf(argument.Type())) { + case coroPhysicalLenInline: + return a.requireNoRuntimeHelpers(call) + case coroPhysicalLenMap: + // Builder.BuiltinCall lowers this exact form through MapLen. Bind the + // occurrence to its owner-scoped helper plan instead of treating the + // helper name or every generic len operand as intrinsically pure. + return a.requireFrozenExactRuntimeHelper(call, "MapLen") + case coroPhysicalLenChan: + // Channel direction and element type do not alter the header operation. + // The observable timer-channel view and channel lock still belong to the + // exact owner-scoped ChanLen helper rather than an inline load. + return a.requireFrozenExactRuntimeHelper(call, "ChanLen") + default: + return "len builtin has no concrete slice, string, map, or channel lowering" + } +} + +type coroPhysicalCapOperandKind uint8 + +const ( + coroPhysicalCapUnsupported coroPhysicalCapOperandKind = iota + coroPhysicalCapInline + coroPhysicalCapChan +) + +// coroPhysicalCapKind admits only outer representations whose physical cap +// lowering is fixed without inspecting a type set. A parameterized slice or +// channel remains exact; a bare type parameter or interface does not. +func coroPhysicalCapKind(typ types.Type) coroPhysicalCapOperandKind { + if typ == nil { + return coroPhysicalCapUnsupported + } + switch types.Unalias(typ).Underlying().(type) { + case *types.Slice: + return coroPhysicalCapInline + case *types.Chan: + return coroPhysicalCapChan + default: + return coroPhysicalCapUnsupported + } +} + +func (a *coroPhysicalPureSSAAudit) validateCapBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 1 || call.Type() == nil || + !types.Identical(a.typeOf(call.Type()), types.Typ[types.Int]) { + return "cap builtin has an invalid argument/result shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "cap" { + return "cap validation requires the exact builtin call" + } + argument := call.Common().Args[0] + if argument == nil { + return "cap builtin has a nil operand" + } + switch coroPhysicalCapKind(a.typeOf(argument.Type())) { + case coroPhysicalCapInline: + return a.requireNoRuntimeHelpers(call) + case coroPhysicalCapChan: + return a.requireFrozenExactRuntimeHelper(call, "ChanCap") + default: + return "cap builtin has no concrete slice or channel lowering" + } +} + +// validateClearBuiltin freezes Builder.BuiltinCall's two Go-defined clear +// forms. Slice clearing delegates to SliceClear (including the element-width +// calculation and target memset); map clearing delegates to MapClear. Neither +// form returns a value, and the selected helper must remain in the exact +// owner-scoped lowering plan so future GC/write-barrier work cannot silently +// turn a direct call into an unsafe native-stack suspension. +func (a *coroPhysicalPureSSAAudit) validateClearBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 1 { + return "clear builtin has an invalid argument/result shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "clear" { + return "clear validation requires the exact builtin call" + } + if result := call.Type(); result != nil { + tuple, ok := types.Unalias(a.typeOf(result)).Underlying().(*types.Tuple) + if !ok || tuple.Len() != 0 { + return "clear builtin has an invalid argument/result shape" + } + } + argument := call.Common().Args[0] + if argument == nil { + return "clear builtin has a nil operand" + } + argumentType := a.typeOf(argument.Type()) + var helper string + switch types.Unalias(argumentType).Underlying().(type) { + case *types.Slice: + helper = "SliceClear" + case *types.Map: + helper = "MapClear" + default: + return "clear builtin operand is neither a slice nor a map" + } + if err := validateCoroPhysicalSSAValueType(argumentType); err != nil { + return "clear builtin operand has unsupported physical type: " + err.Error() + } + return a.requireFrozenExactRuntimeHelper(call, helper) +} + +// validateCloseBuiltin binds close(ch) to the coroutine runtime's +// non-panicking scalar outcome helper. Nil and already-closed errors are +// published by compiler-owned explicit status, never by unwinding through the +// live LLVM frame. +func (a *coroPhysicalPureSSAAudit) validateCloseBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 1 { + return "close builtin has an invalid argument/result shape" + } + if result := call.Type(); result != nil { + tuple, ok := types.Unalias(a.typeOf(result)).Underlying().(*types.Tuple) + if !ok || tuple.Len() != 0 { + return "close builtin has an invalid argument/result shape" + } + } + if _, ok := types.Unalias(a.typeOf(call.Common().Args[0].Type())).Underlying().(*types.Chan); !ok { + return "close builtin argument is not a channel" + } + if !a.allowImplicitNilFault { + return "close builtin requires the explicit-status panic ABI" + } + return a.requireFrozenExactRuntimeHelper(call, "CoroChanTryClose") +} + +// validateUnsafeDataBuiltin accepts only the two header projection intrinsics. +// LLSSA lowers both to extractvalue of the already-materialized Go +// string/slice header; there is no allocation, bounds check, panic edge, or +// hidden runtime call. Pointer lifetime remains governed by the ordinary +// coroutine value/keepalive analysis of the source aggregate. +func (a *coroPhysicalPureSSAAudit) validateUnsafeDataBuiltin(call *ssa.Call, name string) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 1 || call.Type() == nil { + return "unsafe." + name + " builtin has an invalid call shape" + } + argument := a.typeOf(call.Common().Args[0].Type()) + result := a.typeOf(call.Type()) + if argument == nil || result == nil { + return "unsafe." + name + " builtin has no concrete argument/result type" + } + pointer, ok := types.Unalias(result).(*types.Pointer) + if !ok { + return "unsafe." + name + " result is not pointer-shaped" + } + switch name { + case "StringData": + basic, ok := types.Unalias(argument).Underlying().(*types.Basic) + if !ok || basic.Kind() != types.String || !types.Identical(pointer.Elem(), types.Typ[types.Byte]) { + return "unsafe.StringData requires one string argument and a *byte result" + } + case "SliceData": + slice, ok := types.Unalias(argument).Underlying().(*types.Slice) + if !ok || !types.Identical(pointer.Elem(), slice.Elem()) { + return "unsafe.SliceData requires one []T argument and a *T result" + } + default: + return "unsupported unsafe data builtin " + name + } + if err := validateCoroPhysicalSSAValueType(result); err != nil { + return "unsafe." + name + " result has unsupported type: " + err.Error() + } + return a.requireNoRuntimeHelpers(call) +} + +// validateDeleteBuiltin binds the language builtin to the same owner-scoped +// map-key allocation and MapDelete helpers used by ordinary LLSSA lowering. +// In particular, delete is not assumed non-blocking: each helper must still be +// proven plain/no-unwind or represented as a managed coroutine child. +func (a *coroPhysicalPureSSAAudit) validateDeleteBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil { + return "delete builtin has an invalid call shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "delete" || len(call.Common().Args) != 2 { + return "delete validation requires the exact two-argument builtin call" + } + mapping, key := call.Common().Args[0], call.Common().Args[1] + if mapping == nil || key == nil { + return "delete builtin has a nil map or key operand" + } + mapType, ok := types.Unalias(a.typeOf(mapping.Type())).Underlying().(*types.Map) + if !ok { + return "delete target is not a map" + } + if !types.Identical(a.typeOf(key.Type()), a.typeOf(mapType.Key())) { + return "delete key does not match the map key type" + } + for name, typ := range map[string]types.Type{"map": mapping.Type(), "key": key.Type()} { + if err := validateCoroPhysicalSSAValueType(a.typeOf(typ)); err != nil { + return "delete " + name + " has unsupported type: " + err.Error() + } + } + return a.requireFrozenStructuredRuntimeHelpers(call, "AllocU", "MapDelete") +} + +// validatePrintBuiltin freezes Builder.PrintEx's exact lowering. Printing is +// not classified as a pure/no-block operation: every emitted Print* helper is +// an ordinary owner-scoped managed edge. Consequently a helper that reaches a +// potentially blocking host output call must itself be represented by a +// coroutine (and awaited here); only a plan-proven NoSuspend/NoUnwind helper +// may remain a direct plain call. +func (a *coroPhysicalPureSSAAudit) validatePrintBuiltin(call *ssa.Call, name string) string { + if call == nil || call.Common() == nil || name != "print" && name != "println" { + return "print builtin has an invalid call shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != name { + return "print validation requires the exact builtin call" + } + helperSet := make(map[string]struct{}, len(call.Common().Args)+1) + for index, argument := range call.Common().Args { + if argument == nil { + return fmt.Sprintf("%s builtin argument %d is nil", name, index) + } + typ := a.typeOf(argument.Type()) + helper := runtimePrintHelper(typ) + if helper == "" { + return fmt.Sprintf("%s builtin argument %d has unsupported type %s", name, index, typ) + } + helperSet[helper] = struct{}{} + if err := validateCoroPhysicalSSAValueType(typ); err != nil { + return fmt.Sprintf("%s builtin argument %d has unsupported physical type: %v", name, index, err) + } + } + + // print() emits nothing. println(), including println(), always emits the + // trailing newline through PrintByte, so it remains a managed helper edge. + if name == "print" && len(call.Common().Args) == 0 { + return "" + } + if name == "println" { + helperSet["PrintByte"] = struct{}{} + } + if a == nil || a.ctx == nil || a.universe == nil { + return "structured runtime helper validation requires a frozen emission universe" + } + expected := make([]string, 0, len(helperSet)) + for helper := range helperSet { + expected = append(expected, helper) + } + sort.Strings(expected) + if len(expected) == 0 { + return name + " builtin has no exact lowered runtime helper inventory" + } + return a.requireFrozenStructuredRuntimeHelpers(call, expected...) +} + +// validateMinMaxBuiltin mirrors Builder.compareSelect: ordered scalar values +// are lowered to comparisons plus LLVM selects. String ordering additionally +// uses the owner-scoped runtime.StringLess edge for every comparison. +func (a *coroPhysicalPureSSAAudit) validateMinMaxBuiltin(call *ssa.Call, name string) string { + if call == nil || call.Common() == nil || call.Type() == nil || name != "min" && name != "max" || len(call.Common().Args) == 0 { + return name + " builtin has an invalid argument/result shape" + } + result := a.typeOf(call.Type()) + basic, ok := types.Unalias(result).Underlying().(*types.Basic) + if !ok || basic.Info()&types.IsUntyped != 0 || + basic.Info()&(types.IsInteger|types.IsFloat) == 0 && basic.Kind() != types.String { + return name + " builtin result is not one ordered concrete basic type" + } + if err := validateCoroPhysicalSSAValueType(result); err != nil { + return name + " builtin result has unsupported physical type: " + err.Error() + } + for index, argument := range call.Common().Args { + if argument == nil { + return fmt.Sprintf("%s builtin argument %d is nil", name, index) + } + argumentType := a.typeOf(argument.Type()) + if !types.Identical(argumentType, result) { + return fmt.Sprintf("%s builtin argument %d type %s differs from result type %s", name, index, argumentType, result) + } + if err := validateCoroPhysicalSSAValueType(argumentType); err != nil { + return fmt.Sprintf("%s builtin argument %d has unsupported physical type: %v", name, index, err) + } + } + if basic.Kind() == types.String && len(call.Common().Args) > 1 { + return a.requireFrozenExactRuntimeHelper(call, "StringLess") + } + return a.requireNoRuntimeHelpers(call) +} + +// validateAppendBuiltin freezes the exact x/tools SSA shape consumed by +// Builder.BuiltinCall. Ordinary scalar append operands have already been +// materialized as the second slice argument by x/tools; the only non-slice +// source shape is Go's append([]byte, string...) special case. +func (a *coroPhysicalPureSSAAudit) validateAppendBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 2 || call.Type() == nil { + return "append builtin has an invalid argument/result shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "append" { + return "append validation requires the exact builtin call" + } + destinationType := a.typeOf(call.Common().Args[0].Type()) + resultType := a.typeOf(call.Type()) + if !types.Identical(destinationType, resultType) { + return "append destination and result slice types differ" + } + destination, ok := types.Unalias(destinationType).Underlying().(*types.Slice) + if !ok { + return "append destination is not a slice" + } + sourceType := a.typeOf(call.Common().Args[1].Type()) + switch source := types.Unalias(sourceType).Underlying().(type) { + case *types.Slice: + if !types.Identical(a.typeOf(destination.Elem()), a.typeOf(source.Elem())) { + return "append source and destination element types differ" + } + case *types.Basic: + if source.Kind() != types.String || + !types.Identical(types.Unalias(a.typeOf(destination.Elem())), types.Typ[types.Byte]) { + return "append non-slice source is not the []byte/string special case" + } + default: + return "append source is neither a compatible slice nor string" + } + for _, typ := range []types.Type{destinationType, sourceType, resultType} { + if err := validateCoroPhysicalSSAValueType(typ); err != nil { + return "append has unsupported physical value type: " + err.Error() + } + } + return a.requireFrozenOutcomeRuntimeHelper(call, "SliceAppend") +} + +// validateCopyBuiltin freezes Builder.BuiltinCall's two legal forms: copying +// between slices with identical element types, and the []byte <- string +// special case. Both lower through the overlap-safe SliceCopy helper and +// return the built-in int type. +func (a *coroPhysicalPureSSAAudit) validateCopyBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 2 || call.Type() == nil { + return "copy builtin has an invalid argument/result shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "copy" { + return "copy validation requires the exact builtin call" + } + if !types.Identical(a.typeOf(call.Type()), types.Typ[types.Int]) { + return "copy builtin result is not the built-in int type" + } + destinationType := a.typeOf(call.Common().Args[0].Type()) + destination, ok := types.Unalias(destinationType).Underlying().(*types.Slice) + if !ok { + return "copy destination is not a slice" + } + sourceType := a.typeOf(call.Common().Args[1].Type()) + switch source := types.Unalias(sourceType).Underlying().(type) { + case *types.Slice: + if !types.Identical(a.typeOf(destination.Elem()), a.typeOf(source.Elem())) { + return "copy source and destination element types differ" + } + case *types.Basic: + if source.Kind() != types.String || + !types.Identical(types.Unalias(a.typeOf(destination.Elem())), types.Typ[types.Byte]) { + return "copy non-slice source is not the []byte/string special case" + } + default: + return "copy source is neither a compatible slice nor string" + } + for _, typ := range []types.Type{destinationType, sourceType, a.typeOf(call.Type())} { + if err := validateCoroPhysicalSSAValueType(typ); err != nil { + return "copy has unsupported physical value type: " + err.Error() + } + } + return a.requireFrozenExactRuntimeHelper(call, "SliceCopy") +} + +// validateComplexComponentBuiltin mirrors Builder.BuiltinCall's extractvalue +// lowering. Go fixes the component type: complex64 yields float32 and +// complex128 yields float64, including when the operand has a defined type +// whose underlying type is complex. +func (a *coroPhysicalPureSSAAudit) validateComplexComponentBuiltin(call *ssa.Call, name string) string { + if call == nil || call.Common() == nil || len(call.Common().Args) != 1 || call.Type() == nil { + return name + " builtin requires one complex argument and one result" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != name || name != "real" && name != "imag" { + return "complex component validation requires the exact real/imag builtin" + } + operand, ok := types.Unalias(a.typeOf(call.Common().Args[0].Type())).Underlying().(*types.Basic) + if !ok { + return name + " builtin argument is not complex" + } + result, ok := types.Unalias(a.typeOf(call.Type())).Underlying().(*types.Basic) + if !ok { + return name + " builtin result is not floating point" + } + want := types.Invalid + switch operand.Kind() { + case types.Complex64: + want = types.Float32 + case types.Complex128: + want = types.Float64 + default: + return name + " builtin argument is not complex" + } + if result.Kind() != want { + return fmt.Sprintf("%s builtin result kind is %s, want %s", name, result, types.Typ[want]) + } + if err := validateCoroPhysicalSSAValueType(a.typeOf(call.Common().Args[0].Type())); err != nil { + return name + " builtin argument has unsupported physical type: " + err.Error() + } + return "" +} + +// coroPhysicalUnsafeAddCall mirrors LLSSA's inline Advance lowering. It only +// recognizes the exact go/ssa shape of unsafe.Add; the eventual dereference +// still needs its own non-nil/address-retention proof. +func coroPhysicalUnsafeAddCall(call *ssa.Call, patch func(types.Type) types.Type) bool { + if call == nil || call.Common() == nil || len(call.Common().Args) != 2 { + return false + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "Add" { + return false + } + typeOf := func(typ types.Type) types.Type { + if patch != nil { + return patch(typ) + } + return typ + } + if !coroFrameRetentionUnsafePointer(typeOf(call.Common().Args[0].Type())) || + !coroFrameRetentionUnsafePointer(typeOf(call.Type())) { + return false + } + offset, ok := types.Unalias(typeOf(call.Common().Args[1].Type())).Underlying().(*types.Basic) + return ok && offset.Info()&types.IsInteger != 0 +} + +type coroPhysicalAddressRoot uint8 + +const ( + coroPhysicalAddressInvalid coroPhysicalAddressRoot = iota + coroPhysicalAddressLocal + coroPhysicalAddressManagedHeap + coroPhysicalAddressGlobal +) + +// stableAddress accepts statically non-nil package/current-frame storage plus +// exact parameter/slice-derived address uses present in the immutable frame- +// retention proof. A parameter's pointer-shaped type alone is never evidence: +// each dereference must carry a dominating non-nil/non-empty fact. +func (a *coroPhysicalPureSSAAudit) stableAddress(value ssa.Value, visiting map[ssa.Value]bool) (coroPhysicalAddressRoot, string) { + return a.stableAddressAt(value, nil, visiting) +} + +func (a *coroPhysicalPureSSAAudit) stableAddressAt(value ssa.Value, use ssa.Instruction, visiting map[ssa.Value]bool) (coroPhysicalAddressRoot, string) { + if value == nil { + return coroPhysicalAddressInvalid, "nil address" + } + if proof := a.currentFrameRetentionProof(); proof != nil && proof.provesDominatedStableAddress(value, use) { + if root, known := a.provenCoroPhysicalAddressRoot(value, make(map[ssa.Value]bool)); known { + return root, "" + } + return coroPhysicalAddressLocal, "" + } + // An address accepted under the explicit-status ABI is dereferenced only on + // the normal edge of its compiler-inserted nil guard. Transport/root + // provenance was frozen independently above; this path never treats a + // pointer-shaped type alone as a lifetime proof. + if a.allowImplicitNilFault { + if proof := a.currentFrameRetentionProof(); proof != nil && proof.provesGuardableStableAddress(value, use) { + if root, known := a.provenCoroPhysicalAddressRoot(value, make(map[ssa.Value]bool)); known { + return root, "" + } + return coroPhysicalAddressLocal, "" + } + } + if visiting[value] { + return coroPhysicalAddressInvalid, "cyclic address expression" + } + visiting[value] = true + defer delete(visiting, value) + switch value := value.(type) { + case *ssa.Global: + if _, ok := types.Unalias(a.typeOf(value.Type())).Underlying().(*types.Pointer); !ok { + return coroPhysicalAddressInvalid, "global address does not have pointer type" + } + return coroPhysicalAddressGlobal, "" + case *ssa.Alloc: + if value.Heap { + if a.frameRetainsManagedHeapAllocation(value) { + return coroPhysicalAddressManagedHeap, "" + } + if !a.frameRetainsAllocation(value) { + return coroPhysicalAddressInvalid, "heap allocation requires managed allocation/root lowering" + } + } + if a.ctx != nil && (a.ctx.skipSyntheticMakeSliceAlloc(value) || isEmissionVargsAlloc(a.ctx, value)) { + return coroPhysicalAddressInvalid, "synthetic slice/varargs storage is not a standalone local address" + } + return coroPhysicalAddressLocal, "" + case *ssa.FieldAddr: + pointer, ok := types.Unalias(a.typeOf(value.X.Type())).Underlying().(*types.Pointer) + if !ok { + return coroPhysicalAddressInvalid, "field base is not a pointer" + } + structure, ok := types.Unalias(pointer.Elem()).Underlying().(*types.Struct) + if !ok || value.Field < 0 || value.Field >= structure.NumFields() { + return coroPhysicalAddressInvalid, "field address is outside its frozen struct shape" + } + return a.stableAddressAt(value.X, use, visiting) + case *ssa.IndexAddr: + pointer, ok := types.Unalias(a.typeOf(value.X.Type())).Underlying().(*types.Pointer) + if !ok { + return coroPhysicalAddressInvalid, "index base is not a fixed-array pointer" + } + array, ok := types.Unalias(pointer.Elem()).Underlying().(*types.Array) + if !ok || !coroConstantIndexInBounds(value.Index, array.Len()) { + return coroPhysicalAddressInvalid, "index may panic; address indexing requires a compile-time in-range fixed-array index" + } + return a.stableAddressAt(value.X, use, visiting) + default: + return coroPhysicalAddressInvalid, fmt.Sprintf( + "address root %T has no exact non-nil frame-retention proof (%s)", + value, coroPhysicalAddressDiagnostic(value, 0, make(map[ssa.Value]bool)), + ) + } +} + +func coroPhysicalAddressDiagnostic(value ssa.Value, depth int, visiting map[ssa.Value]bool) string { + if value == nil { + return "nil" + } + if depth >= 6 || visiting[value] { + return fmt.Sprintf("%T:%s", value, value.Name()) + } + visiting[value] = true + defer delete(visiting, value) + next := func(child ssa.Value) string { + return coroPhysicalAddressDiagnostic(child, depth+1, visiting) + } + switch value := value.(type) { + case *ssa.Convert: + return fmt.Sprintf("convert[%s](%s)", value.Type(), next(value.X)) + case *ssa.ChangeType: + return fmt.Sprintf("changetype[%s](%s)", value.Type(), next(value.X)) + case *ssa.Phi: + edges := make([]string, 0, len(value.Edges)) + for _, edge := range value.Edges { + edges = append(edges, next(edge)) + } + return "phi(" + strings.Join(edges, ",") + ")" + case *ssa.Call: + callee := "dynamic" + if value.Common() != nil && value.Common().StaticCallee() != nil { + callee = value.Common().StaticCallee().String() + } + return "call(" + callee + ")" + case *ssa.FieldAddr: + return fmt.Sprintf("fieldaddr[%d](%s)", value.Field, next(value.X)) + case *ssa.IndexAddr: + return "indexaddr(" + next(value.X) + ")" + default: + return fmt.Sprintf("%T:%s", value, value.Name()) + } +} + +// provenCoroPhysicalAddressRoot classifies an address only after the immutable +// retention proof has authorized that exact value/use pair. It cannot make an +// address stable by itself. Keeping this provenance separate prevents a global +// or managed-heap field address from being mislabeled as a frame-local store +// merely because all three are transport-stable under the current profile. +func (a *coroPhysicalPureSSAAudit) provenCoroPhysicalAddressRoot(value ssa.Value, visiting map[ssa.Value]bool) (coroPhysicalAddressRoot, bool) { + if value == nil || visiting[value] { + return coroPhysicalAddressInvalid, false + } + visiting[value] = true + defer delete(visiting, value) + switch value := value.(type) { + case *ssa.Global: + return coroPhysicalAddressGlobal, true + case *ssa.Alloc: + if value.Heap { + if a.frameRetainsManagedHeapAllocation(value) { + return coroPhysicalAddressManagedHeap, true + } + if !a.frameRetainsAllocation(value) { + return coroPhysicalAddressInvalid, false + } + } + return coroPhysicalAddressLocal, true + case *ssa.FieldAddr: + return a.provenCoroPhysicalAddressRoot(value.X, visiting) + case *ssa.IndexAddr: + return a.provenCoroPhysicalAddressRoot(value.X, visiting) + case *ssa.ChangeType: + return a.provenCoroPhysicalAddressRoot(value.X, visiting) + case *ssa.Convert: + return a.provenCoroPhysicalAddressRoot(value.X, visiting) + case *ssa.Call: + if coroPhysicalUnsafeAddCall(value, a.typeOf) { + return a.provenCoroPhysicalAddressRoot(value.Common().Args[0], visiting) + } + case *ssa.Phi: + root := coroPhysicalAddressInvalid + for _, edge := range value.Edges { + candidate, ok := a.provenCoroPhysicalAddressRoot(edge, visiting) + if !ok { + return coroPhysicalAddressInvalid, false + } + if root == coroPhysicalAddressInvalid { + root = candidate + continue + } + if candidate == coroPhysicalAddressGlobal || root == coroPhysicalAddressGlobal { + root = coroPhysicalAddressGlobal + } else if candidate == coroPhysicalAddressManagedHeap || root == coroPhysicalAddressManagedHeap { + root = coroPhysicalAddressManagedHeap + } + } + return root, root != coroPhysicalAddressInvalid + } + return coroPhysicalAddressInvalid, false +} + +func (a *coroPhysicalPureSSAAudit) plannedRuntimeHelpers(instr ssa.Instruction) ([]string, string) { + if a == nil || a.ctx == nil || a.universe == nil || instr == nil { + return nil, "runtime helper validation requires an exact frozen site plan" + } + helpers, err := a.universe.coroProgramIR.plannedRuntimeHelpers(a.ctx, instr) + if err != nil { + return nil, "load frozen runtime helper site plan: " + err.Error() + } + return helpers, "" +} + +func (a *coroPhysicalPureSSAAudit) requireNoRuntimeHelpers(instr ssa.Instruction) string { + return a.requireNoRuntimeHelpersExcept(instr) +} + +// requireOnlyCompilerElidedRuntimeHelpers verifies that the frozen logical +// helper inventory contains no edge beyond the helpers replaced by this +// instruction's structured ExplicitStatus lowering. Unlike +// requireNoRuntimeHelpersExcept, this is not a domination proof: codegen emits +// none of the listed helpers on either branch. +func (a *coroPhysicalPureSSAAudit) requireOnlyCompilerElidedRuntimeHelpers( + instr ssa.Instruction, + allowed ...string, +) string { + if a == nil || a.ctx == nil || a.universe == nil { + return "" + } + allowedSet := make(map[string]struct{}, len(allowed)) + for _, helper := range allowed { + allowedSet[helper] = struct{}{} + } + helpers, reason := a.plannedRuntimeHelpers(instr) + if reason != "" { + return reason + } + var unexpected []string + for _, helper := range helpers { + if _, ok := allowedSet[helper]; !ok { + unexpected = append(unexpected, helper) + } + } + if len(unexpected) != 0 { + return "operation lowers through non-elided runtime helper(s) " + strings.Join(unexpected, ", ") + } + return "" +} + +// requireFrozenCoroSafeRuntimeHelpers is the narrow capability gate for an +// operation whose canonical LLGo lowering necessarily calls a known runtime +// helper. It accepts no name outside allowed, and still requires the ordinary +// whole-build lowered-call fact plus one demanded coroutine-safe target plan. +// In particular, this does not make arbitrary allocation helpers legal: the +// captured-closure caller names only AllocU and the frozen emission universe +// must bind that exact logical edge to the runtime allocator body. +func (a *coroPhysicalPureSSAAudit) requireFrozenCoroSafeRuntimeHelpers(instr ssa.Instruction, allowed ...string) string { + if a == nil || a.ctx == nil || a.universe == nil || a.plan == nil || a.fn == nil { + return "runtime helper capability validation requires a frozen emission universe" + } + helpers, reason := a.plannedRuntimeHelpers(instr) + if reason != "" { + return reason + } + if len(helpers) == 0 { + return "runtime helper capability validation found no lowered helper" + } + accepted := make(map[string]struct{}, len(allowed)) + for _, helper := range allowed { + accepted[helper] = struct{}{} + } + for _, helper := range helpers { + if _, ok := accepted[helper]; !ok { + return "operation lowers through unapproved runtime helper " + helper + } + } + if !a.allHelpersHaveCoroSafeLowering(helpers) { + return "approved runtime helper(s) lack an exact coroutine-safe lowered-call plan: " + strings.Join(helpers, ", ") + } + return "" +} + +// requireFrozenExactRuntimeHelper is the single-helper form used for a +// non-suspending, non-unwinding runtime operation. It still accepts either a +// proven direct plain target or a managed coroutine target according to the +// shared lowered-call capability gate; it never infers safety from the helper +// name alone. +func (a *coroPhysicalPureSSAAudit) requireFrozenExactRuntimeHelper(instr ssa.Instruction, helper string) string { + if reason := a.requireFrozenCoroSafeRuntimeHelpers(instr, helper); reason != "" { + return reason + } + helpers, reason := a.plannedRuntimeHelpers(instr) + if reason != "" { + return reason + } + if len(helpers) != 1 || helpers[0] != helper { + return "operation does not lower through exactly one " + helper + " helper" + } + target, planned := a.plan.ResolveLoweredCall(a.fn, helper) + if !planned || target == nil { + return "runtime helper " + helper + " lacks an exact lowered-call target" + } + return "" +} + +// requireFrozenOutcomeRuntimeHelper is the stricter gate for a language +// operation whose runtime implementation can panic on ordinary input. A plain +// helper, even a currently small one, cannot unwind through a live LLVM +// coroutine frame. The exact owner-scoped helper must therefore be an +// ExplicitStatus coroutine whose Return/Panic outcome is consumed by the +// shared child-await lowering. +func (a *coroPhysicalPureSSAAudit) requireFrozenOutcomeRuntimeHelper(instr ssa.Instruction, helper string) string { + if a == nil { + return "outcome runtime helper validation requires a physical SSA audit" + } + if !a.allowImplicitNilFault { + return "potentially panicking runtime helper requires the explicit-status panic ABI" + } + if reason := a.requireFrozenCoroSafeRuntimeHelpers(instr, helper); reason != "" { + return reason + } + helpers, reason := a.plannedRuntimeHelpers(instr) + if reason != "" { + return reason + } + if len(helpers) != 1 || helpers[0] != helper { + return "operation does not lower through exactly one " + helper + " helper" + } + target, planned := a.plan.ResolveLoweredCall(a.fn, helper) + if !planned || target == nil { + return "outcome runtime helper " + helper + " lacks an exact lowered-call target" + } + call, planned := a.plan.ResolveLoweredCallRecord(a.fn, helper) + if !planned || call.RawPlain { + return "outcome runtime helper " + helper + " cannot use a raw/plain terminal island" + } + targetPlan, planned := a.plan.FunctionPlan(target) + if !planned || targetPlan.External != coro.Defined || targetPlan.Emission != coro.EmitCoroutine || + targetPlan.Primary != coro.PrimaryCoroutine || + (targetPlan.FuncRep != coro.DirectCoro && targetPlan.FuncRep != coro.Dispatch) || + !targetPlan.Demand.Contains(coro.AsyncDemand) || !targetPlan.Effect.Contains(coro.OutcomeStructured) || + !targetPlan.Exec.Contains(coro.MayUnwind) { + return "outcome runtime helper " + helper + " is not one demanded MayUnwind ExplicitStatus coroutine" + } + return "" +} + +// requireFrozenStructuredRuntimeHelpers is the reusable gate for composite +// language lowerings that issue more than one compiler-owned runtime call. +// It binds the exact helper inventory to owner-scoped lowered-call facts. A +// helper may remain plain only when it is proven non-suspending and +// non-unwinding; a MayUnwind helper must return through an ExplicitStatus +// coroutine child. Thus adding a map, iterator, assertion, or future typed +// lowering cannot silently recreate native-stack unwinding between awaits. +func (a *coroPhysicalPureSSAAudit) requireFrozenStructuredRuntimeHelpers(instr ssa.Instruction, expected ...string) string { + if a == nil || a.ctx == nil || a.universe == nil { + return "structured runtime helper validation requires a frozen emission universe" + } + helpers, reason := a.plannedRuntimeHelpers(instr) + if reason != "" { + return reason + } + return a.requireFrozenStructuredRuntimeHelperInventory(instr, helpers, expected...) +} + +// requireFrozenTypeAssertRuntimeHelpers corrects the logical helper scan with +// the physical callable transport selected by the frontend. The generic +// scanner sees a Go signature and conservatively reports MatchesClosure; raw C +// signatures lower as one direct pointer, so codegen emits no such helper. +func (a *coroPhysicalPureSSAAudit) requireFrozenTypeAssertRuntimeHelpers(assertion *ssa.TypeAssert, expected ...string) string { + var helpers []string + if a != nil && a.ctx != nil && a.universe != nil { + var reason string + helpers, reason = a.plannedRuntimeHelpers(assertion) + if reason != "" { + return reason + } + if !coroTypeAssertUsesManagedClosure(a.ctx, assertion) { + filtered := helpers[:0] + for _, helper := range helpers { + if helper != "MatchesClosure" { + filtered = append(filtered, helper) + } + } + helpers = filtered + } + } + if len(expected) == 0 && len(helpers) == 0 { + return "" + } + return a.requireFrozenStructuredRuntimeHelperInventory(assertion, helpers, expected...) +} + +func (a *coroPhysicalPureSSAAudit) requireFrozenStructuredRuntimeHelperInventory( + instr ssa.Instruction, + helpers []string, + expected ...string, +) string { + if a == nil || a.ctx == nil || a.universe == nil || a.plan == nil || a.fn == nil { + return "structured runtime helper validation requires a frozen emission universe" + } + want := make(map[string]struct{}, len(expected)) + for _, helper := range expected { + if helper == "" { + return "structured runtime helper inventory contains an empty helper name" + } + want[helper] = struct{}{} + } + if len(want) != len(expected) || len(helpers) != len(want) { + return fmt.Sprintf("structured runtime helper inventory = %v, want exactly %v", helpers, expected) + } + for _, helper := range helpers { + if _, ok := want[helper]; !ok { + return fmt.Sprintf("structured runtime helper inventory = %v, want exactly %v", helpers, expected) + } + } + + lowered := make(map[string]coro.SSALoweredCall) + for _, call := range a.plan.LoweredCalls(a.fn) { + lowered[call.LogicalName] = call + } + for _, helper := range helpers { + call, ok := lowered[helper] + if !ok || call.Target == nil || call.ExplicitStatusElided { + return "structured runtime helper " + helper + " lacks an exact non-elided lowered-call fact" + } + target, planned := a.plan.ResolveLoweredCall(a.fn, helper) + if !planned || target == nil || target != call.Target { + return "structured runtime helper " + helper + " lacks one consistent owner-scoped target" + } + plan, planned := a.plan.FunctionPlan(target) + if !planned || plan.External != coro.Defined || plan.Demand == coro.NoDemand { + return "structured runtime helper " + helper + " does not target one demanded defined body" + } + if call.RawPlain { + if !a.validRawPlainLoweredCall(call, plan) { + return "structured runtime helper " + helper + " has no validated raw/plain closure" + } + continue + } + switch plan.Emission { + case coro.EmitPlain: + if plan.Primary != coro.PrimaryPlain || plan.FuncRep != coro.DirectPlain || + plan.Effect != coro.NoSuspend || plan.Exec.Contains(coro.MayUnwind) || + plan.Exec&(coro.BlockForeign|coro.NeedsPreempt|coro.OpaqueExec) != 0 { + return "structured runtime helper " + helper + " is not one non-suspending, non-unwinding direct plain body" + } + case coro.EmitCoroutine: + if plan.Primary != coro.PrimaryCoroutine || + (plan.FuncRep != coro.DirectCoro && plan.FuncRep != coro.Dispatch) || + !plan.Demand.Contains(coro.AsyncDemand) || !plan.Effect.MaySuspend() { + return "structured runtime helper " + helper + " is not one demanded coroutine child" + } + if plan.Exec.Contains(coro.MayUnwind) && + (!a.allowImplicitNilFault || !plan.Effect.Contains(coro.OutcomeStructured)) { + return "structured runtime helper " + helper + " may unwind without the ExplicitStatus coroutine outcome ABI" + } + default: + return "structured runtime helper " + helper + " has no callable managed emission" + } + } + return "" +} + +// requireFrozenTerminalRuntimeHelpers accepts an exact compiler-lowered panic +// edge only when every emitted helper is present in allowed, is frozen as an +// exact lowered call, and has a demanded direct no-suspend plain body. The +// helper may return on the non-panic predicate, but it cannot suspend beneath +// the live coroutine frame. +func (a *coroPhysicalPureSSAAudit) requireFrozenTerminalRuntimeHelpers(instr ssa.Instruction, allowed ...string) string { + if a == nil || a.ctx == nil || a.universe == nil || a.plan == nil || a.fn == nil { + return "terminal runtime helper validation requires a frozen emission universe" + } + helpers, reason := a.plannedRuntimeHelpers(instr) + if reason != "" { + return reason + } + if len(helpers) == 0 { + return "terminal runtime helper validation found no lowered helper" + } + accepted := make(map[string]struct{}, len(allowed)) + for _, helper := range allowed { + accepted[helper] = struct{}{} + } + lowered := make(map[string]coro.SSALoweredCall) + for _, call := range a.plan.LoweredCalls(a.fn) { + lowered[call.LogicalName] = call + } + for _, helper := range helpers { + if _, ok := accepted[helper]; !ok { + return "operation lowers through unapproved terminal runtime helper " + helper + } + call, ok := lowered[helper] + if !ok || call.Target == nil { + return "terminal runtime helper " + helper + " lacks an exact lowered-call fact" + } + plan, ok := a.plan.FunctionPlan(call.Target) + if !ok || plan.External != coro.Defined || plan.Emission != coro.EmitPlain || + plan.Primary != coro.PrimaryPlain || plan.FuncRep != coro.DirectPlain || + plan.Effect != coro.NoSuspend || plan.Demand == coro.NoDemand || + plan.Exec&(coro.BlockForeign|coro.ThreadAffine|coro.NeedsPreempt|coro.OpaqueExec) != 0 { + return "terminal runtime helper " + helper + " is not one demanded direct no-suspend plain body" + } + } + return "" +} + +// requireNoRuntimeHelpersExcept permits only helpers whose panic predicate is +// made unreachable by the exact address-use dominance fact. It is not a +// general helper allowlist: without that exact proof even these names remain +// rejected, and every other lowered helper always remains rejected. +func (a *coroPhysicalPureSSAAudit) requireNoRuntimeHelpersExcept(instr ssa.Instruction, dominatedOnly ...string) string { + if a == nil || a.ctx == nil || a.universe == nil { + return "" + } + helpers, reason := a.plannedRuntimeHelpers(instr) + if reason != "" { + return reason + } + if len(helpers) == 0 { + return "" + } + if a.allHelpersHaveCoroSafeLowering(helpers) { + return "" + } + proof := a.currentFrameRetentionProof() + if proof != nil { + allowed := make(map[string]struct{}, len(dominatedOnly)) + for _, helper := range dominatedOnly { + allowed[helper] = struct{}{} + } + if len(allowed) != 0 { + var address ssa.Value + switch instruction := instr.(type) { + case *ssa.FieldAddr: + address = instruction + case *ssa.IndexAddr: + address = instruction + case *ssa.UnOp: + address = instruction.X + } + if address != nil && proof.provesDominatedStableAddress(address, instr) { + allDominated := true + for _, helper := range helpers { + if _, ok := allowed[helper]; !ok { + allDominated = false + break + } + } + if allDominated { + return "" + } + } + } + } + return "operation lowers through managed runtime helper(s) " + strings.Join(helpers, ", ") +} + +func (a *coroPhysicalPureSSAAudit) allHelpersHaveCoroSafeLowering(helpers []string) bool { + if a == nil || a.plan == nil || a.fn == nil || len(helpers) == 0 { + return false + } + lowered := make(map[string]coro.SSALoweredCall) + for _, call := range a.plan.LoweredCalls(a.fn) { + lowered[call.LogicalName] = call + } + for _, helper := range helpers { + call, ok := lowered[helper] + if !ok || call.Target == nil || call.ExplicitStatusElided { + return false + } + plan, ok := a.plan.FunctionPlan(call.Target) + if !ok || plan.External != coro.Defined || plan.Demand == coro.NoDemand { + return false + } + if call.RawPlain { + if !a.validRawPlainLoweredCall(call, plan) { + return false + } + continue + } + switch plan.Emission { + case coro.EmitPlain: + if plan.Primary != coro.PrimaryPlain || plan.FuncRep != coro.DirectPlain || + plan.Effect != coro.NoSuspend || plan.Exec&(coro.NeedsPreempt|coro.OpaqueExec) != 0 || + a.allowImplicitNilFault && plan.Exec.Contains(coro.MayUnwind) { + return false + } + case coro.EmitCoroutine: + if !a.allowImplicitNilFault || plan.Primary != coro.PrimaryCoroutine || + (plan.FuncRep != coro.DirectCoro && plan.FuncRep != coro.Dispatch) || + !plan.Demand.Contains(coro.AsyncDemand) || !plan.Effect.MaySuspend() { + return false + } + default: + return false + } + } + return true +} + +// validRawPlainLoweredCall verifies the plan half of a compiler-owned +// raw/plain occurrence. The live-closure validator has already proved every +// reachable Go/C leaf and marks both the callable entry and its exact raw body; +// aggregate managed Effect/Exec facts deliberately remain unchanged because a +// separate managed consumer may still need a coroutine entry. +func (a *coroPhysicalPureSSAAudit) validRawPlainLoweredCall(call coro.SSALoweredCall, plan coro.FunctionPlan) bool { + return a != nil && a.plan != nil && call.RawPlain && !call.UnwindOnly && !call.ExplicitStatusElided && + call.Target != nil && plan.External == coro.Defined && plan.RawPlainDemand && plan.RawPlainEntry && + a.plan.HasRawPlainVariant(call.Target) && + (plan.Emission == coro.EmitRawPlain || plan.Emission == coro.EmitPlain || plan.Emission == coro.EmitCoroutine) +} + +func (a *coroPhysicalPureSSAAudit) typeOf(typ types.Type) types.Type { + if typ == nil || a == nil || a.ctx == nil { + return typ + } + return a.ctx.patchType(typ) +} + +func validateCoroPhysicalSSAValueType(typ types.Type) error { + if typ == nil { + return fmt.Errorf("nil type") + } + if tuple, ok := types.Unalias(typ).Underlying().(*types.Tuple); ok { + for i := 0; i < tuple.Len(); i++ { + if err := validateCoroPhysicalValueType(tuple.At(i).Type(), make(map[types.Type]bool)); err != nil { + return fmt.Errorf("tuple field %d: %w", i, err) + } + } + return nil + } + return validateCoroPhysicalValueType(typ, make(map[types.Type]bool)) +} + +func coroConstantIndexInBounds(index ssa.Value, bound int64) bool { + if index == nil || bound < 0 { + return false + } + value, ok := index.(*ssa.Const) + if !ok || value.Value == nil { + return false + } + basic, ok := types.Unalias(value.Type()).Underlying().(*types.Basic) + if !ok || basic.Info()&types.IsInteger == 0 { + return false + } + if basic.Info()&types.IsUnsigned == 0 && constant.Sign(value.Value) < 0 { + return false + } + integer, exact := constant.Uint64Val(value.Value) + return exact && integer < uint64(bound) +} + +func coroPureBasicScalar(typ types.Type) bool { + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + if !ok { + return false + } + return basic.Info()&(types.IsBoolean|types.IsInteger|types.IsFloat) != 0 +} + +func coroPureConversion(source, target types.Type) bool { + if source == nil || target == nil { + return false + } + sourceUnderlying := types.Unalias(source).Underlying() + targetUnderlying := types.Unalias(target).Underlying() + if types.Identical(sourceUnderlying, targetUnderlying) { + return true + } + sourceBasic, sourceIsBasic := sourceUnderlying.(*types.Basic) + targetBasic, targetIsBasic := targetUnderlying.(*types.Basic) + if sourceIsBasic && targetIsBasic { + if sourceBasic.Kind() == types.String || targetBasic.Kind() == types.String { + return false + } + sourceNumeric := sourceBasic.Info()&(types.IsInteger|types.IsFloat|types.IsComplex) != 0 + targetNumeric := targetBasic.Info()&(types.IsInteger|types.IsFloat|types.IsComplex) != 0 + if sourceNumeric && targetNumeric { + return true + } + return (sourceBasic.Kind() == types.UnsafePointer && targetBasic.Kind() == types.Uintptr) || + (sourceBasic.Kind() == types.Uintptr && targetBasic.Kind() == types.UnsafePointer) + } + _, sourcePointer := sourceUnderlying.(*types.Pointer) + _, targetPointer := targetUnderlying.(*types.Pointer) + if sourcePointer && targetPointer { + return true + } + return (sourcePointer && targetIsBasic && targetBasic.Kind() == types.UnsafePointer) || + (targetPointer && sourceIsBasic && sourceBasic.Kind() == types.UnsafePointer) +} + +func coroTypeContainsGCPointer(typ types.Type, visiting map[types.Type]bool) bool { + if typ == nil { + return false + } + typ = types.Unalias(typ) + if visiting[typ] { + return false + } + visiting[typ] = true + defer delete(visiting, typ) + switch typ := typ.(type) { + case *types.Named: + return coroTypeContainsGCPointer(typ.Underlying(), visiting) + case *types.Pointer, *types.Map, *types.Chan, *types.Signature, *types.Interface, *types.Slice: + return true + case *types.Basic: + return typ.Kind() == types.String || typ.Kind() == types.UnsafePointer + case *types.Array: + return coroTypeContainsGCPointer(typ.Elem(), visiting) + case *types.Struct: + for i := 0; i < typ.NumFields(); i++ { + if coroTypeContainsGCPointer(typ.Field(i).Type(), visiting) { + return true + } + } + } + return false +} + +func coroTypeDefinitelyNonZero(typ types.Type, visiting map[types.Type]bool) bool { + if typ == nil { + return false + } + typ = types.Unalias(typ) + if visiting[typ] { + return false + } + visiting[typ] = true + defer delete(visiting, typ) + switch typ := typ.(type) { + case *types.Named: + return coroTypeDefinitelyNonZero(typ.Underlying(), visiting) + case *types.Basic, *types.Pointer, *types.Map, *types.Chan, *types.Signature, *types.Interface, *types.Slice: + return true + case *types.Array: + return typ.Len() > 0 && coroTypeDefinitelyNonZero(typ.Elem(), visiting) + case *types.Struct: + for i := 0; i < typ.NumFields(); i++ { + if coroTypeDefinitelyNonZero(typ.Field(i).Type(), visiting) { + return true + } + } + } + return false +} diff --git a/cl/coro_pure_ssa_test.go b/cl/coro_pure_ssa_test.go new file mode 100644 index 0000000000..50bc3d79e0 --- /dev/null +++ b/cl/coro_pure_ssa_test.go @@ -0,0 +1,831 @@ +//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 ( + "bytes" + "go/ast" + "go/token" + "go/types" + "regexp" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroPureSSAFixture = `package foo + +type Pair struct { + A uint32 + B [2]uint32 +} + +type Word uintptr +type NamedPointer *uint32 + +var Global Pair +var Backing [2]uint32 + +func Child(value uint32) uint32 { return value + 1 } +func PairValue() Pair { return Pair{A: 3} } +func ArrayValue() [2]uint32 { return [2]uint32{5, 7} } +func ScalarPair() (uint32, uint32) { return 11, 13 } + +func Aggregate() uint32 { + left, right := ScalarPair() + return PairValue().A + ArrayValue()[1] + left + right +} + +func Root(pointer *uint32) (Pair, any, []uint32, uintptr) { + var local Pair + var values [2]uint32 + local.A = 7 + values[1] = 9 + local.B = values + named := NamedPointer(pointer) + boxed := any(named) + view := Backing[:] + for step := uint32(0); step < 2; step++ { + local.A += step + } + Global = local + next := Child(local.A) + word := Word(next) + global := Global + return local, boxed, view, uintptr(word) + uintptr(global.A) +} +` + +func TestCoroPureSSAPhysicalABIV1NativeAndWasm(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, ssaPkg, files, universe, plan := prepareCoroPureSSATestPlan(t, test.target) + defer prog.Dispose() + assertCoroPureSSAInstructionCoverage(t, ssaPkg) + root := ssaPkg.Func("Root") + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.FuncRep != coro.DirectCoro || + rootPlan.Demand != coro.AsyncDemand || !rootPlan.Exec.Contains(coro.NeedsPreempt) || + !rootPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Root plan = %+v, present=%t; want preemptible child-await coroutine", rootPlan, ok) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify pure SSA coroutine before CoroSplit: %v\n%s", err, module.String()) + } + rootIR := requireCoroPhysicalFunction(t, module, "foo.Root").String() + aggregateIR := requireCoroPhysicalFunction(t, module, "foo.Aggregate").String() + for _, required := range []string{ + "alloca %foo.Pair", + "foo.Child$coro", + "call void @" + coroAwaitPrepareHookV1, + "call i1 @" + coroPreemptPollHookV1, + } { + if !strings.Contains(rootIR, required) { + t.Fatalf("Root pure SSA coroutine lacks %q:\n%s", required, rootIR) + } + } + if !regexp.MustCompile(`getelementptr inbounds(?: (?:nuw|nusw))* %foo\.Pair`).MatchString(rootIR) { + t.Fatalf("Root pure SSA coroutine lacks typed Pair field addressing:\n%s", rootIR) + } + for _, forbidden := range []string{ + "CheckIndexRange", "AssertNilDeref", "AllocU", "AllocZ", "NewSlice2", "NewSlice3Bounds", "NewItab", + } { + if strings.Contains(rootIR, forbidden) { + t.Fatalf("Root pure SSA lowering introduced hidden helper %q:\n%s", forbidden, rootIR) + } + if got := strings.Count(rootIR, "call void @"+coroYieldPrepareHookV1); got < 2 { + t.Fatalf("Root preemption handoffs = %d, want multiple block safepoints after aggregate/interface/slice construction:\n%s", got, rootIR) + } + } + if !strings.Contains(aggregateIR, "foo.PairValue$coro") || + !strings.Contains(aggregateIR, "foo.ArrayValue$coro") || !strings.Contains(aggregateIR, "extractvalue") { + t.Fatalf("Aggregate lost its fixed-array/field/multi-result lowering:\n%s", aggregateIR) + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() { + t.Fatalf("CoroSplit did not create Root resume entry:\n%s", module.String()) + } + resumeIR := resume.String() + for _, resultStore := range []*regexp.Regexp{ + regexp.MustCompile(`store %foo\.Pair `), + regexp.MustCompile(`store %"[^"]*\.eface" `), + regexp.MustCompile(`store %"[^"]*\.Slice" `), + } { + if !resultStore.MatchString(resumeIR) { + t.Fatalf("value live across await/preempt did not reach its typed result store (%s):\n%s", resultStore, resumeIR) + } + if aggregateResume := module.NamedFunction("foo.Aggregate$coro.resume"); aggregateResume.IsNil() { + t.Fatalf("CoroSplit did not create Aggregate resume entry:\n%s", module.String()) + } + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte("foo.Root$coro")) || + !bytes.Contains(object.Bytes(), []byte("foo.Aggregate$coro")) { + t.Fatal("post-CoroSplit object lost a pure SSA coroutine symbol") + } + }) + } +} + +func TestCoroStringRangeNormalizesUntypedConstantSource(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +func ConstantRange() int { + total := 0 + for _, value := range "abc" { + total += int(value) + } + return total +} +`) + function := ssaPkg.Func("ConstantRange") + var found *ssa.Range + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + if rng, ok := instruction.(*ssa.Range); ok { + found = rng + } + } + } + if found == nil { + t.Fatal("constant string range fixture has no Range instruction") + } + basic, ok := types.Unalias(found.X.Type()).Underlying().(*types.Basic) + if !ok || basic.Kind() != types.UntypedString { + t.Fatalf("constant Range source type = %v; want untyped string SSA input", found.X.Type()) + } + physical, accepted := coroPhysicalRangeStringType(found.X.Type()) + if !accepted || !types.Identical(physical, types.Typ[types.String]) { + t.Fatalf("constant Range physical type = %v, %t; want concrete string", physical, accepted) + } + if _, accepted := coroPhysicalRangeStringType(types.Typ[types.UntypedInt]); accepted { + t.Fatal("untyped integer was accepted as a string Range source") + } +} + +func TestCoroPureAggregateEqualityPhysicalABIV1NativeAndWasm(t *testing.T) { + llssa.Initialize(llssa.InitAll) + const source = `package foo +import "unsafe" +type Ticket struct { Epoch, Generation uint32 } +type Lease struct { ID [2]uintptr; Ticket Ticket } +type RunDecision struct { + G *byte + Ticket Ticket + Cases [2]uint32 + Outcome uint8 + Task uint8 + Lease Lease + Flag bool + Scale float32 + Number complex64 + Channel chan byte + Raw unsafe.Pointer + _ string +} +func Child(value uint32) uint32 { return value + 1 } +func Leaf(left, right RunDecision) bool { + _ = Child(left.Cases[0]) + return left != right +} +` + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, source) + var prog llssa.Program + if test.target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, test.target) + } + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + leaf, child := ssaPkg.Func("Leaf"), ssaPkg.Func("Child") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: leaf, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: 1, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == child { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + if fn == leaf { + return coro.SSAFunctionPolicy{Exec: coro.MayUnwind}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + leafPlan, ok := plan.FunctionPlan(leaf) + if !ok || leafPlan.Emission != coro.EmitCoroutine || leafPlan.Primary != coro.PrimaryCoroutine || + !leafPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("Leaf plan = %+v, present=%t; want PhysicalABIV1 structured child-await coroutine", leafPlan, ok) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify aggregate equality before CoroSplit: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foo.Leaf").String() + for _, required := range []string{"extractvalue", "icmp", "fcmp"} { + if !strings.Contains(body, required) { + t.Fatalf("RunDecision-like equality lacks recursive pure lowering %q:\n%s", required, body) + } + } + for _, forbidden := range []string{"StringEqual", "EfaceEqual", "IfaceType"} { + if strings.Contains(body, forbidden) { + t.Fatalf("RunDecision-like equality unexpectedly calls helper %q:\n%s", forbidden, body) + } + } + runCoroABITestPipeline(t, prog, module) + if resume := module.NamedFunction("foo.Leaf$coro.resume"); resume.IsNil() { + t.Fatalf("CoroSplit did not materialize aggregate equality resume entry:\n%s", module.String()) + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit aggregate equality object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 { + t.Fatal("aggregate equality emitted an empty object") + } + }) + } +} + +func TestCoroPureAggregateEqualityRejectsHelperBackedLeaves(t *testing.T) { + tests := []struct { + name string + source string + }{ + { + name: "string field", + source: `package foo +type Value struct { Count uint32; Text string } +func Root(left, right Value) bool { return left == right } +`, + }, + { + name: "nested string array", + source: `package foo +type Value struct { Text [2]string } +func Root(left, right Value) bool { return left != right } +`, + }, + { + name: "interface field", + source: `package foo +type Value struct { Payload any } +func Root(left, right Value) bool { return left == right } +`, + }, + { + name: "nested interface array", + source: `package foo +type Value struct { Payload [2]any } +func Root(left, right Value) bool { return left != right } +`, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + prog, _, _, root, audit, _ := prepareCoroFrameRootAudit(t, test.source, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + found := false + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + operation, ok := instruction.(*ssa.BinOp) + if !ok || (operation.Op != token.EQL && operation.Op != token.NEQ) { + continue + } + found = true + handled, reason := audit.validate(operation) + if !handled || !strings.Contains(reason, "aggregate equality contains a helper-backed or unsupported element") { + t.Fatalf("helper-backed aggregate equality validation = handled %t, reason %q", handled, reason) + } + } + } + if !found { + t.Fatal("helper-backed fixture has no aggregate equality") + } + }) + } +} + +func TestCoroPureSSAPreflightRemainsFailClosed(t *testing.T) { + for _, test := range []struct { + name string + source string + want string + }{ + { + name: "capturing closure", + source: `package foo +func Root(value uint32) func() uint32 { return func() uint32 { return value } } +`, + want: "heap allocation requires managed allocation", + }, + { + name: "dynamic call", + source: `package foo +func Root(callback func() uint32) uint32 { return callback() } +`, + want: "requires a compilation CallPlan", + }, + { + name: "possibly panicking slice index", + source: `package foo +func Root(values []uint32, index int) uint32 { return values[index] } +`, + want: "index base is not a fixed-array pointer", + }, + { + name: "allocating interface box", + source: `package foo +func Root(value uint64) any { return any(value) } +`, + want: "structured runtime helper validation requires a frozen emission universe", + }, + { + name: "heap allocation", + source: `package foo +func Root() *uint32 { value := uint32(1); return &value } +`, + want: "heap allocation requires managed allocation", + }, + { + name: "pointer global store without barrier", + source: `package foo +var Global *uint32 +func Root(value *uint32) { Global = value } +`, + want: "global typed store of a pointer-containing value requires explicit write-barrier lowering", + }, + } { + t.Run(test.name, func(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, test.source) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + root := ssaPkg.Func("Root") + plan := coro.FunctionPlan{ + ID: coro.FunctionID("foo.Root"), + External: coro.Defined, + Demand: coro.AsyncDemand, + ManagedDemand: coro.AsyncDemand, + Emission: coro.EmitCoroutine, + Primary: coro.PrimaryCoroutine, + FuncRep: coro.DirectCoro, + Effect: coro.YieldOnly, + } + err = validateCoroPhysicalABIWithUniverse(root, plan, nil, universe, true, true) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("preflight error = %v, want %q", err, test.want) + } + }) + } +} + +func TestCoroPureSSAChangeInterfaceUsesExactHelperInventory(t *testing.T) { + prog, _, universe, root, audit, _ := prepareCoroFrameRootAudit(t, `package foo +type Source interface { First(); Second() } +type Target interface { First() } +func Root(value Source) Target { return Target(value) } +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var change *ssa.ChangeInterface + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.ChangeInterface); ok { + change = candidate + } + } + } + if change == nil { + t.Fatal("fixture has no ChangeInterface") + } + if helpers := universe.loweredRuntimeHelpers(audit.ctx, change); strings.Join(helpers, ",") != "IfaceType,NewItab" { + t.Fatalf("non-empty interface conversion helpers = %v; want IfaceType, NewItab", helpers) + } + if handled, reason := audit.validate(change); !handled || !strings.Contains(reason, "structured runtime helper validation requires a frozen emission universe") { + t.Fatalf("non-empty interface conversion validation = handled %t, reason %q", handled, reason) + } +} + +func TestCoroPureSSATypeAssertUsesExactHelperInventory(t *testing.T) { + for _, test := range []struct { + name string + source string + wantHelpers string + wantReason string + }{ + { + name: "empty interface comma ok concrete", + source: `package foo +func Root(value any) (string, bool) { + result, ok := value.(string) + return result, ok +} +`, + }, + { + name: "empty interface single concrete", + source: `package foo +func Root(value any) string { return value.(string) } +`, + wantHelpers: "PanicTypeAssert", + wantReason: "structured runtime helper validation requires a frozen emission universe", + }, + { + name: "nonempty interface comma ok concrete", + source: `package foo +type Value string +func (Value) M() {} +type Source interface { M() } +func Root(value Source) (Value, bool) { + result, ok := value.(Value) + return result, ok +} +`, + wantHelpers: "IfaceType", + wantReason: "structured runtime helper validation requires a frozen emission universe", + }, + { + name: "nonempty interface comma ok interface", + source: `package foo +type Source interface { M() } +type Target interface { M(); N() } +func Root(value Source) (Target, bool) { + result, ok := value.(Target) + return result, ok +} +`, + wantHelpers: "IfaceType,Implements,NewItab", + wantReason: "structured runtime helper validation requires a frozen emission universe", + }, + } { + t.Run(test.name, func(t *testing.T) { + prog, _, universe, root, audit, _ := prepareCoroFrameRootAudit(t, test.source, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var assertion *ssa.TypeAssert + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.TypeAssert); ok { + assertion = candidate + } + } + } + if assertion == nil { + t.Fatal("fixture has no TypeAssert") + } + if got := strings.Join(universe.loweredRuntimeHelpers(audit.ctx, assertion), ","); got != test.wantHelpers { + t.Fatalf("type assertion helpers = %q; want %q", got, test.wantHelpers) + } + handled, reason := audit.validate(assertion) + if !handled || reason != test.wantReason { + t.Fatalf("type assertion validation = handled %t, reason %q; want reason %q", handled, reason, test.wantReason) + } + }) + } +} + +func TestCoroPureSSAStringConversionsUseExactHelperInventory(t *testing.T) { + const source = `package foo +func FromBytes(value []byte) string { return string(value) } +func FromRunes(value []rune) string { return string(value) } +func FromInt(value int) string { return string(value) } +func FromUint(value uint) string { return string(value) } +func ToBytes(value string) []byte { return []byte(value) } +func ToRunes(value string) []rune { return []rune(value) } +` + for function, helper := range map[string]string{ + "FromBytes": "StringFromBytes", + "FromRunes": "StringFromRunes", + "FromInt": "StringFromInt64", + "FromUint": "StringFromUint64", + "ToBytes": "StringToBytes", + "ToRunes": "StringToRunes", + } { + t.Run(function, func(t *testing.T) { + prog, _, universe, root, audit, _ := prepareCoroFrameRootAudit(t, source, function, EmissionUniverseOptions{}) + defer prog.Dispose() + var conversion *ssa.Convert + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.Convert); ok { + conversion = candidate + } + } + } + if conversion == nil { + t.Fatal("fixture has no Convert") + } + if got := strings.Join(universe.loweredRuntimeHelpers(audit.ctx, conversion), ","); got != helper { + t.Fatalf("conversion helpers = %q; want %q", got, helper) + } + if handled, reason := audit.validate(conversion); !handled || reason != "runtime helper capability validation requires a frozen emission universe" { + t.Fatalf("string conversion validation = handled %t, reason %q", handled, reason) + } + }) + } +} + +func TestCoroPureSSAStringComparisonsUseExactHelperInventory(t *testing.T) { + for _, test := range []struct { + name string + op string + helper string + }{ + {name: "equal", op: "==", helper: "StringEqual"}, + {name: "not-equal", op: "!=", helper: "StringEqual"}, + {name: "less", op: "<", helper: "StringLess"}, + {name: "less-equal", op: "<=", helper: "StringLess"}, + {name: "greater", op: ">", helper: "StringLess"}, + {name: "greater-equal", op: ">=", helper: "StringLess"}, + } { + t.Run(test.name, func(t *testing.T) { + source := "package foo\nfunc Root(left, right string) bool { return left " + test.op + " right }\n" + prog, _, universe, root, audit, _ := prepareCoroFrameRootAudit(t, source, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var comparison *ssa.BinOp + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.BinOp); ok { + comparison = candidate + } + } + } + if comparison == nil { + t.Fatal("fixture has no BinOp") + } + if got := strings.Join(universe.loweredRuntimeHelpers(audit.ctx, comparison), ","); got != test.helper { + t.Fatalf("comparison helpers = %q; want %q", got, test.helper) + } + if handled, reason := audit.validate(comparison); !handled || reason != "runtime helper capability validation requires a frozen emission universe" { + t.Fatalf("string comparison validation = handled %t, reason %q", handled, reason) + } + }) + } +} + +func TestCoroPureSSASignedShiftUsesExplicitStatusOutcome(t *testing.T) { + prog, _, universe, root, audit, _ := prepareCoroFrameRootAudit(t, `package foo +func Root(value uint64, count int) uint64 { return value >> count } +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var shift *ssa.BinOp + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.BinOp); ok && candidate.Op == token.SHR { + shift = candidate + } + } + } + if shift == nil { + t.Fatal("fixture has no signed-count shift") + } + if helpers := universe.loweredRuntimeHelpers(audit.ctx, shift); strings.Join(helpers, ",") != "AssertNegativeShift" { + t.Fatalf("signed shift helpers = %v; want AssertNegativeShift", helpers) + } + if handled, reason := audit.validate(shift); !handled || reason != "potentially panicking runtime helper requires the explicit-status panic ABI" { + t.Fatalf("signed shift without ExplicitStatus = handled %t, reason %q", handled, reason) + } + audit.allowImplicitNilFault = true + if handled, reason := audit.validate(shift); !handled || reason != "runtime helper capability validation requires a frozen emission universe" { + t.Fatalf("signed shift with ExplicitStatus = handled %t, reason %q", handled, reason) + } +} + +func TestCoroPureSSAGlobalPointerStoreRequiresExactNonMovingProfile(t *testing.T) { + prog, _, _, root, audit, _ := prepareCoroFrameRootAudit(t, `package foo +var Global *uint32 +func Root(value *uint32) { Global = value } +`, "Root", EmissionUniverseOptions{}) + defer prog.Dispose() + var store *ssa.Store + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.Store); ok { + store = candidate + } + } + } + if store == nil { + t.Fatal("fixture has no global pointer Store") + } + const want = "global typed store of a pointer-containing value requires explicit write-barrier lowering" + if reason := audit.validateStore(store); reason != want { + t.Fatalf("unprofiled global pointer store reason = %q; want %q", reason, want) + } + audit.frameRetentionABI = CoroFrameRetentionParkABIV2 + if reason := audit.validateStore(store); reason != "" { + t.Fatalf("non-moving profile global pointer store rejected: %s", reason) + } + + old := emitShadowStackInstrumentation + emitShadowStackInstrumentation = true + defer func() { emitShadowStackInstrumentation = old }() + if reason := audit.validateStore(store); reason != want { + t.Fatalf("precise/shadow profile global pointer store reason = %q; want %q", reason, want) + } +} + +func TestCoroPureSSANilComparisonsFollowGoSemantics(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, `package foo +import "unsafe" +func Interface(value error) bool { return value != nil } +func Slice(value []byte) bool { return value == nil } +func Unsafe(value unsafe.Pointer) bool { return value != nil } +`) + for _, name := range []string{"Interface", "Slice", "Unsafe"} { + fn := ssaPkg.Func(name) + audit := &coroPhysicalPureSSAAudit{fn: fn, reachableBlocks: coroPhysicalConstantReachableBlocks(fn)} + found := false + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + operation, ok := instruction.(*ssa.BinOp) + if !ok { + continue + } + found = true + if reason := audit.validateBinOp(operation); reason != "" { + t.Fatalf("%s nil comparison rejected: %s", name, reason) + } + } + } + if !found { + t.Fatalf("%s fixture has no binary nil comparison", name) + } + } +} + +func prepareCoroPureSSATestPlan(t *testing.T, target *llssa.Target) ( + llssa.Program, *ssa.Package, []*ast.File, *EmissionUniverse, *coro.SSAPlan, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroPureSSAFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + root, aggregate := ssaPkg.Func("Root"), ssaPkg.Func("Aggregate") + child := ssaPkg.Func("Child") + pairValue, arrayValue, scalarPair := ssaPkg.Func("PairValue"), ssaPkg.Func("ArrayValue"), ssaPkg.Func("ScalarPair") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: root, Demand: coro.AsyncDemand}, + {Function: aggregate, Demand: coro.AsyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: 1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == child || fn == pairValue || fn == arrayValue || fn == scalarPair { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, ssaPkg, files, universe, plan +} + +func assertCoroPureSSAInstructionCoverage(t *testing.T, pkg *ssa.Package) { + t.Helper() + seen := struct { + alloc, fieldAddr, indexAddr, index, slice, extract bool + field, makeInterface, store, load bool + changeType, convert bool + }{} + for _, name := range []string{"Root", "Aggregate"} { + fn := pkg.Func(name) + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + switch instruction := instruction.(type) { + case *ssa.Alloc: + seen.alloc = true + case *ssa.FieldAddr: + seen.fieldAddr = true + case *ssa.IndexAddr: + seen.indexAddr = true + case *ssa.Index: + seen.index = true + case *ssa.Slice: + seen.slice = true + case *ssa.Extract: + seen.extract = true + case *ssa.Field: + seen.field = true + case *ssa.MakeInterface: + seen.makeInterface = true + case *ssa.Store: + seen.store = true + case *ssa.UnOp: + seen.load = seen.load || instruction.Op.String() == "*" + case *ssa.ChangeType: + seen.changeType = true + case *ssa.Convert: + seen.convert = true + } + } + } + } + if !seen.alloc || !seen.fieldAddr || !seen.indexAddr || !seen.index || !seen.slice || !seen.extract || + !seen.field || !seen.makeInterface || !seen.store || !seen.load || !seen.changeType || !seen.convert { + t.Fatalf("pure SSA fixture did not materialize every audited instruction class: %+v", seen) + } +} diff --git a/cl/coro_raw_c_adapter.go b/cl/coro_raw_c_adapter.go new file mode 100644 index 0000000000..56e6e40423 --- /dev/null +++ b/cl/coro_raw_c_adapter.go @@ -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 ( + "fmt" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +type coroRawCChangeTypePlan struct { + target *ssa.Function + resultType types.Type + rawRetag bool +} + +// resolveCoroRawCChangeType freezes the only implicit cross-transport adapter +// currently implemented by LLGo: an exact, context-free Go function may be +// published as one //llgo:type C code pointer when the whole-program plan has +// independently selected and validated its raw/plain entry. This proof is +// occurrence-local. It neither changes another use of the Go function nor +// permits a dynamic Managed<->RawC reinterpretation. +func resolveCoroRawCChangeType( + plan *coro.SSAPlan, + universe *EmissionUniverse, + owner *ssa.Function, + change *ssa.ChangeType, +) (coroRawCChangeTypePlan, bool, error) { + if plan == nil || universe == nil || owner == nil || change == nil || change.X == nil { + return coroRawCChangeTypePlan{}, false, nil + } + sourceType := coroCallableEffectiveType(universe, owner, change.X.Type()) + resultType := coroCallableEffectiveType(universe, owner, change.Type()) + sourceTransport, err := coroCallableLeafTransport(universe, sourceType) + if err != nil { + return coroRawCChangeTypePlan{}, false, fmt.Errorf("source transport: %w", err) + } + resultTransport, err := coroCallableLeafTransport(universe, resultType) + if err != nil { + return coroRawCChangeTypePlan{}, false, fmt.Errorf("result transport: %w", err) + } + if sourceTransport == coro.ManagedTransport && resultTransport == coro.ManagedTransport { + return coroRawCChangeTypePlan{}, false, nil + } + fail := func(format string, args ...any) (coroRawCChangeTypePlan, bool, error) { + return coroRawCChangeTypePlan{}, true, fmt.Errorf( + "coroutine raw C function adapter in %q: %s", owner.Name(), fmt.Sprintf(format, args...), + ) + } + if sourceTransport == coro.RawCCodePointer && resultTransport == coro.ManagedTransport { + return fail("RawC-to-Managed ChangeType has no descriptor construction recipe") + } + + sourcePlan, sourceFound := plan.ValuePlan(change.X) + resultPlan, resultFound := plan.ValuePlan(change) + if !sourceFound || sourcePlan.Value != change.X || len(sourcePlan.Funcs) != 1 || len(sourcePlan.Funcs[0].Path) != 0 { + return fail("source %q has no exact scalar ValuePlan", change.X.Name()) + } + if !resultFound || resultPlan.Value != change || len(resultPlan.Funcs) != 1 || len(resultPlan.Funcs[0].Path) != 0 { + return fail("result %q has no exact scalar ValuePlan", change.Name()) + } + sourceLeaf, resultLeaf := sourcePlan.Funcs[0], resultPlan.Funcs[0] + if sourceLeaf.Transport != sourceTransport || resultLeaf.Transport != resultTransport { + return fail( + "frozen ValuePlan transport disagrees with frontend metadata (source=%s/%s result=%s/%s)", + sourceLeaf.Transport, sourceTransport, resultLeaf.Transport, resultTransport, + ) + } + if resultLeaf.Transport != coro.RawCCodePointer || resultLeaf.Rep != coro.DirectPlain { + return fail("raw result requires RawCCodePointer/DirectPlain, got %s/%s", resultLeaf.Transport, resultLeaf.Rep) + } + if sourceLeaf.Transport == coro.RawCCodePointer { + if sourceLeaf.Rep != coro.DirectPlain || sourceLeaf.MayBeNil != resultLeaf.MayBeNil || + !equalCoroFunctionTargets(sourceLeaf.Targets, resultLeaf.Targets) { + return fail("RawC retag changes representation, nilability, or targets") + } + return coroRawCChangeTypePlan{resultType: resultType, rawRetag: true}, true, nil + } + if sourceLeaf.Transport != coro.ManagedTransport || + (sourceLeaf.Rep != coro.DirectPlain && sourceLeaf.Rep != coro.DirectCoro) { + return fail("Go-to-RawC source requires an exact managed direct entry, got %s/%s", sourceLeaf.Transport, sourceLeaf.Rep) + } + if sourceLeaf.MayBeNil || resultLeaf.MayBeNil || len(sourceLeaf.Targets) != 1 || + len(resultLeaf.Targets) != 1 || sourceLeaf.Targets[0] != resultLeaf.Targets[0] { + return fail("Go-to-RawC adapter requires one identical, statically non-nil target") + } + target, found := plan.Function(resultLeaf.Targets[0]) + if !found || target == nil { + return fail("target %q is absent from the compilation plan", resultLeaf.Targets[0]) + } + static, exact := change.X.(*ssa.Function) + if !exact || static == nil || len(static.FreeVars) != 0 { + return fail("source is not one exact non-capturing SSA function") + } + canonical, resolved := universe.Resolve(static) + if !resolved || canonical == nil || canonical != target { + return fail("static source %q does not resolve to frozen target %q", static.Name(), resultLeaf.Targets[0]) + } + targetPlan, planned := plan.FunctionPlan(target) + if !planned || targetPlan.ID != resultLeaf.Targets[0] { + return fail("target %q has no canonical FunctionPlan", resultLeaf.Targets[0]) + } + if !plan.HasRawPlainVariant(target) { + return fail("target %q has no frozen raw/plain variant", targetPlan.ID) + } + if err := validatePlannedRawPlainEntry(target, targetPlan); err != nil { + return fail("target has no public raw/plain entry: %v", err) + } + if !types.Identical(types.Unalias(sourceType).Underlying(), types.Unalias(resultType).Underlying()) { + return fail("source and result signatures are not identical") + } + return coroRawCChangeTypePlan{target: target, resultType: resultType}, true, nil +} + +func equalCoroFunctionTargets(left, right []coro.FunctionID) bool { + if len(left) != len(right) { + return false + } + for index := range left { + if left[index] != right[index] { + return false + } + } + return true +} + +func validateCoroRawCFunctionAdapters(plan *coro.SSAPlan, universe *EmissionUniverse) error { + if plan == nil || universe == nil { + return fmt.Errorf("coroutine raw C function adapters require a plan and emission universe") + } + for _, function := range plan.Functions() { + if function.Function == nil || function.Plan.Emission == coro.EmitNone { + continue + } + for _, block := range function.Function.Blocks { + for _, instruction := range block.Instrs { + change, ok := instruction.(*ssa.ChangeType) + if !ok { + continue + } + if _, _, err := resolveCoroRawCChangeType(plan, universe, function.Function, change); err != nil { + return fmt.Errorf("%s: %w", change.String(), err) + } + } + } + } + return nil +} + +func (p *context) tryCompileCoroRawCChangeType(b llssa.Builder, change *ssa.ChangeType) (llssa.Expr, bool) { + if p == nil || p.compilation == nil || + p.compilation.CoroPlan == nil || p.compilation.EmissionUniverse == nil || p.goFn == nil { + return llssa.Expr{}, false + } + adapter, recognized, err := resolveCoroRawCChangeType( + p.compilation.CoroPlan, p.compilation.EmissionUniverse, p.goFn, change, + ) + if err != nil { + panic(err) + } + if !recognized { + return llssa.Expr{}, false + } + targetType := p.prog.Type(adapter.resultType, llssa.InC) + if adapter.rawRetag { + return b.ChangeType(targetType, p.compileValue(b, change.X)), true + } + function, py, kind := p.compileRawPlainFunction(adapter.target) + if kind != goFunc || function == nil || py != nil { + panic(fmt.Errorf("coroutine raw C function adapter target %q did not compile as one raw Go entry", adapter.target.Name())) + } + return b.ChangeType(targetType, function.Expr), true +} diff --git a/cl/coro_raw_c_adapter_test.go b/cl/coro_raw_c_adapter_test.go new file mode 100644 index 0000000000..aa79b8ba0f --- /dev/null +++ b/cl/coro_raw_c_adapter_test.go @@ -0,0 +1,266 @@ +//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" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const coroRawCAdapterFixtureSource = `package adapter + +//llgo:type C +type RawCallback func(int) int + +func sink(RawCallback) {} + +func target(value int) int { return value + 1 } + +func RawOnly() { + sink(RawCallback(target)) +} + +func Mixed(value int) int { + sink(RawCallback(target)) + return target(value) +} + +func DynamicToRaw(fn func(int) int) RawCallback { + return RawCallback(fn) +} + +func RawToManaged(fn RawCallback) func(int) int { + return (func(int) int)(fn) +} +` + +type coroRawCAdapterFixture struct { + prog llssa.Program + pkg *ssa.Package + universe *EmissionUniverse +} + +func prepareCoroRawCAdapterFixture(t *testing.T) coroRawCAdapterFixture { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroRawCAdapterFixtureSource) + prog := newLLSSAProg(t) + ParsePkgSyntax(prog, ssaPkg.Pkg, files) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return coroRawCAdapterFixture{prog: prog, pkg: ssaPkg, universe: universe} +} + +func (fixture coroRawCAdapterFixture) analyze( + t *testing.T, + root *ssa.Function, + rawCallbackOwner *ssa.Function, +) (*coro.SSAPlan, error) { + t.Helper() + ssaUniverse, err := coro.NewSSAEmissionUniverse(fixture.pkg.Prog, fixture.universe.Functions()) + if err != nil { + return nil, err + } + functionIDs := fixture.universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + target := fixture.pkg.Func("target") + sink := fixture.pkg.Func("sink") + return coro.AnalyzeSSA(fixture.pkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyRawCFunctionType: func(typ types.Type) (bool, error) { + _, signature := types.Unalias(typ).Underlying().(*types.Signature) + return signature && fixture.prog.TypeBackground(typ) == llssa.InC, nil + }, + ClassifyRawDirectPlainCallArgument: func(owner *ssa.Function, call ssa.CallInstruction, argument int) (bool, error) { + return rawCallbackOwner != nil && owner == rawCallbackOwner && call.Common() != nil && + call.Common().StaticCallee() == sink && argument == 0, nil + }, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == target { + // Force a distinct managed coroutine primary in the mixed-demand + // test. Raw-only demand still emits just its validated legacy body. + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) +} + +func TestCoroRawCAdapterResolvesRawOnlyExactStaticTarget(t *testing.T) { + fixture := prepareCoroRawCAdapterFixture(t) + defer fixture.prog.Dispose() + + owner := fixture.pkg.Func("RawOnly") + plan, err := fixture.analyze(t, owner, owner) + if err != nil { + t.Fatal(err) + } + target := fixture.pkg.Func("target") + targetPlan, found := plan.FunctionPlan(target) + if !found || !targetPlan.RawPlainOnly || targetPlan.ManagedDemand != coro.NoDemand || + !targetPlan.RawPlainDemand || !targetPlan.RawPlainEntry || targetPlan.Emission != coro.EmitRawPlain || + !plan.HasRawPlainVariant(target) { + t.Fatalf("raw-only target plan = %+v, present=%t variant=%t", targetPlan, found, plan.HasRawPlainVariant(target)) + } + + change := coroRawCAdapterChangeType(t, owner) + adapter, recognized, err := resolveCoroRawCChangeType(plan, fixture.universe, owner, change) + if err != nil { + t.Fatal(err) + } + if !recognized || adapter.target != target || adapter.rawRetag || adapter.resultType == nil { + t.Fatalf("raw-only adapter = %+v, recognized=%t; want exact target raw entry", adapter, recognized) + } + coroAssertRawCAdapterValuePlans(t, plan, change, targetPlan.ID, coro.DirectPlain) +} + +func TestCoroRawCAdapterSelectionIsOccurrenceSpecific(t *testing.T) { + fixture := prepareCoroRawCAdapterFixture(t) + defer fixture.prog.Dispose() + + owner := fixture.pkg.Func("Mixed") + plan, err := fixture.analyze(t, owner, owner) + if err != nil { + t.Fatal(err) + } + target := fixture.pkg.Func("target") + targetPlan, found := plan.FunctionPlan(target) + if !found || targetPlan.RawPlainOnly || targetPlan.ManagedDemand == coro.NoDemand || + !targetPlan.RawPlainDemand || !targetPlan.RawPlainEntry || targetPlan.Emission != coro.EmitCoroutine || + targetPlan.Primary != coro.PrimaryCoroutine || !plan.HasRawPlainVariant(target) { + t.Fatalf("mixed target plan = %+v, present=%t variant=%t", targetPlan, found, plan.HasRawPlainVariant(target)) + } + + change := coroRawCAdapterChangeType(t, owner) + adapter, recognized, err := resolveCoroRawCChangeType(plan, fixture.universe, owner, change) + if err != nil { + t.Fatal(err) + } + if !recognized || adapter.target != target || adapter.rawRetag { + t.Fatalf("mixed raw occurrence adapter = %+v, recognized=%t", adapter, recognized) + } + coroAssertRawCAdapterValuePlans(t, plan, change, targetPlan.ID, coro.DirectCoro) + + managedCall := coroRawCAdapterStaticCall(t, owner, target) + callPlan, found := plan.CallPlan(managedCall) + if !found || callPlan.Transport != coro.ManagedTransport || callPlan.Rep != coro.DirectCoro || + len(callPlan.Targets) != 1 || callPlan.Targets[0] != targetPlan.ID { + t.Fatalf("unrelated managed call plan = %+v, present=%t; want managed coroutine entry", callPlan, found) + } +} + +func TestCoroRawCAdapterDynamicCrossingsFailClosed(t *testing.T) { + fixture := prepareCoroRawCAdapterFixture(t) + defer fixture.prog.Dispose() + + t.Run("ManagedToRawC", func(t *testing.T) { + owner := fixture.pkg.Func("DynamicToRaw") + plan, err := fixture.analyze(t, owner, nil) + if err != nil { + t.Fatal(err) + } + change := coroRawCAdapterChangeType(t, owner) + _, recognized, err := resolveCoroRawCChangeType(plan, fixture.universe, owner, change) + if !recognized || err == nil || + !strings.Contains(err.Error(), "Go-to-RawC source requires an exact managed direct entry, got managed/dispatch") { + t.Fatalf("dynamic Managed-to-RawC adapter = recognized %t, error %v", recognized, err) + } + }) + + t.Run("RawCToManaged", func(t *testing.T) { + owner := fixture.pkg.Func("RawToManaged") + plan, err := fixture.analyze(t, owner, nil) + if err != nil { + t.Fatal(err) + } + change := coroRawCAdapterChangeType(t, owner) + _, recognized, err := resolveCoroRawCChangeType(plan, fixture.universe, owner, change) + if !recognized || err == nil || !strings.Contains(err.Error(), "RawC-to-Managed ChangeType has no descriptor construction recipe") { + t.Fatalf("RawC-to-Managed adapter = recognized %t, error %v", recognized, err) + } + }) +} + +func coroRawCAdapterChangeType(t *testing.T, owner *ssa.Function) *ssa.ChangeType { + t.Helper() + var found *ssa.ChangeType + for _, block := range owner.Blocks { + for _, instruction := range block.Instrs { + change, ok := instruction.(*ssa.ChangeType) + if !ok { + continue + } + if found != nil { + t.Fatalf("function %s has multiple ChangeType instructions", owner) + } + found = change + } + } + if found == nil { + t.Fatalf("function %s has no ChangeType instruction", owner) + } + return found +} + +func coroRawCAdapterStaticCall(t *testing.T, owner, target *ssa.Function) *ssa.Call { + t.Helper() + for _, block := range owner.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Common() != nil && call.Common().StaticCallee() == target { + return call + } + } + } + t.Fatalf("function %s has no static call to %s", owner, target) + return nil +} + +func coroAssertRawCAdapterValuePlans( + t *testing.T, + plan *coro.SSAPlan, + change *ssa.ChangeType, + target coro.FunctionID, + wantSourceRep coro.FuncRep, +) { + t.Helper() + source, sourceFound := plan.ValuePlan(change.X) + result, resultFound := plan.ValuePlan(change) + if !sourceFound || len(source.Funcs) != 1 || source.Funcs[0].Transport != coro.ManagedTransport || + source.Funcs[0].Rep != wantSourceRep || source.Funcs[0].MayBeNil || + len(source.Funcs[0].Targets) != 1 || source.Funcs[0].Targets[0] != target { + t.Fatalf("raw adapter source plan = %+v, present=%t", source, sourceFound) + } + if !resultFound || len(result.Funcs) != 1 || result.Funcs[0].Transport != coro.RawCCodePointer || + result.Funcs[0].Rep != coro.DirectPlain || result.Funcs[0].MayBeNil || + len(result.Funcs[0].Targets) != 1 || result.Funcs[0].Targets[0] != target { + t.Fatalf("raw adapter result plan = %+v, present=%t", result, resultFound) + } +} diff --git a/cl/coro_raw_plain_entry_test.go b/cl/coro_raw_plain_entry_test.go new file mode 100644 index 0000000000..08e41367f5 --- /dev/null +++ b/cl/coro_raw_plain_entry_test.go @@ -0,0 +1,751 @@ +//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/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +func TestCoroRawPlainEntryDualLoweringKeepsManagedCallsManaged(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func RawHelper(value uint32) uint32 { + for value != 0 { value-- } + return value +} + +func Dual(value uint32) uint32 { return RawHelper(value) } +func Parent(value uint32) uint32 { return Dual(value) } +`) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + parent, dual, helper := ssaPkg.Func("Parent"), ssaPkg.Func("Dual"), ssaPkg.Func("RawHelper") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: parent, Demand: coro.AsyncDemand}, + {Function: dual, Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == dual { + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + } + if fn == helper { + return coro.SSAFunctionPolicy{RawPlainVariant: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + dualPlan, ok := plan.FunctionPlan(dual) + if !ok || !dualPlan.RawPlainEntry || !plan.HasRawPlainVariant(dual) || dualPlan.Emission != coro.EmitCoroutine || dualPlan.Primary != coro.PrimaryCoroutine { + prog.Dispose() + t.Fatalf("Dual plan = %+v, present=%t; want managed coroutine plus physical raw plain entry", dualPlan, ok) + } + helperPlan, ok := plan.FunctionPlan(helper) + if !ok || helperPlan.RawPlainEntry || !plan.HasRawPlainVariant(helper) || helperPlan.Emission != coro.EmitCoroutine || helperPlan.Primary != coro.PrimaryCoroutine { + prog.Dispose() + t.Fatalf("RawHelper plan = %+v, present=%t; want managed coroutine plus internal raw plain variant", helperPlan, ok) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify dual raw/managed module: %v\n%s", err, module.String()) + } + + for _, name := range []string{"foo.Dual", "foo.Dual$coro", "foo.RawHelper", "foo.RawHelper$coro"} { + if module.NamedFunction(name).IsNil() { + t.Fatalf("dual lowering is missing %q:\n%s", name, module.String()) + } + } + rawDual := module.NamedFunction("foo.Dual").String() + if !strings.Contains(rawDual, "@foo.RawHelper(") || strings.Contains(rawDual, "RawHelper$coro") { + t.Fatalf("raw Dual did not call the raw/plain helper variant:\n%s", rawDual) + } + managedDual := module.NamedFunction("foo.Dual$coro").String() + if !strings.Contains(managedDual, "RawHelper$coro") { + t.Fatalf("managed Dual did not await the managed helper entry:\n%s", managedDual) + } + managedParent := module.NamedFunction("foo.Parent$coro").String() + if !strings.Contains(managedParent, "Dual$coro") || strings.Contains(managedParent, "@foo.Dual(") { + t.Fatalf("ordinary managed Parent selected the raw Dual entry:\n%s", managedParent) + } +} + +func TestCoroRawPlainOnlyEmitsOneLegacyBody(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func RawHelper(value uint32) uint32 { + for value != 0 { value-- } + return value +} +func Host(value uint32) uint32 { return RawHelper(value) } +`) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + host, helper := ssaPkg.Func("Host"), ssaPkg.Func("RawHelper") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{ + Function: host, RawPlainDemand: true, + }}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch fn { + case host: + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + case helper: + return coro.SSAFunctionPolicy{RawPlainVariant: true}, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + for _, fn := range []*ssa.Function{host, helper} { + got, ok := plan.FunctionPlan(fn) + if !ok || !got.RawPlainOnly || got.ManagedDemand != coro.NoDemand || !got.RawPlainDemand || + got.Emission != coro.EmitRawPlain || got.Primary != coro.PrimaryPlain || + got.FuncRep != coro.DirectPlain || !plan.HasRawPlainVariant(fn) { + prog.Dispose() + t.Fatalf("%s raw-only plan = %+v, present=%t variant=%t", fn.Name(), got, ok, plan.HasRawPlainVariant(fn)) + } + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify raw-only module: %v\n%s", err, module.String()) + } + for _, name := range []string{"foo.Host", "foo.RawHelper"} { + if module.NamedFunction(name).IsNil() { + t.Fatalf("raw-only lowering is missing base %q:\n%s", name, module.String()) + } + if !module.NamedFunction(name + coroPrimarySuffix).IsNil() { + t.Fatalf("raw-only lowering emitted managed twin %q:\n%s", name+coroPrimarySuffix, module.String()) + } + } + hostBody := module.NamedFunction("foo.Host").String() + if !strings.Contains(hostBody, "@foo.RawHelper(") || strings.Contains(hostBody, "RawHelper$coro") { + t.Fatalf("raw-only Host did not call the helper base:\n%s", hostBody) + } + if strings.Contains(module.String(), "llvm.coro.") || strings.Contains(module.String(), coroRootFactoryPrefix) { + t.Fatalf("raw-only module contains coroutine machinery:\n%s", module.String()) + } +} + +func TestCoroRawPlainOnlyCompilesClosedSingletonSyncDispatch(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func Target(value int) int { return value + 1 } +func Host(fn func(int) int, value int) int { + if fn == nil { return 0 } + return fn(value) +} +`) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + host, target := ssaPkg.Func("Host"), ssaPkg.Func("Target") + dynamicCall := coroPlainDispatchOnlyDynamicCall(t, host) + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{ + Function: host, RawPlainDemand: true, + }}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == host { + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyClosedDynamicCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.SSAClosedDynamicCallCertificate, bool, error) { + if call != dynamicCall { + return coro.SSAClosedDynamicCallCertificate{}, false, nil + } + return coro.SSAClosedDynamicCallCertificate{ + Targets: []*ssa.Function{target}, MayBeNil: true, SyncDispatch: true, + }, true, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + hostPlan, ok := plan.FunctionPlan(host) + if !ok || hostPlan.Emission != coro.EmitRawPlain || !hostPlan.RawPlainOnly || + hostPlan.ManagedDemand != coro.NoDemand || !hostPlan.RawPlainDemand || !plan.HasRawPlainVariant(host) { + prog.Dispose() + t.Fatalf("Host plan = %+v, present=%t variant=%t; want final raw-only body", hostPlan, ok, plan.HasRawPlainVariant(host)) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.Emission != coro.EmitPlain || targetPlan.Effect != coro.NoSuspend || + targetPlan.FuncRep != coro.Dispatch || targetPlan.ManagedDemand != coro.SyncDemand || targetPlan.RawPlainDemand { + prog.Dispose() + t.Fatalf("Target plan = %+v, present=%t; want managed-sync plain descriptor", targetPlan, ok) + } + callPlan, ok := plan.CallPlan(dynamicCall) + if !ok || !callPlan.SyncDispatch || callPlan.Open || callPlan.Rep != coro.Dispatch || + !callPlan.MayBeNil || len(callPlan.Targets) != 1 || callPlan.Targets[0] != targetPlan.ID { + prog.Dispose() + t.Fatalf("Host SyncDispatch plan = %+v, present=%t", callPlan, ok) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.FuncRepABI = coro.FuncRepABIV1 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatalf("compile raw-only SyncDispatch package: %v", err) + } + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify raw-only SyncDispatch module: %v\n%s", err, module.String()) + } + hostIR := module.NamedFunction("foo.Host") + if hostIR.IsNil() || !module.NamedFunction("foo.Host"+coroPrimarySuffix).IsNil() { + t.Fatalf("raw-only SyncDispatch did not emit exactly the base Host body:\n%s", module.String()) + } + if body := hostIR.String(); !strings.Contains(body, "coro.dispatch") || !strings.Contains(body, "llvm.trap") { + t.Fatalf("raw-only Host did not lower its certified nullable descriptor call:\n%s", body) + } + if targetIR := module.NamedFunction("foo.Target"); targetIR.IsNil() || !module.NamedFunction("foo.Target"+coroPrimarySuffix).IsNil() { + t.Fatalf("SyncDispatch target did not retain one plain primary:\n%s", module.String()) + } + if strings.Contains(module.String(), "llvm.coro.") || strings.Contains(module.String(), coroRootFactoryPrefix) { + t.Fatalf("raw-only SyncDispatch module contains coroutine machinery:\n%s", module.String()) + } +} + +func TestCoroExactManagedGoLinknameAliasNeedsNoRawPlainEntry(t *testing.T) { + testProg := newEmissionTestProgram() + declarationPkg := testProg.addPackage(t, "example.com/coro/linkdecl", `package linkdecl +func runtimeHook(value uint32) uint32 +func Root(value uint32) uint32 { return runtimeHook(value) } +`) + definitionPkg := testProg.addPackage(t, "example.com/coro/linkdef", `package linkdef +//go:linkname implementation example.com/coro/linkdecl.runtimeHook +func implementation(value uint32) uint32 { return value + 1 } +`) + testProg.ssa.Build() + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{ + {SSA: declarationPkg.ssa, Files: []*ast.File{declarationPkg.file}}, + {SSA: definitionPkg.ssa, Files: []*ast.File{definitionPkg.file}}, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + declaration := declarationPkg.ssa.Func("runtimeHook") + implementation := definitionPkg.ssa.Func("implementation") + if resolved, ok := universe.Resolve(declaration); !ok || resolved != implementation { + prog.Dispose() + t.Fatalf("runtimeHook resolution = %v, %t; want exact implementation %v", resolved, ok, implementation) + } + managed, err := universe.exactManagedGoLinknameDefinition(implementation) + if err != nil || !managed { + prog.Dispose() + t.Fatalf("managed go:linkname proof = %t, %v; want true, nil", managed, err) + } + + ssaUniverse, err := coro.NewSSAEmissionUniverse(testProg.ssa, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := declarationPkg.ssa.Func("Root") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(testProg.ssa, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ResolveFunction: func(fn *ssa.Function) (*ssa.Function, bool, error) { + canonical, ok := universe.Resolve(fn) + return canonical, ok, nil + }, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == implementation { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + implementationPlan, ok := plan.FunctionPlan(implementation) + if !ok || implementationPlan.Emission != coro.EmitCoroutine || implementationPlan.Primary != coro.PrimaryCoroutine || + implementationPlan.RawPlainEntry || plan.HasRawPlainVariant(implementation) { + prog.Dispose() + t.Fatalf("implementation plan = %+v, present=%t raw-variant=%t; want one managed coroutine primary", + implementationPlan, ok, plan.HasRawPlainVariant(implementation)) + } + // A dynamically transported reference to the same canonical body publishes + // a descriptor for the managed primary. The exact declaration/definition + // alias remains a managed Go symbol; only validation without the frozen + // universe must continue to treat the redirecting directive as a raw edge. + dispatchPlan := implementationPlan + dispatchPlan.FuncRep = coro.Dispatch + if err := validateCoroDynamicDispatchTarget(implementation, dispatchPlan); err == nil || + !strings.Contains(err.Error(), "ABI directive") { + prog.Dispose() + t.Fatalf("unfrozen managed-linkname descriptor validation = %v; want fail-closed directive rejection", err) + } + if err := validateCoroDynamicDispatchTarget(implementation, dispatchPlan, universe); err != nil { + prog.Dispose() + t.Fatalf("frozen managed-linkname descriptor validation: %v", err) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + definitionLL, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, definitionPkg.ssa, []*ast.File{definitionPkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + declarationLL, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, declarationPkg.ssa, []*ast.File{declarationPkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + definitionLL.Module().Dispose() + prog.Dispose() + t.Fatal(err) + } + defer prog.Dispose() + definitionModule := definitionLL.Module() + declarationModule := declarationLL.Module() + defer definitionModule.Dispose() + defer declarationModule.Dispose() + for name, module := range map[string]llvm.Module{"definition": definitionModule, "declaration": declarationModule} { + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed go:linkname %s module: %v\n%s", name, err, module.String()) + } + } + const baseName = "example.com/coro/linkdecl.runtimeHook" + if raw := definitionModule.NamedFunction(baseName); !raw.IsNil() { + t.Fatalf("managed go:linkname unexpectedly emitted a raw/plain body:\n%s", raw.String()) + } + managedEntry := definitionModule.NamedFunction(baseName + coroPrimarySuffix) + if managedEntry.IsNil() { + t.Fatalf("managed go:linkname coroutine primary is absent:\n%s", definitionModule.String()) + } + declarationEntry := declarationModule.NamedFunction(baseName + coroPrimarySuffix) + if declarationEntry.IsNil() || !declarationEntry.FirstBasicBlock().IsNil() { + t.Fatalf("bodyless go:linkname declaration archive did not retain a declaration-only canonical coroutine entry:\n%s", declarationModule.String()) + } + rootBody := declarationModule.NamedFunction("example.com/coro/linkdecl.Root" + coroPrimarySuffix).String() + if !strings.Contains(rootBody, "runtimeHook$coro") || strings.Contains(rootBody, "runtimeHook\"(") { + t.Fatalf("managed root did not select the canonical coroutine alias:\n%s", rootBody) + } +} + +func TestCoroUnpairedGoLinknameDefinitionRemainsRawBoundary(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +import _ "unsafe" + +//go:linkname Unpaired example.com/external.runtimeHook +func Unpaired(value uint32) uint32 { return value + 1 } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + fn := ssaPkg.Func("Unpaired") + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: fn, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + MaxPlainInstructions: -1, + ClassifyFunction: func(candidate *ssa.Function) (coro.SSAFunctionPolicy, error) { + if candidate == fn { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + functionPlan, ok := plan.FunctionPlan(fn) + if !ok { + t.Fatal("unpaired function has no coroutine plan") + } + if managed, err := universe.exactManagedGoLinknameDefinition(fn); err != nil || managed { + t.Fatalf("unpaired managed go:linkname proof = %t, %v; want false, nil", managed, err) + } + if err := validateCoroPhysicalABIWithUniverse(fn, functionPlan, plan, universe, true, true); err == nil || + !strings.Contains(err.Error(), "ABI directive") { + t.Fatalf("unpaired go:linkname validation = %v; want fail-closed ABI directive rejection", err) + } +} + +func TestCoroRawPlainEntryOwnsABIDirectiveWhileManagedPrimaryUsesSuffix(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func RawHelper(value uint32) uint32 { + for value != 0 { value-- } + return value +} +//export Host +func Host(value uint32) uint32 { return RawHelper(value) } +func Parent(value uint32) uint32 { return Host(value) } +`) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + parent, host, helper := ssaPkg.Func("Parent"), ssaPkg.Func("Host"), ssaPkg.Func("RawHelper") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: parent, Demand: coro.AsyncDemand}, + {Function: host, Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch fn { + case host: + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + case helper: + return coro.SSAFunctionPolicy{RawPlainVariant: true}, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + hostPlan, ok := plan.FunctionPlan(host) + if !ok || !hostPlan.RawPlainEntry || !plan.HasRawPlainVariant(host) || hostPlan.Emission != coro.EmitCoroutine { + prog.Dispose() + t.Fatalf("Host plan = %+v, present=%t raw-variant=%t", hostPlan, ok, plan.HasRawPlainVariant(host)) + } + withoutRawEntry := hostPlan + withoutRawEntry.RawPlainEntry = false + if err := validateCoroPhysicalABIWithUniverse(host, withoutRawEntry, plan, universe, true, true); err == nil || !strings.Contains(err.Error(), "ABI directive") { + prog.Dispose() + t.Fatalf("non-raw ABI directive validation = %v; want rejection", err) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify ABI-directed dual module: %v\n%s", err, module.String()) + } + baseName := "Host" + raw := module.NamedFunction(baseName) + managed := module.NamedFunction(baseName + coroPrimarySuffix) + if raw.IsNil() || managed.IsNil() { + t.Fatalf("ABI-directed dual lowering missing raw=%q or managed=%q:\n%s", baseName, baseName+coroPrimarySuffix, module.String()) + } + if strings.Contains(raw.Name(), coroPrimarySuffix) || managed.Name() == baseName { + t.Fatalf("export ownership crossed variants: raw=%q managed=%q", raw.Name(), managed.Name()) + } + moduleIR := module.String() + compilerUsedStart := strings.Index(moduleIR, "@llvm.compiler.used") + compilerUsedEnd := -1 + if compilerUsedStart >= 0 { + compilerUsedEnd = strings.Index(moduleIR[compilerUsedStart:], "\n") + } + if compilerUsedStart < 0 || compilerUsedEnd < 0 { + t.Fatalf("ABI-directed raw base has no llvm.compiler.used export retention:\n%s", moduleIR) + } + compilerUsed := moduleIR[compilerUsedStart : compilerUsedStart+compilerUsedEnd] + if !strings.Contains(compilerUsed, "@Host") || strings.Contains(compilerUsed, "Host$coro") { + t.Fatalf("ABI export retention is not owned exclusively by the raw base: %s", compilerUsed) + } + if !strings.Contains(raw.String(), "@foo.RawHelper(") || strings.Contains(raw.String(), "RawHelper$coro") { + t.Fatalf("ABI-directed raw base did not keep the raw helper call:\n%s", raw.String()) + } + if !strings.Contains(managed.String(), "RawHelper$coro") { + t.Fatalf("managed suffixed primary did not keep managed helper lowering:\n%s", managed.String()) + } +} + +func TestCoroRawPlainVariantCapturedClosurePreservesBindingABI(t *testing.T) { + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, `package runtime +import "unsafe" +func AllocU(size uintptr) unsafe.Pointer { + if size == 0 { return nil } + return nil +} +`) + fooPkg := testProg.addPackage(t, "foo", `package foo +type Box int +func (seed *Box) Add(delta int) int { + if seed == nil { return delta } + return delta +} +func Dual(seed *Box, value int) int { + callback := seed.Add + return callback(value) +} +func Parent(seed *Box, value int) int { return Dual(seed, value) } +`) + testProg.ssa.Build() + ssaPkg := fooPkg.ssa + files := []*ast.File{fooPkg.file} + dual := ssaPkg.Func("Dual") + parent := ssaPkg.Func("Parent") + var makeClosure *ssa.MakeClosure + for _, block := range dual.Blocks { + for _, instruction := range block.Instrs { + if closure, ok := instruction.(*ssa.MakeClosure); ok { + makeClosure = closure + } + } + } + if makeClosure == nil { + t.Fatal("Dual has no bound-method closure") + } + captured, ok := makeClosure.Fn.(*ssa.Function) + if !ok || captured == nil || len(captured.FreeVars) != 1 || len(makeClosure.Bindings) != 1 { + t.Fatalf("Dual captured closure = %v, bindings=%v", makeClosure.Fn, makeClosure.Bindings) + } + var add *ssa.Function + for _, block := range captured.Blocks { + for _, instruction := range block.Instrs { + if call, ok := instruction.(*ssa.Call); ok { + add = call.Common().StaticCallee() + } + } + } + if add == nil { + t.Fatal("bound-method closure has no exact Add target") + } + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: ssaPkg, Files: files}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: parent, Demand: coro.AsyncDemand}, + {Function: dual, Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyLoweredCalls: universe.CoroLoweredCalls, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch fn { + case dual: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, RawPlainEntry: true}, nil + case captured: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, RawPlainVariant: true}, nil + case add: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, RawPlainVariant: true}, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + capturedPlan, ok := plan.FunctionPlan(captured) + if !ok || capturedPlan.RawPlainEntry || !plan.HasRawPlainVariant(captured) || + capturedPlan.Emission != coro.EmitCoroutine || capturedPlan.Primary != coro.PrimaryCoroutine || capturedPlan.FuncRep != coro.DirectCoro { + prog.Dispose() + t.Fatalf("captured plan = %+v, present=%t variant=%t; want internal dual body only", capturedPlan, ok, plan.HasRawPlainVariant(captured)) + } + if valuePlan, present := plan.ValuePlan(makeClosure); !present || len(valuePlan.Funcs) != 1 || valuePlan.Funcs[0].Rep != coro.DirectCoro { + prog.Dispose() + t.Fatalf("captured closure value plan = %+v, present=%t; want one exact direct coroutine context", valuePlan, present) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.FuncRepABI = coro.FuncRepABIV1 + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify captured raw variant module: %v\n%s", err, module.String()) + } + capturedName, err := universe.physicalName(ssaPkg, captured, funcName(ssaPkg.Pkg, captured, false)) + if err != nil { + t.Fatal(err) + } + for _, name := range []string{"foo.Dual", "foo.Dual$coro", capturedName, capturedName + "$coro"} { + if module.NamedFunction(name).IsNil() { + t.Fatalf("captured dual lowering is missing %q:\n%s", name, module.String()) + } + } + rawDual := module.NamedFunction("foo.Dual").String() + if !strings.Contains(rawDual, "@\""+capturedName+"\"") && !strings.Contains(rawDual, "@"+capturedName) || + strings.Contains(rawDual, capturedName+"$coro") { + t.Fatalf("raw Dual did not construct its closure from the internal raw variant:\n%s", rawDual) + } + if !strings.Contains(rawDual, "store ptr %0") { + t.Fatalf("raw Dual did not store the exact seed binding into its closure context:\n%s", rawDual) + } + rawCaptured := module.NamedFunction(capturedName).String() + if !strings.Contains(rawCaptured, "load { ptr }") || !strings.Contains(rawCaptured, "extractvalue { ptr }") || !strings.Contains(rawCaptured, "Add") { + t.Fatalf("captured raw variant did not load the closure binding and combine it with its source argument:\n%s", rawCaptured) + } + managedDual := module.NamedFunction("foo.Dual$coro").String() + if !strings.Contains(managedDual, capturedName+"$coro") || strings.Contains(managedDual, "@\""+capturedName+"\"(") { + t.Fatalf("managed Dual selected the internal raw closure body:\n%s", managedDual) + } +} diff --git a/cl/coro_raw_plain_validate.go b/cl/coro_raw_plain_validate.go new file mode 100644 index 0000000000..8d5c98fb4a --- /dev/null +++ b/cl/coro_raw_plain_validate.go @@ -0,0 +1,282 @@ +/* + * 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 ( + "fmt" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// validateCoroRawPlainConsumers proves every call edge that is compiled a +// second time inside a dedicated legacy-stack body. The managed-body consumer +// verifier cannot cover these entries: EmitRawPlain has no managed body, and +// an EmitCoroutine raw variant resolves static/lowered calls differently from +// its managed primary. +// +// Local descriptor construction is deliberately fail-closed for now. Raw +// bodies can consume an exact incoming/stored Dispatch value, as required by +// the TLS destructor, but compileValue intentionally does not yet manufacture +// descriptor thunks while rawPlainBody is active. +func validateCoroRawPlainConsumers(plan *coro.SSAPlan, universe *EmissionUniverse, plainDispatch bool) error { + if plan == nil || universe == nil { + return fmt.Errorf("coroutine raw plain consumer validation requires a compilation plan and emission universe") + } + for _, function := range plan.Functions() { + fn, functionPlan := function.Function, function.Plan + if !plan.HasRawPlainVariant(fn) || + (functionPlan.Emission != coro.EmitRawPlain && functionPlan.Emission != coro.EmitCoroutine) { + continue + } + + for _, lowered := range plan.LoweredCalls(fn) { + target, frozen, err := universe.ResolveCoroLoweredCall(fn, lowered.LogicalName) + if err != nil { + return fmt.Errorf("coroutine raw plain ABI: function %q lowered call %q: %w", functionPlan.ID, lowered.LogicalName, err) + } + if !frozen || target == nil || target != lowered.Target { + return fmt.Errorf( + "coroutine raw plain ABI: function %q lowered call %q disagrees between the frozen emission universe and SSA plan", + functionPlan.ID, lowered.LogicalName, + ) + } + if err := validateCoroRawPlainCallTarget(plan, target); err != nil { + return fmt.Errorf("coroutine raw plain ABI: function %q lowered call %q: %w", functionPlan.ID, lowered.LogicalName, err) + } + } + + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + if err := validateCoroRawPlainLocalDescriptorProducer(plan, universe, fn, instruction); err != nil { + return err + } + call, isCall := instruction.(ssa.CallInstruction) + if !isCall { + continue + } + if direct, ok := call.(*ssa.Call); ok { + _, critical, err := universe.coroCriticalCallSite(direct) + if err != nil { + return coroLeafInstructionError(fn, functionPlan, instruction, "invalid critical marker: "+err.Error()) + } + if critical { + return coroLeafInstructionError(fn, functionPlan, instruction, + "managed critical intrinsic is invalid in a raw/plain body") + } + } + if plan.ElidesCall(call) { + continue + } + common := call.Common() + if common == nil { + return coroLeafInstructionError(fn, functionPlan, instruction, "raw plain call has no CallCommon") + } + if _, builtin := common.Value.(*ssa.Builtin); builtin { + continue + } + callPlan, planned := plan.CallPlan(call) + if !planned { + return coroLeafInstructionError(fn, functionPlan, instruction, "raw plain call has no compilation CallPlan") + } + if _, spawn := call.(*ssa.Go); spawn || callPlan.Kind == coro.CallSpawn { + return coroLeafInstructionError(fn, functionPlan, instruction, "raw plain body cannot spawn a goroutine") + } + + static := common.StaticCallee() + if static == nil || common.IsInvoke() || common.Method != nil { + if callPlan.Transport == coro.RawCCodePointer { + if _, ordinary := call.(*ssa.Call); !ordinary || common.IsInvoke() || common.Method != nil || + callPlan.Kind != coro.CallForeign || callPlan.Rep != coro.DirectPlain || !callPlan.Open || + callPlan.Unresolved != coro.UnknownForeign || callPlan.SyncDispatch { + return coroLeafInstructionError(fn, functionPlan, instruction, + "raw plain body has a malformed raw C code-pointer call") + } + if err := validateCoroCallableTransportValue(plan, fn, common.Value, universe); err != nil { + return coroLeafInstructionError(fn, functionPlan, instruction, + "raw C code-pointer callee: "+err.Error()) + } + if callPlan.MayBeNil && !ssaFunctionValueProvenNonNilAt(common.Value, call) { + return coroLeafInstructionError(fn, functionPlan, instruction, + "nullable raw C code-pointer call has no dominating non-nil proof") + } + continue + } + if !plainDispatch { + return coroLeafInstructionError(fn, functionPlan, instruction, "raw plain synchronous descriptor call requires the v1 plain dispatch capability") + } + if !callPlan.SyncDispatch { + return coroLeafInstructionError(fn, functionPlan, instruction, "raw plain body has a dynamic call without an exact SyncDispatch certificate") + } + if err := validateCoroPlainDispatchCall(plan, fn, call, callPlan, universe); err != nil { + return coroLeafInstructionError(fn, functionPlan, instruction, "invalid raw plain SyncDispatch call: "+err.Error()) + } + continue + } + + canonical, ok := universe.Resolve(static) + if !ok || canonical == nil { + return coroLeafInstructionError(fn, functionPlan, instruction, fmt.Sprintf( + "raw plain static callee %q is outside the frozen emission universe", static.Name(), + )) + } + if callPlan.Open || len(callPlan.Targets) != 1 { + return coroLeafInstructionError(fn, functionPlan, instruction, "raw plain static call does not have one exact closed target") + } + target, found := plan.Function(callPlan.Targets[0]) + if !found || target == nil || target != canonical { + return coroLeafInstructionError(fn, functionPlan, instruction, fmt.Sprintf( + "raw plain static call target disagrees with its frozen CallPlan target %q", callPlan.Targets[0], + )) + } + if err := validateCoroRawPlainCallTarget(plan, target); err != nil { + return coroLeafInstructionError(fn, functionPlan, instruction, err.Error()) + } + } + } + } + return nil +} + +func validateCoroRawPlainCallTarget(plan *coro.SSAPlan, target *ssa.Function) error { + if plan == nil || target == nil { + return fmt.Errorf("raw plain call has no exact target") + } + targetPlan, planned := plan.FunctionPlan(target) + if !planned { + return fmt.Errorf("raw plain call target %q has no function plan", target.Name()) + } + switch targetPlan.Emission { + case coro.EmitPlain: + if targetPlan.External != coro.Defined || targetPlan.Primary != coro.PrimaryPlain || targetPlan.Effect.MaySuspend() { + return fmt.Errorf( + "raw plain call target %q has an invalid plain entry (external=%s effect=%s primary=%s)", + targetPlan.ID, targetPlan.External, targetPlan.Effect, targetPlan.Primary, + ) + } + return nil + case coro.EmitExternal: + if targetPlan.External == coro.Defined || targetPlan.FuncRep == coro.DirectCoro { + return fmt.Errorf( + "raw plain call target %q has an invalid external entry (external=%s representation=%s)", + targetPlan.ID, targetPlan.External, targetPlan.FuncRep, + ) + } + return nil + case coro.EmitRawPlain, coro.EmitCoroutine: + if err := validatePlannedRawPlainVariant(target, targetPlan, plan.HasRawPlainVariant(target)); err != nil { + return fmt.Errorf("raw plain call target %q has no valid raw entry: %w", targetPlan.ID, err) + } + return nil + case coro.EmitNone: + return fmt.Errorf("raw plain call target %q is not emitted", targetPlan.ID) + default: + return fmt.Errorf("raw plain call target %q has invalid emission %d", targetPlan.ID, uint8(targetPlan.Emission)) + } +} + +func validateCoroRawPlainLocalDescriptorProducer(plan *coro.SSAPlan, universe *EmissionUniverse, owner *ssa.Function, instruction ssa.Instruction) error { + if plan == nil || owner == nil || instruction == nil { + return nil + } + if box, ok := instruction.(*ssa.MakeInterface); ok && coroCompilerElidedFunctionAddressBox(plan, universe, owner, box) { + return nil + } + if closure, ok := instruction.(*ssa.MakeClosure); ok { + dispatch, err := coroValueIsScalarManagedDispatch(plan, closure) + if err != nil { + return coroPlainDispatchInstructionError(owner, instruction, err.Error()) + } + if dispatch { + return coroPlainDispatchInstructionError( + owner, instruction, + "raw plain body cannot yet construct a local descriptor closure; only exact incoming or stored Dispatch values are supported", + ) + } + } + call, _ := instruction.(ssa.CallInstruction) + var staticValue ssa.Value + if call != nil && call.Common() != nil && call.Common().StaticCallee() != nil { + staticValue = call.Common().Value + } + for _, operand := range instruction.Operands(nil) { + if operand == nil || *operand == nil || *operand == staticValue { + continue + } + function, ok := (*operand).(*ssa.Function) + if !ok { + continue + } + dispatch, err := coroValueIsScalarManagedDispatch(plan, function) + if err != nil { + return coroPlainDispatchInstructionError(owner, instruction, err.Error()) + } + if !dispatch { + continue + } + return coroPlainDispatchInstructionError( + owner, instruction, + fmt.Sprintf("raw plain body cannot yet construct local descriptor value %q; only exact incoming or stored Dispatch values are supported", function.Name()), + ) + } + return nil +} + +// coroCompilerElidedFunctionAddressBox mirrors the frontend recipe used by +// funcPCABI0/funcAddr: x/tools inserts MakeInterface, but code generation +// inspects its exact static function operand and emits only a code address. +// This is not descriptor construction and grants no worker-call capability. +func coroCompilerElidedFunctionAddressBox(plan *coro.SSAPlan, universe *EmissionUniverse, owner *ssa.Function, box *ssa.MakeInterface) bool { + if plan == nil || universe == nil || owner == nil || box == nil || box.Parent() != owner { + return false + } + refs := box.Referrers() + if refs == nil || len(*refs) != 1 { + return false + } + direct, ok := (*refs)[0].(*ssa.Call) + if !ok || direct.Parent() != owner || direct.Common() == nil || len(direct.Common().Args) != 1 || + direct.Common().Args[0] != box || !plan.ElidesCall(direct) { + return false + } + if plan.StaticCodeAddressArgument(direct, 0) { + target, exact := coroFuncPCABI0ExactStaticOperand(direct) + return exact && target == box.X && universe.validateCoroFuncPCABI0CallSite(direct) == nil + } + if !plan.RawFunctionAddressArgument(direct, 0) { + return false + } + validatedBox, target, err := universe.validateCoroFuncAddrCallSite(direct) + return err == nil && validatedBox == box && target == box.X +} + +func coroValueIsScalarManagedDispatch(plan *coro.SSAPlan, value ssa.Value) (bool, error) { + if plan == nil || value == nil { + return false, nil + } + valuePlan, found := plan.ValuePlan(value) + if !found || len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0 || valuePlan.Funcs[0].Rep != coro.Dispatch { + return false, nil + } + if valuePlan.Funcs[0].Transport != coro.ManagedTransport { + return false, fmt.Errorf( + "value %q has Dispatch representation with non-managed transport %s", + value.Name(), valuePlan.Funcs[0].Transport, + ) + } + return true, nil +} diff --git a/cl/coro_raw_plain_validate_test.go b/cl/coro_raw_plain_validate_test.go new file mode 100644 index 0000000000..af0ef857e2 --- /dev/null +++ b/cl/coro_raw_plain_validate_test.go @@ -0,0 +1,299 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +func TestCoroRawPlainPreflightRejectsForgedStaticEdge(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func A() int { return 1 } +func B() int { return 2 } +func Host() int { return A() } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + host := ssaPkg.Func("Host") + plan := analyzeCoroRawPlainValidationPlan(t, universe, ssaPkg, host, coro.SSAConfig{ + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == host { + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + var staticCall *ssa.Call + for _, block := range host.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Common().StaticCallee() == ssaPkg.Func("A") { + staticCall = call + } + } + } + if staticCall == nil { + t.Fatal("Host has no static A call") + } + // The immutable plan still names A. Mutating the source operand to the + // signature-compatible B models any frontend/codegen edge that diverges + // after analysis; preflight must stop before an LLVM package is created. + staticCall.Call.Value = ssaPkg.Func("B") + err = rawPlainValidationCompilation(plan, universe, false).preflightCoroPlan() + if err == nil || !strings.Contains(err.Error(), "raw plain static call target disagrees with its frozen CallPlan target") { + t.Fatalf("forged raw static edge preflight error = %v", err) + } +} + +func TestCoroRawPlainPreflightRejectsForgedLoweredEdge(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func Helper() {} +func Host() {} +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + host, helper := ssaPkg.Func("Host"), ssaPkg.Func("Helper") + plan := analyzeCoroRawPlainValidationPlan(t, universe, ssaPkg, host, coro.SSAConfig{ + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == host { + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyLoweredCalls: func(owner *ssa.Function) ([]coro.SSALoweredCall, error) { + if owner == host { + return []coro.SSALoweredCall{{LogicalName: "forged.helper", Target: helper}}, nil + } + return nil, nil + }, + }) + if got := plan.LoweredCalls(host); len(got) != 1 || got[0].Target != helper { + t.Fatalf("forged lowered plan = %+v", got) + } + err = rawPlainValidationCompilation(plan, universe, false).preflightCoroPlan() + if err == nil || !strings.Contains(err.Error(), "lowered call \"forged.helper\" disagrees between the frozen emission universe and SSA plan") { + t.Fatalf("forged raw lowered edge preflight error = %v", err) + } +} + +func TestCoroRawPlainPreflightRejectsLocalDescriptorProducer(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +func Target(value int) int { return value + 1 } +func Apply(fn func(int) int, value int) int { + if fn == nil { return 0 } + return fn(value) +} +func Host(value int) int { return Apply(Target, value) } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + host, apply, target := ssaPkg.Func("Host"), ssaPkg.Func("Apply"), ssaPkg.Func("Target") + dynamicCall := coroPlainDispatchOnlyDynamicCall(t, apply) + var publicationCall ssa.CallInstruction + for _, block := range host.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if ok && call.Common() != nil && call.Common().StaticCallee() == apply { + publicationCall = call + } + } + } + if publicationCall == nil { + t.Fatal("Host has no Apply publication call") + } + plan := analyzeCoroRawPlainValidationPlan(t, universe, ssaPkg, host, coro.SSAConfig{ + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == host { + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyClosedDynamicCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.SSAClosedDynamicCallCertificate, bool, error) { + if call != dynamicCall { + return coro.SSAClosedDynamicCallCertificate{}, false, nil + } + return coro.SSAClosedDynamicCallCertificate{ + Targets: []*ssa.Function{target}, + MayBeNil: true, + SyncDispatch: true, + SyncOnlyCallArguments: []coro.SSASyncOnlyCallArgument{{ + Call: publicationCall, Argument: 0, + }}, + }, true, nil + }, + }) + targetValue, found := plan.ValuePlan(target) + if !found || len(targetValue.Funcs) != 1 || targetValue.Funcs[0].Rep != coro.Dispatch { + t.Fatalf("Target ValuePlan = %+v, present=%t; want a local descriptor producer", targetValue, found) + } + err = rawPlainValidationCompilation(plan, universe, true).preflightCoroPlan() + if err == nil || !strings.Contains(err.Error(), "raw plain body cannot yet construct local descriptor value \"Target\"") { + t.Fatalf("raw local descriptor producer preflight error = %v", err) + } +} + +func TestCoroRawPlainAcceptsCompilerElidedStaticCodeAddressBox(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +//llgo:link funcPCABI0 llgo.funcPCABI0 +func funcPCABI0(any) uintptr +func libc_execve_trampoline() +func Host() uintptr { return funcPCABI0(libc_execve_trampoline) } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + host := ssaPkg.Func("Host") + plan := analyzeCoroRawPlainValidationPlan(t, universe, ssaPkg, host, coro.SSAConfig{ + ClassifyStaticCodeAddressCallArgument: func(_ *ssa.Function, call ssa.CallInstruction, argument int) (bool, error) { + return universe.CoroStaticCodeAddressCallArgument(call, argument) + }, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == host { + return coro.SSAFunctionPolicy{RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + var box *ssa.MakeInterface + for _, block := range host.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.MakeInterface); ok { + box = candidate + } + } + } + if box != nil { + refs := box.Referrers() + if refs == nil || len(*refs) != 1 { + t.Fatalf("raw funcPCABI0 box referrers = %v", refs) + } + direct, ok := (*refs)[0].(*ssa.Call) + if !ok || !plan.StaticCodeAddressArgument(direct, 0) { + t.Fatalf("raw funcPCABI0 call has no frozen static code-address argument: call=%T %v", (*refs)[0], (*refs)[0]) + } + } + if box == nil || !coroCompilerElidedFunctionAddressBox(plan, universe, host, box) { + t.Fatal("raw funcPCABI0 operand is not recognized as one compiler-elided static code address") + } + if err := rawPlainValidationCompilation(plan, universe, false).preflightCoroPlan(); err != nil { + t.Fatalf("compiler-elided raw static code address rejected: %v", err) + } +} + +func TestCoroRawPlainPreflightRejectsCriticalMarker(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, `package foo +import _ "unsafe" +//go:linkname enter llgo.coroCriticalEnter +func enter() +//go:linkname exit llgo.coroCriticalExit +func exit() +var cell uint32 +func Host(value uint32) { enter(); cell = value; exit() } +`) + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + host := ssaPkg.Func("Host") + plan := analyzeCoroRawPlainValidationPlan(t, universe, ssaPkg, host, coro.SSAConfig{ + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == host { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly, Exec: coro.NeedsPreempt, RawPlainEntry: true}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + err = rawPlainValidationCompilation(plan, universe, false).preflightCoroPlan() + if err == nil || !strings.Contains(err.Error(), "managed critical intrinsic is invalid in a raw/plain body") { + t.Fatalf("raw critical marker preflight error = %v", err) + } +} + +func analyzeCoroRawPlainValidationPlan( + t *testing.T, + universe *EmissionUniverse, + ssaPkg *ssa.Package, + root *ssa.Function, + extra coro.SSAConfig, +) *coro.SSAPlan { + t.Helper() + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + extra.EmissionUniverse = ssaUniverse + extra.FunctionIDs = functionIDs + extra.MaxPlainInstructions = -1 + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, RawPlainDemand: true}}, extra) + if err != nil { + t.Fatal(err) + } + return plan +} + +func rawPlainValidationCompilation(plan *coro.SSAPlan, universe *EmissionUniverse, plainDispatch bool) *Compilation { + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + if plainDispatch { + compilation.CoroProfile = CoroProfileStackless + compilation.FuncRepABI = coro.FuncRepABIV1 + } + return compilation +} diff --git a/cl/coro_recover.go b/cl/coro_recover.go new file mode 100644 index 0000000000..166c3a1d5d --- /dev/null +++ b/cl/coro_recover.go @@ -0,0 +1,84 @@ +/* + * 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" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +func isCoroRecoverBuiltinCall(call *ssa.Call) bool { + if call == nil || call.Common() == nil { + return false + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + return ok && builtin.Name() == "recover" +} + +// compileCoroRecover replaces LLGo's legacy pthread-TLS Recover helper inside +// an explicit-status physical coroutine. The runtime validates the current +// frame against the exact parent-owned deferred-child scope and writes either +// the retained panic pair or two nil words. Constructing the empty interface +// directly keeps this operation allocation-free on every target. +func (p *context) compileCoroRecover(b llssa.Builder, call *ssa.CallCommon) llssa.Expr { + body := p.coroBody() + if body == nil || !p.coroEmissionExplicitStatus() || + b.Func != p.fn || call == nil || len(call.Args) != 0 || body.abi.recoverTakeHook == "" { + panic("coroutine recover requires an exact explicit-status physical call") + } + result := call.Signature().Results() + if result == nil || result.Len() != 1 { + panic("coroutine recover requires one empty-interface result") + } + resultType := p.patchType(result.At(0).Type()) + iface, ok := types.Unalias(resultType).Underlying().(*types.Interface) + if !ok || !iface.Empty() { + panic("coroutine recover result is not an empty interface") + } + + typeWord := p.coroFrameAlloca(p.prog.VoidPtr()) + dataWord := p.coroFrameAlloca(p.prog.VoidPtr()) + b.Store(typeWord, p.prog.Nil(p.prog.VoidPtr())) + b.Store(dataWord, p.prog.Nil(p.prog.VoidPtr())) + take := p.pkg.NewFunc(body.abi.recoverTakeHook, coroRecoverTakeSignature(), llssa.InC) + b.Call( + take.Expr, + body.task, + body.coro.Handle(), + b.Convert(p.prog.VoidPtr(), typeWord), + b.Convert(p.prog.VoidPtr(), dataWord), + ) + return b.Aggregate( + p.type_(resultType, llssa.InGo), + b.Convert(p.prog.AbiTypePtr(), b.Load(typeWord)), + b.Load(dataWord), + ) +} + +func coroRecoverTakeSignature() *types.Signature { + const noPos = 0 + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(noPos, nil, "g", pointer), + types.NewParam(noPos, nil, "child", pointer), + types.NewParam(noPos, nil, "typeOut", pointer), + types.NewParam(noPos, nil, "dataOut", pointer), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} diff --git a/cl/coro_recover_ir_test.go b/cl/coro_recover_ir_test.go new file mode 100644 index 0000000000..a5cdcc0c4a --- /dev/null +++ b/cl/coro_recover_ir_test.go @@ -0,0 +1,270 @@ +//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 ( + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroRecoverIRFixture = `package foo + +var FirstPayload uint32 +var SecondPayload uint32 + +func Catch() { recover() } + +func CatchAndRepanic() { + recover() + panic(&SecondPayload) +} + +func RootRecover(doPanic bool) { + defer Catch() + if doPanic { panic(&FirstPayload) } +} + +func RootRecoverNil() any { return recover() } + +func RootRepanic(doPanic bool) { + defer CatchAndRepanic() + if doPanic { panic(&FirstPayload) } +} +` + +func TestCoroExplicitStatusRecoverIRNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroRecoverIRFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + requireExactStaticCoroRecoverDefer(t, plan, functions["RootRecover"], functions["Catch"]) + requireExactStaticCoroRecoverDefer(t, plan, functions["RootRepanic"], functions["CatchAndRepanic"]) + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify explicit-status recover before CoroSplit: %v\n%s", err, module.String()) + } + assertCoroRecoverIR(t, module, false) + + runCoroABITestPipeline(t, prog, module) + assertCoroRecoverIR(t, module, true) + }) + } +} + +func assertCoroRecoverIR(t *testing.T, module llvm.Module, split bool) { + t.Helper() + suffix := "$coro" + if split { + suffix += ".resume" + } + function := func(source string) llvm.Value { + t.Helper() + value := module.NamedFunction("foo." + source + suffix) + if value.IsNil() { + t.Fatalf("recover fixture function %q is absent (post-split=%t):\n%s", source+suffix, split, module.String()) + } + return value + } + + rootRecover := function("RootRecover") + catch := function("Catch") + rootNil := function("RootRecoverNil") + rootRepanic := function("RootRepanic") + catchAndRepanic := function("CatchAndRepanic") + + if got := countCoroIRDirectCalls(rootRecover, coroAwaitPrepareHookV1); got != 1 { + t.Fatalf("RootRecover await_prepare_v3 calls = %d, want 1 (post-split=%t):\n%s", got, split, rootRecover.String()) + } + if got := countCoroIRDirectCalls(catch, coroRecoverTakeHookV1); got != 1 { + t.Fatalf("Catch recover_take_v1 calls = %d, want 1 (post-split=%t):\n%s", got, split, catch.String()) + } + if got := countCoroIRDirectCalls(rootNil, coroRecoverTakeHookV1); got != 1 { + t.Fatalf("root recover(nil) take calls = %d, want 1 (post-split=%t):\n%s", got, split, rootNil.String()) + } + if got := countCoroIRDirectCalls(rootNil, coroAwaitPrepareHookV1); got != 0 { + t.Fatalf("root recover(nil) unexpectedly creates a child transaction (post-split=%t):\n%s", split, rootNil.String()) + } + + if got := countCoroIRDirectCalls(rootRepanic, coroAwaitPrepareHookV1); got != 1 { + t.Fatalf("RootRepanic await_prepare_v3 calls = %d, want 1 (post-split=%t):\n%s", got, split, rootRepanic.String()) + } + if got := countCoroIRDirectCalls(catchAndRepanic, coroRecoverTakeHookV1); got != 1 { + t.Fatalf("CatchAndRepanic recover_take_v1 calls = %d, want 1 (post-split=%t):\n%s", got, split, catchAndRepanic.String()) + } + if got := countCoroIRDirectCalls(catchAndRepanic, coroPanicPrepareHookV1); got != 1 { + t.Fatalf("CatchAndRepanic repanic publications = %d, want 1 (post-split=%t):\n%s", got, split, catchAndRepanic.String()) + } + if !strings.Contains(catchAndRepanic.String(), "@foo.SecondPayload") { + t.Fatalf("CatchAndRepanic does not publish the replacement panic payload (post-split=%t):\n%s", split, catchAndRepanic.String()) + } + if strings.Contains(catchAndRepanic.String(), "@foo.FirstPayload") { + t.Fatalf("CatchAndRepanic retained the recovered payload as its repanic payload (post-split=%t):\n%s", split, catchAndRepanic.String()) + } + + for _, value := range []llvm.Value{rootRecover, catch, rootNil, rootRepanic, catchAndRepanic} { + if legacy := firstLegacyRecoverCall(value); legacy != "" { + t.Fatalf("%s calls legacy recover helper %q (post-split=%t):\n%s", value.Name(), legacy, split, value.String()) + } + } +} + +func countCoroIRDirectCalls(function llvm.Value, callee string) int { + count := 0 + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() == llvm.Call && instruction.CalledValue().Name() == callee { + count++ + } + } + } + return count +} + +func firstLegacyRecoverCall(function llvm.Value) string { + for _, block := range function.BasicBlocks() { + for instruction := block.FirstInstruction(); !instruction.IsNil(); instruction = llvm.NextInstruction(instruction) { + if instruction.InstructionOpcode() != llvm.Call { + continue + } + name := instruction.CalledValue().Name() + if name == "runtime.Recover" || strings.HasSuffix(name, "/runtime.Recover") || + strings.HasSuffix(name, "/runtime/internal/runtime.Recover") { + return name + } + } + } + return "" +} + +func requireExactStaticCoroRecoverDefer( + t *testing.T, plan *coro.SSAPlan, caller, target *ssa.Function, +) { + t.Helper() + callerPlan, callerOK := plan.FunctionPlan(caller) + targetPlan, targetOK := plan.FunctionPlan(target) + if !callerOK || callerPlan.Emission != coro.EmitCoroutine || + !callerPlan.Exec.Contains(coro.NeedsCleanupFrame) || !callerPlan.Effect.Contains(coro.AwaitStructured) { + t.Fatalf("recover caller plan = %+v, present=%t", callerPlan, callerOK) + } + if !targetOK || targetPlan.Emission != coro.EmitCoroutine || targetPlan.FuncRep != coro.DirectCoro { + t.Fatalf("recover target plan = %+v, present=%t", targetPlan, targetOK) + } + found := 0 + for _, block := range caller.Blocks { + for _, instruction := range block.Instrs { + deferred, ok := instruction.(*ssa.Defer) + if !ok || deferred.Common().StaticCallee() != target { + continue + } + found++ + callPlan, ok := plan.CallPlan(deferred) + if !ok || callPlan.Kind != coro.CallDefer || callPlan.Rep != coro.DirectCoro || callPlan.Open || + callPlan.MayBeNil || len(callPlan.Targets) != 1 || callPlan.Targets[0] != targetPlan.ID { + t.Fatalf("recover defer call plan = %+v, present=%t; want one exact DirectCoro target", callPlan, ok) + } + } + } + if found != 1 { + t.Fatalf("exact recover defer sites = %d, want 1", found) + } +} + +func compileCoroRecoverIRFixture( + t *testing.T, target *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, map[string]*ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroRecoverIRFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := map[string]*ssa.Function{ + "Catch": ssaPkg.Func("Catch"), + "CatchAndRepanic": ssaPkg.Func("CatchAndRepanic"), + "RootRecover": ssaPkg.Func("RootRecover"), + "RootRecoverNil": ssaPkg.Func("RootRecoverNil"), + "RootRepanic": ssaPkg.Func("RootRepanic"), + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: functions["RootRecover"], Demand: coro.AsyncDemand}, + {Function: functions["RootRecoverNil"], Demand: coro.AsyncDemand}, + {Function: functions["RootRepanic"], Demand: coro.AsyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + for _, fixture := range functions { + if function == fixture { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, functions +} diff --git a/cl/coro_root.go b/cl/coro_root.go new file mode 100644 index 0000000000..8f36c50b52 --- /dev/null +++ b/cl/coro_root.go @@ -0,0 +1,358 @@ +/* + * 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 ( + "crypto/sha256" + "encoding/hex" + "fmt" + "go/token" + "go/types" + "sort" + "strings" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroRootFactoryPrefix = "__llgo_coro_root_factory_v1." + coroRootFactoryDescriptorPrefix = "__llgo_coro_root_factory_descriptor_v1." + coroRootPackageAnchorPrefix = "__llgo_coro_root_package_v1." + coroRootPackageAnchorVersionV1 = uint32(1) +) + +type coroRootFactoryRegistration struct { + functionID coro.FunctionID + abiHash [16]byte + descriptor llssa.Expr +} + +func coroRootFactorySignature() *types.Signature { + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "out", types.Typ[types.UnsafePointer]), + types.NewParam(token.NoPos, nil, "startup", types.Typ[types.UnsafePointer]), + ) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "handle", types.Typ[types.UnsafePointer])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func explicitCoroRoot(plan *coro.SSAPlan, fn *ssa.Function) (coro.SSARootPlan, bool) { + if plan == nil || fn == nil { + return coro.SSARootPlan{}, false + } + for _, root := range plan.Roots() { + if root.Function == fn { + return root, true + } + } + return coro.SSARootPlan{}, false +} + +func validateCoroRootEntries(plan *coro.SSAPlan) error { + if plan == nil { + return fmt.Errorf("coroutine root validation requires a compilation CoroPlan") + } + for _, root := range plan.Roots() { + if root.Function == nil { + return fmt.Errorf("coroutine root factory %q has no SSA function", root.ID) + } + function, ok := plan.FunctionPlan(root.Function) + if !ok || function.ID != root.ID { + return fmt.Errorf("coroutine root factory %q has no canonical function plan", root.ID) + } + if function.External != coro.Defined || + !function.ManagedDemand.Contains(root.ManagedDemand) || + root.RawPlainDemand && !function.RawPlainDemand { + return fmt.Errorf( + "coroutine root %q requires a defined body whose demand contains the explicit root (external=%s emission=%s representation=%s managed=%s raw=%t root-managed=%s root-raw=%t)", + root.ID, function.External, function.Emission, function.FuncRep, + function.ManagedDemand, function.RawPlainDemand, root.ManagedDemand, root.RawPlainDemand, + ) + } + if root.Function.Parent() != nil || len(root.Function.FreeVars) != 0 { + return fmt.Errorf("coroutine root %q must be a top-level non-capturing entry; captured environments are supplied only by dynamic descriptors", root.ID) + } + if root.RawPlainDemand { + if err := validatePlannedRawPlainEntry(root.Function, function); err != nil { + return fmt.Errorf("coroutine raw root %q: %w", root.ID, err) + } + } + switch function.Emission { + case coro.EmitPlain: + // AsyncDemand describes an entry context, not a requirement to clone + // or coroutine-lower a body that cannot suspend. A plain root is invoked + // through its plain primary inside a scheduler-owned bootstrap coroutine + // and needs no per-function root factory. Independent first-class uses + // may still require a Dispatch descriptor for that same single body. + if function.FuncRep != coro.DirectPlain && function.FuncRep != coro.Dispatch { + return fmt.Errorf( + "plain coroutine root %q requires a plain-primary representation, got %s", + root.ID, function.FuncRep, + ) + } + case coro.EmitCoroutine: + if root.ManagedDemand.Contains(coro.SyncDemand) && !function.RawPlainEntry { + return fmt.Errorf( + "coroutine root %q (%s) has synchronous demand without a planned raw plain entry, got root=%s total=%s (managed dimensions); suspending edges: %s", + root.ID, root.Function.String(), root.ManagedDemand, function.ManagedDemand, coroRootSuspendingEdges(plan, root.Function), + ) + } + if root.ManagedDemand.Contains(coro.AsyncDemand) && !function.ManagedDemand.Contains(coro.AsyncDemand) { + return fmt.Errorf("coroutine root factory %q has async root demand absent from managed demand %s", root.ID, function.ManagedDemand) + } + if root.ManagedDemand.Contains(coro.AsyncDemand) && function.FuncRep != coro.DirectCoro { + return fmt.Errorf( + "coroutine root factory %q requires direct-coro representation, got %s", + root.ID, function.FuncRep, + ) + } + case coro.EmitRawPlain: + if !root.RawPlainDemand || root.ManagedDemand != coro.NoDemand || !function.RawPlainOnly { + return fmt.Errorf( + "raw-only coroutine root %q has incompatible root/plan dimensions (root-managed=%s root-raw=%t raw-only=%t)", + root.ID, root.ManagedDemand, root.RawPlainDemand, function.RawPlainOnly, + ) + } + default: + return fmt.Errorf( + "coroutine root %q requires a plain, raw-plain, or coroutine body, got emission %s", + root.ID, function.Emission, + ) + } + } + return nil +} + +func coroRootSuspendingEdges(plan *coro.SSAPlan, fn *ssa.Function) string { + type pending struct { + function *ssa.Function + path string + } + var leaves []string + queue := []pending{{function: fn}} + seen := make(map[*ssa.Function]bool) + for len(queue) != 0 && len(seen) < 256 { + item := queue[0] + queue = queue[1:] + if item.function == nil || seen[item.function] { + continue + } + seen[item.function] = true + children := 0 + for _, block := range item.function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(ssa.CallInstruction) + if !ok { + continue + } + callPlan, ok := plan.CallPlan(call) + if !ok { + continue + } + for _, id := range callPlan.Targets { + target, found := plan.Function(id) + targetPlan, planned := plan.FunctionPlan(target) + if found && planned && targetPlan.Effect.MaySuspend() { + path := item.path + " -> " + target.String() + children++ + queue = append(queue, pending{function: target, path: path}) + } + } + } + } + for _, lowered := range plan.LoweredCalls(item.function) { + targetPlan, planned := plan.FunctionPlan(lowered.Target) + if lowered.Target != nil && planned && targetPlan.Effect.MaySuspend() { + path := item.path + " -> lowered:" + lowered.Target.String() + children++ + queue = append(queue, pending{function: lowered.Target, path: path}) + } + } + if children == 0 && item.function != fn { + functionPlan, _ := plan.FunctionPlan(item.function) + leaves = append(leaves, item.path+"["+functionPlan.Effect.String()+"]") + } + } + if len(leaves) == 0 { + return "" + } + sort.Strings(leaves) + return strings.Join(leaves, ", ") +} + +// emitCoroRootFactory emits a typed, non-coroutine factory only for an +// explicitly declared Async root. The startup/result objects are owned by the +// runtime and outlive this native wrapper invocation; the factory merely loads +// scalar arguments and calls the root's unique coroutine ramp. +func (p *context) emitCoroRootFactory(pkg llssa.Package, entry plannedFunctionSymbol, abi coroPhysicalABI, sourceSig *types.Signature, ramp llssa.Function) { + if p.compilation == nil || p.compilation.CoroPlan == nil { + panic("coroutine root factory requires a compilation CoroPlan") + } + root, ok := explicitCoroRoot(p.compilation.CoroPlan, entry.function) + if !ok { + return + } + if !root.ManagedDemand.Contains(coro.AsyncDemand) { + // An explicit synchronous raw-address root is satisfied by the separately + // emitted legacy entry and needs no scheduler bootstrap factory. + return + } + if entry.plan.ID != root.ID { + panic(fmt.Sprintf("coroutine root factory: unsupported root %q managed demand %s", root.ID, root.ManagedDemand)) + } + + fields := make([]*types.Var, sourceSig.Params().Len()) + for i := range fields { + fields[i] = types.NewField(token.NoPos, nil, fmt.Sprintf("a%d", i), sourceSig.Params().At(i).Type(), false) + } + startupGoType := types.NewStruct(fields, nil) + startupType := p.prog.Type(startupGoType, llssa.InGo) + resultType := p.prog.Type(abi.resultSlotType, llssa.InGo) + hash := hex.EncodeToString(abi.hash[:]) + factoryName := coroRootFactoryPrefix + hash + factory := pkg.FuncOf(factoryName) + if factory == nil { + factory = pkg.NewFunc(factoryName, coroRootFactorySignature(), llssa.InC) + } + if !factory.HasBody() { + b := factory.MakeBody(1) + physicalArgs := make([]llssa.Expr, 0, len(fields)+2) + physicalArgs = append(physicalArgs, factory.PhysicalParam(0), factory.PhysicalParam(1)) + if len(fields) != 0 { + startup := b.Convert(p.prog.Pointer(startupType), factory.PhysicalParam(2)) + for i := range fields { + physicalArgs = append(physicalArgs, b.Load(b.FieldAddr(startup, i))) + } + } + handle := b.Call(ramp.Expr, physicalArgs...) + b.Return(handle) + b.EndBuild() + b.Dispose() + } + descriptor := pkg.NewCoroRootFactoryDescriptor(coroRootFactoryDescriptorPrefix+hash, llssa.CoroRootFactoryDescriptorOptions{ + Version: coroPhysicalABIVersionV1, + ABIHash: abi.hash, + Factory: factory.Expr, + Startup: startupType, + Result: resultType, + }) + p.coroRootFactories = append(p.coroRootFactories, coroRootFactoryRegistration{ + functionID: root.ID, + abiHash: abi.hash, + descriptor: descriptor, + }) +} + +// emitCoroRootPackageAnchor emits the package's one linker-visible root +// registry after all source and deferred init compilation has finished. Root +// factories may be discovered in frontend emission order; the registry ABI is +// always canonical FunctionID order. +func (p *context) emitCoroRootPackageAnchor(pkg llssa.Package) { + if len(p.coroRootFactories) == 0 { + return + } + roots := append([]coroRootFactoryRegistration(nil), p.coroRootFactories...) + sort.Slice(roots, func(i, j int) bool { + return roots[i].functionID < roots[j].functionID + }) + descriptors := make([]llssa.Expr, len(roots)) + for i, root := range roots { + if i != 0 && roots[i-1].functionID == root.functionID { + panic(fmt.Sprintf("coroutine root package anchor: duplicate canonical root %q", root.functionID)) + } + descriptors[i] = root.descriptor + } + hash := p.coroRootPackageAnchorHash(pkg, roots) + pkg.NewCoroRootPackageAnchor( + coroRootPackageAnchorPrefix+hex.EncodeToString(hash[:]), + llssa.CoroRootPackageAnchorOptions{ + Version: coroRootPackageAnchorVersionV1, + ABIHash: hash, + Descriptors: descriptors, + }, + ) +} + +// coroRootPackageAnchorHash is the single source for both the anchor symbol +// suffix and its embedded ABI hash. Normal builds use the canonical whole-plan +// digest supplied by the driver. Direct cl tests intentionally may omit that +// digest, so a domain-separated fallback covers the ordered roots and complete +// effective target layout without introducing pointer or emission-order state. +func (p *context) coroRootPackageAnchorHash(pkg llssa.Package, roots []coroRootFactoryRegistration) [16]byte { + coroABI := coro.PhysicalABIV1 + schedulerABI := coro.SchedulerChildAwaitABIV0 + panicABI := coro.PanicLegacyABIV0 + funcRepABI := coro.FuncRepABIV0 + planDigest := "" + if p.compilation != nil { + planDigest = p.compilation.CoroPlanDigest + if p.compilation.CoroABI != "" { + coroABI = p.compilation.CoroABI + } + if p.compilation.SchedulerABI != "" { + schedulerABI = p.compilation.SchedulerABI + } + if p.compilation.PanicABI != "" { + panicABI = p.compilation.PanicABI + } + if p.compilation.FuncRepABI != "" { + funcRepABI = p.compilation.FuncRepABI + } + } + target := p.prog.TargetSpec() + rootIdentities := make([]string, len(roots)) + for i, root := range roots { + rootIdentities[i] = string(root.functionID) + "\x00" + hex.EncodeToString(root.abiHash[:]) + } + if planDigest == "" { + fallback := strings.Join(rootIdentities, "\x00") + sum := sha256.Sum256([]byte(fmt.Sprintf( + "llgo-coro-root-package-plan-fallback-v1\x00roots=%s\x00triple=%s\x00cpu=%s\x00features=%s\x00target-abi=%s\x00data-layout=%s\x00ptr=%d", + fallback, + target.Triple, + target.CPU, + target.Features, + target.TargetABI, + p.prog.DataLayout(), + p.prog.PointerSize(), + ))) + planDigest = hex.EncodeToString(sum[:]) + } + key := fmt.Sprintf( + "llgo-coro-root-package-v1\x00package=%s\x00plan=%s\x00coro=%s\x00scheduler=%s\x00panic=%s\x00func-rep=%s\x00triple=%s\x00cpu=%s\x00features=%s\x00target-abi=%s\x00data-layout=%s\x00ptr=%d\x00roots=%s", + pkg.Path(), + planDigest, + coroABI, + schedulerABI, + panicABI, + funcRepABI, + target.Triple, + target.CPU, + target.Features, + target.TargetABI, + p.prog.DataLayout(), + p.prog.PointerSize(), + strings.Join(rootIdentities, "\x00"), + ) + sum := sha256.Sum256([]byte(key)) + var hash [16]byte + copy(hash[:], sum[:len(hash)]) + return hash +} diff --git a/cl/coro_safe_index.go b/cl/coro_safe_index.go new file mode 100644 index 0000000000..66a20cacea --- /dev/null +++ b/cl/coro_safe_index.go @@ -0,0 +1,54 @@ +/* + * 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 ( + "fmt" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// frozenSafeFixedArrayIndex consumes the per-instruction plan fact used to +// remove one redundant bounds helper. Re-running the shared proof here is only +// a consistency check against mutated SSA/frontend type projection; the plan +// fact is the sole authority for selecting unchecked code generation. +func (p *context) frozenSafeFixedArrayIndex( + operation ssa.Instruction, + collection, index ssa.Value, +) bool { + if p == nil || operation == nil || collection == nil || index == nil || + p.compilation == nil || + p.compilation.CoroPlan == nil || p.emissionUniverse == nil { + return false + } + if p.goFn == nil || operation.Parent() != p.goFn { + panic(fmt.Errorf("safe fixed-array index escaped its exact SSA owner")) + } + plannedBound, planned := p.compilation.CoroPlan.ExactSafeFixedArrayIndex(operation) + actualBound, fixedArray := emissionFixedArrayBound(p, collection) + recomputed := fixedArray && coro.ProveSSAExactSafeFixedArrayIndex( + operation.Parent(), index, actualBound, operation, + ) + if planned != recomputed || planned && plannedBound != actualBound { + panic(fmt.Errorf( + "safe fixed-array index in %q disagrees between frozen plan and frontend proof (planned=%t bound=%d recomputed=%t bound=%d)", + p.goFn.Name(), planned, plannedBound, recomputed, actualBound, + )) + } + return planned +} diff --git a/cl/coro_semantic_plan.go b/cl/coro_semantic_plan.go new file mode 100644 index 0000000000..2a1102bcfa --- /dev/null +++ b/cl/coro_semantic_plan.go @@ -0,0 +1,225 @@ +/* + * 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 ( + "fmt" + "go/token" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +// coroSemanticInstructionPlan is the owner-scoped, pre-analysis recipe for +// one source instruction. It is deliberately smaller than Go SSA: operands, +// results, Phi edges and ordinary CFG remain owned by x/tools. The recipe is +// the single production authority for local Effect/Exec and for the semantic +// identity later copied into LoweringFacts and the physical function plan. +type coroSemanticInstructionPlan struct { + class coro.OpClass + recipe coro.RecipeID + effect coro.Effect + exec coro.ExecFlags + materialized bool + debug bool +} + +// planCoroSemanticInstruction is the only raw-SSA semantic recipe classifier. +// It runs while the emission closure is still open. Analysis, preflight and +// emission consume the frozen result and must not repeat this switch. +func planCoroSemanticInstruction(instruction ssa.Instruction) (coroSemanticInstructionPlan, error) { + ordinary := func(recipe string) (coroSemanticInstructionPlan, error) { + return coroSemanticInstructionPlan{ + class: coro.OpPure, + recipe: coro.RecipeID(recipe), + effect: coro.NoSuspend, + }, nil + } + control := func(recipe string, exec coro.ExecFlags) (coroSemanticInstructionPlan, error) { + return coroSemanticInstructionPlan{ + class: coro.OpControl, + recipe: coro.RecipeID(recipe), + effect: coro.NoSuspend, + exec: exec, + materialized: true, + }, nil + } + if instruction == nil { + return coroSemanticInstructionPlan{}, fmt.Errorf("semantic instruction plan requires one source instruction") + } + switch instruction := instruction.(type) { + case *ssa.Alloc: + return ordinary("cl.ssa.alloc.v1") + case *ssa.Phi: + return ordinary("cl.ssa.phi.v1") + case *ssa.Call: + plan, err := ordinary("cl.ssa.call.v1") + if err != nil { + return plan, err + } + if common := instruction.Common(); common != nil { + if builtin, ok := common.Value.(*ssa.Builtin); ok && builtin.Name() == "panic" { + plan.class = coro.OpControl + plan.exec = coro.MayUnwind + plan.materialized = true + plan.recipe = coro.RecipeID("cl.ssa.builtin-panic.v0") + } + } + return plan, nil + case *ssa.BinOp: + return ordinary("cl.ssa.binop.v1") + case *ssa.UnOp: + if instruction.Op == token.ARROW { + return coroSemanticInstructionPlan{ + class: coro.OpChannel, + recipe: coro.RecipeID("cl.ssa.channel-recv.v0"), + effect: coro.MayPark, + materialized: true, + }, nil + } + return ordinary("cl.ssa.unop.v1") + case *ssa.ChangeType: + return ordinary("cl.ssa.change-type.v1") + case *ssa.Convert: + return ordinary("cl.ssa.convert.v1") + case *ssa.MultiConvert: + return ordinary("cl.ssa.multi-convert.v1") + case *ssa.ChangeInterface: + return ordinary("cl.ssa.change-interface.v1") + case *ssa.SliceToArrayPointer: + return ordinary("cl.ssa.slice-to-array-pointer.v1") + case *ssa.MakeInterface: + return ordinary("cl.ssa.make-interface.v1") + case *ssa.MakeClosure: + return ordinary("cl.ssa.make-closure.v1") + case *ssa.MakeMap: + return ordinary("cl.ssa.make-map.v1") + case *ssa.MakeChan: + return ordinary("cl.ssa.make-chan.v1") + case *ssa.MakeSlice: + return ordinary("cl.ssa.make-slice.v1") + case *ssa.Slice: + return ordinary("cl.ssa.slice.v1") + case *ssa.FieldAddr: + return ordinary("cl.ssa.field-addr.v1") + case *ssa.Field: + return ordinary("cl.ssa.field.v1") + case *ssa.IndexAddr: + return ordinary("cl.ssa.index-addr.v1") + case *ssa.Index: + return ordinary("cl.ssa.index.v1") + case *ssa.Lookup: + return ordinary("cl.ssa.lookup.v1") + case *ssa.Select: + effect := coro.NoSuspend + recipe := "cl.ssa.select.v0" + if instruction.Blocking { + effect = coro.MayPark + } + return coroSemanticInstructionPlan{ + class: coro.OpSelect, + recipe: coro.RecipeID(recipe), + effect: effect, + materialized: true, + }, nil + case *ssa.Range: + return ordinary("cl.ssa.range.v1") + case *ssa.Next: + return ordinary("cl.ssa.next.v1") + case *ssa.TypeAssert: + return ordinary("cl.ssa.type-assert.v1") + case *ssa.Extract: + return ordinary("cl.ssa.extract.v1") + case *ssa.Jump: + return ordinary("cl.ssa.jump.v1") + case *ssa.If: + return ordinary("cl.ssa.if.v1") + case *ssa.Return: + plan, err := control("cl.ssa.return.v1", 0) + plan.materialized = false + return plan, err + case *ssa.RunDefers: + return control("cl.ssa.run-defers.v0", coro.NeedsCleanupFrame) + case *ssa.Panic: + return control("cl.ssa.panic.v0", coro.MayUnwind) + case *ssa.Go: + return coroSemanticInstructionPlan{ + class: coro.OpSpawn, + recipe: coro.RecipeID("cl.ssa.spawn.v0"), + effect: coro.NoSuspend, + materialized: true, + }, nil + case *ssa.Defer: + return control("cl.ssa.defer.v0", coro.NeedsCleanupFrame) + case *ssa.Send: + return coroSemanticInstructionPlan{ + class: coro.OpChannel, + recipe: coro.RecipeID("cl.ssa.channel-send.v0"), + effect: coro.MayPark, + materialized: true, + }, nil + case *ssa.Store: + return ordinary("cl.ssa.store.v1") + case *ssa.MapUpdate: + return ordinary("cl.ssa.map-update.v1") + case *ssa.DebugRef: + return coroSemanticInstructionPlan{ + class: coro.OpPure, + recipe: coro.RecipeID("cl.ssa.debug-ref.v1"), + effect: coro.NoSuspend, + debug: true, + }, nil + default: + return coroSemanticInstructionPlan{}, fmt.Errorf("unsupported source instruction type %T", instruction) + } +} + +func coroSemanticCFGHasCycle(blocks []*ssa.BasicBlock) bool { + if len(blocks) == 0 { + return false + } + indegree := make([]int, len(blocks)) + for _, block := range blocks { + if block == nil { + continue + } + for _, successor := range block.Succs { + if successor == nil || successor.Index < 0 || successor.Index >= len(indegree) { + return true + } + indegree[successor.Index]++ + } + } + queue := make([]*ssa.BasicBlock, 0, len(blocks)) + for index, block := range blocks { + if block != nil && indegree[index] == 0 { + queue = append(queue, block) + } + } + visited := 0 + for head := 0; head < len(queue); head++ { + block := queue[head] + visited++ + for _, successor := range block.Succs { + indegree[successor.Index]-- + if indegree[successor.Index] == 0 { + queue = append(queue, successor) + } + } + } + return visited != len(blocks) +} diff --git a/cl/coro_signature_audit_test.go b/cl/coro_signature_audit_test.go new file mode 100644 index 0000000000..ce97075049 --- /dev/null +++ b/cl/coro_signature_audit_test.go @@ -0,0 +1,198 @@ +//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" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +// This test freezes the x/tools SSA operand shapes used by coroutine ABI +// normalization. In particular, a declared receiver is a Function.Param and a +// direct-call argument even though go/types keeps it outside Signature.Params. +// Interface receivers and closure bindings use different SSA fields and must +// not be folded into the same signature-only rule. +func TestCoroPhysicalABISSAOperandShapeAudit(t *testing.T) { + pkg, _, _ := buildGoSSAPkg(t, `package foo + +type Counter struct { value int } +func (counter *Counter) Add(delta int) int { return counter.value + delta } + +type Adder interface { Add(int) int } +func Direct(counter *Counter) int { return counter.Add(2) } +func Invoke(adder Adder) int { return adder.Add(3) } + +func CallClosure(base int) int { + add := func(delta int) int { return base + delta } + return add(4) +} + +func Recursive(value int) int { + if value == 0 { return 0 } + return Recursive(value - 1) + 1 +} + +func Heap() *Counter { return &Counter{} } +`) + + method := coroSignatureAuditDeclaredMethod(t, pkg) + sig := method.Signature + if sig == nil || sig.Recv() == nil || sig.Params().Len() != 1 { + t.Fatalf("declared method signature = %v", sig) + } + if got, want := len(method.Params), sig.Params().Len()+1; got != want { + t.Fatalf("method SSA params = %d, want receiver + signature params = %d", got, want) + } + if !types.Identical(method.Params[0].Type(), sig.Recv().Type()) { + t.Fatalf("method first SSA param %s != receiver %s", method.Params[0].Type(), sig.Recv().Type()) + } + for index := 0; index < sig.Params().Len(); index++ { + if !types.Identical(method.Params[index+1].Type(), sig.Params().At(index).Type()) { + t.Fatalf("method SSA param %d %s != signature param %d %s", index+1, method.Params[index+1].Type(), index, sig.Params().At(index).Type()) + } + } + + // LLGo already uses this receiver-to-leading-parameter normalization for + // ordinary Go declarations. A coroutine signature projection can reuse the + // same ordering without changing the immutable source Signature or Params. + normalized := llssa.FuncAddCtx(sig.Recv(), sig) + if normalized.Recv() != nil || normalized.Params().Len() != len(method.Params) { + t.Fatalf("normalized method signature = %v, SSA params = %d", normalized, len(method.Params)) + } + for index, parameter := range method.Params { + if !types.Identical(normalized.Params().At(index).Type(), parameter.Type()) { + t.Fatalf("normalized param %d %s != SSA param %s", index, normalized.Params().At(index).Type(), parameter.Type()) + } + } + + direct := pkg.Func("Direct") + directCall := coroSignatureAuditOnlyCall(t, direct, func(call *ssa.Call) bool { + return call.Common().StaticCallee() == method + }) + if directCall.Common().IsInvoke() || len(directCall.Common().Args) != len(method.Params) { + t.Fatalf("direct method call = %s; args=%d method-params=%d", directCall, len(directCall.Common().Args), len(method.Params)) + } + if directCall.Common().Args[0] != direct.Params[0] { + t.Fatalf("direct method receiver is not call argument zero: %s", directCall) + } + + invoke := pkg.Func("Invoke") + invokeCall := coroSignatureAuditOnlyCall(t, invoke, func(call *ssa.Call) bool { + return call.Common().IsInvoke() + }) + if invokeCall.Common().Value != invoke.Params[0] || invokeCall.Common().Method == nil { + t.Fatalf("interface receiver/method are not carried by CallCommon: %+v", invokeCall.Common()) + } + if got := len(invokeCall.Common().Args); got != 1 { + t.Fatalf("interface invoke args = %d, want only the explicit delta argument", got) + } + + closureOwner := pkg.Func("CallClosure") + if len(closureOwner.AnonFuncs) != 1 { + t.Fatalf("CallClosure anonymous functions = %d, want one", len(closureOwner.AnonFuncs)) + } + closure := closureOwner.AnonFuncs[0] + if closure.Parent() != closureOwner || len(closure.Params) != 1 || len(closure.FreeVars) != 1 { + t.Fatalf("closure shape: parent=%v params=%d free-vars=%d", closure.Parent(), len(closure.Params), len(closure.FreeVars)) + } + makeClosure := coroSignatureAuditOnlyMakeClosure(t, closureOwner) + if makeClosure.Fn != closure || len(makeClosure.Bindings) != len(closure.FreeVars) { + t.Fatalf("MakeClosure shape = %s; bindings=%d free-vars=%d", makeClosure, len(makeClosure.Bindings), len(closure.FreeVars)) + } + closureCall := coroSignatureAuditOnlyCall(t, closureOwner, func(call *ssa.Call) bool { + return call.Common().StaticCallee() == closure + }) + if closureCall.Common().Value != makeClosure || len(closureCall.Common().Args) != len(closure.Params) { + t.Fatalf("closure call = %s; value=%T args=%d params=%d", closureCall, closureCall.Common().Value, len(closureCall.Common().Args), len(closure.Params)) + } + if closureCall.Common().Args[0] == makeClosure.Bindings[0] { + t.Fatal("closure binding was incorrectly duplicated into ordinary call arguments") + } + + recursive := pkg.Func("Recursive") + recursiveCall := coroSignatureAuditOnlyCall(t, recursive, func(call *ssa.Call) bool { + return call.Common().StaticCallee() == recursive + }) + if len(recursiveCall.Common().Args) != len(recursive.Params) { + t.Fatalf("recursive call args=%d params=%d", len(recursiveCall.Common().Args), len(recursive.Params)) + } + + heap := pkg.Func("Heap") + allocations := 0 + for _, block := range heap.Blocks { + for _, instruction := range block.Instrs { + if alloc, ok := instruction.(*ssa.Alloc); ok && alloc.Heap { + allocations++ + } + } + } + if allocations != 1 { + t.Fatalf("Heap escaping SSA allocations = %d, want one", allocations) + } +} + +func coroSignatureAuditDeclaredMethod(t *testing.T, pkg *ssa.Package) *ssa.Function { + t.Helper() + var matches []*ssa.Function + for function := range ssautil.AllFunctions(pkg.Prog) { + if function != nil && function.Name() == "Add" && function.Signature != nil && function.Signature.Recv() != nil && function.Object() != nil { + matches = append(matches, function) + } + } + if len(matches) != 1 { + t.Fatalf("declared Add methods = %d, want one", len(matches)) + } + return matches[0] +} + +func coroSignatureAuditOnlyCall(t *testing.T, function *ssa.Function, match func(*ssa.Call) bool) *ssa.Call { + t.Helper() + var matches []*ssa.Call + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + if call, ok := instruction.(*ssa.Call); ok && match(call) { + matches = append(matches, call) + } + } + } + if len(matches) != 1 { + t.Fatalf("%s matching calls = %d, want one", function, len(matches)) + } + return matches[0] +} + +func coroSignatureAuditOnlyMakeClosure(t *testing.T, function *ssa.Function) *ssa.MakeClosure { + t.Helper() + var matches []*ssa.MakeClosure + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + if closure, ok := instruction.(*ssa.MakeClosure); ok { + matches = append(matches, closure) + } + } + } + if len(matches) != 1 { + t.Fatalf("%s MakeClosure instructions = %d, want one", function, len(matches)) + } + return matches[0] +} diff --git a/cl/coro_site_plan.go b/cl/coro_site_plan.go new file mode 100644 index 0000000000..7c6d29b252 --- /dev/null +++ b/cl/coro_site_plan.go @@ -0,0 +1,458 @@ +/* + * 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 ( + "fmt" + "sort" + "strings" + + "golang.org/x/tools/go/ssa" +) + +// coroSiteEmissionObserver binds actual LLSSA runtime-helper emission back to +// the exact pre-analysis SitePlan. It is a compile-time verifier only; no +// observer state enters generated code or the runtime ABI. +type coroSiteEmissionObserver struct { + instruction ssa.Instruction + expected map[string]none + seen map[string]none + expectedIntrinsic bool + seenIntrinsic bool + expectedIntrinsicOpcode int + expectedIntrinsicSemantics CoroIntrinsicCallSemantics + expectedElision CoroCallElisionKind + seenElision bool + expectedPhysical coroPhysicalInstructionPlan + hasExpectedPhysical bool + seenSemantic bool + seenPhysical bool + seenPhysicalControl bool + seenPhysicalOperation bool + seenPhysicalOutcome bool + seenPhysicalNilGuard bool + seenPhysicalBoundsGuard bool + observeFrozenSite bool +} + +func (p *context) beginCoroSiteEmission(instruction ssa.Instruction) func() { + return p.beginCoroSiteEmissionMode(instruction, coroRuntimeHelperAtSource) +} + +func (p *context) beginCoroRelocatedSiteEmission(instruction ssa.Instruction, placement coroRuntimeHelperPlacement) func() { + if placement == coroRuntimeHelperAtSource { + panic("relocated coroutine SitePlan emission requires a non-source placement") + } + return p.beginCoroSiteEmissionMode(instruction, placement) +} + +func (p *context) beginCoroSiteEmissionMode(instruction ssa.Instruction, placement coroRuntimeHelperPlacement) func() { + if p == nil || instruction == nil || p.compilation == nil || p.emissionUniverse == nil || + !p.hasCoroPhysicalEmission() || p.rawPlainBody { + return func() {} + } + plan := coroEmissionSitePlan{} + helpers := []string(nil) + if p.emissionUniverse.CompleteRuntimeABI() { + var err error + plan, err = p.emissionUniverse.coroProgramIR.sitePlan(p, instruction) + if err != nil { + panic(fmt.Errorf("coroutine emission site %q: %w", instruction.String(), err)) + } + helpers = plan.managedRuntimeHelpersAt(placement) + } + physical := coroPhysicalInstructionPlan{} + hasPhysical := false + if physicalPlan := p.coroEmissionPlan(); placement == coroRuntimeHelperAtSource && physicalPlan != nil { + var err error + physical, err = physicalPlan.instructionPlan(instruction) + if err != nil { + panic(fmt.Errorf("coroutine emission site %q: %w", instruction.String(), err)) + } + hasPhysical = true + } + filtered := helpers[:0] + for _, helper := range helpers { + if hasPhysical && physical.elidesRuntimeHelper(helper) { + continue + } + filtered = append(filtered, helper) + } + helpers = filtered + observer := &coroSiteEmissionObserver{ + instruction: instruction, + expected: make(map[string]none, len(helpers)), + seen: make(map[string]none, len(helpers)), + observeFrozenSite: p.emissionUniverse.CompleteRuntimeABI(), + } + if hasPhysical { + observer.expectedPhysical = physical + observer.hasExpectedPhysical = true + observer.seenPhysical = physical.recipe == coroPhysicalInstructionOrdinary + observer.seenPhysicalControl = physical.control == coroPhysicalControlNone + observer.seenPhysicalOperation = physical.operation == coroPhysicalOperationNone + observer.seenPhysicalOutcome = physical.outcome == coroPhysicalOutcomeNone + } + if plan.hasCallPlan { + if plan.callPlan.failure != "" { + panic(fmt.Errorf("coroutine emission site %q has an invalid frozen call SitePlan: %s", instruction.String(), plan.callPlan.failure)) + } + callPlan := plan.callPlan.plan + observer.expectedIntrinsic = callPlan.Intrinsic && callPlan.Elision == CoroCallElidedIntrinsic + observer.expectedIntrinsicOpcode = plan.callPlan.opcode + observer.expectedIntrinsicSemantics = callPlan.IntrinsicSemantics + observer.expectedElision = callPlan.Elision + } + for _, helper := range helpers { + observer.expected[helper] = none{} + } + previous := p.coroEmissionSite() + p.setCoroEmissionSite(observer) + return func() { + recovered := recover() + p.setCoroEmissionSite(previous) + if recovered != nil { + panic(recovered) + } + if missing := observer.missing(); len(missing) != 0 { + function := "" + if instruction.Parent() != nil { + function = instruction.Parent().String() + } + physical := "none" + if observer.hasExpectedPhysical { + physical = observer.expectedPhysical.recipe.String() + } + panic(fmt.Errorf( + "coroutine emission site %q in %q with physical recipe %s omitted frozen runtime helper(s) %s", + instruction.String(), function, physical, strings.Join(missing, ", "), + )) + } + if observer.expectedIntrinsic && !observer.seenIntrinsic { + panic(fmt.Errorf( + "coroutine emission site %q omitted frozen intrinsic recipe %d", + instruction.String(), observer.expectedIntrinsicSemantics, + )) + } + if observer.expectedElision != CoroCallNotElided && !observer.seenElision { + panic(fmt.Errorf( + "coroutine emission site %q omitted frozen call elision %d", + instruction.String(), observer.expectedElision, + )) + } + if observer.hasExpectedPhysical && !observer.seenSemantic { + panic(fmt.Errorf( + "coroutine emission site %q omitted frozen semantic recipe %s", + instruction.String(), observer.expectedPhysical.semantic.recipe, + )) + } + if observer.hasExpectedPhysical && !observer.seenPhysical { + panic(fmt.Errorf( + "coroutine emission site %q omitted frozen physical recipe %s", + instruction.String(), observer.expectedPhysical.recipe, + )) + } + if observer.hasExpectedPhysical && !observer.seenPhysicalControl { + panic(fmt.Errorf( + "coroutine emission site %q omitted frozen physical control recipe %s", + instruction.String(), observer.expectedPhysical.control, + )) + } + if observer.hasExpectedPhysical && !observer.seenPhysicalOperation { + panic(fmt.Errorf( + "coroutine emission site %q omitted frozen physical operation recipe %s", + instruction.String(), observer.expectedPhysical.operation, + )) + } + if observer.hasExpectedPhysical && !observer.seenPhysicalOutcome { + panic(fmt.Errorf( + "coroutine emission site %q omitted frozen physical outcome recipe %s", + instruction.String(), observer.expectedPhysical.outcome, + )) + } + if observer.hasExpectedPhysical && observer.expectedPhysical.nilGuard != observer.seenPhysicalNilGuard { + panic(fmt.Errorf( + "coroutine emission site %q physical nil-guard emission=%t, frozen SitePlan requires %t", + instruction.String(), observer.seenPhysicalNilGuard, observer.expectedPhysical.nilGuard, + )) + } + if observer.hasExpectedPhysical && observer.expectedPhysical.boundsGuard != observer.seenPhysicalBoundsGuard { + panic(fmt.Errorf( + "coroutine emission site %q physical bounds-guard emission=%t, frozen SitePlan requires %t", + instruction.String(), observer.seenPhysicalBoundsGuard, observer.expectedPhysical.boundsGuard, + )) + } + } +} + +func (p *context) observeCoroSemanticInstruction(instruction ssa.Instruction) { + observer := p.coroEmissionSite() + if observer == nil { + return + } + if !observer.hasExpectedPhysical || observer.instruction != instruction || observer.expectedPhysical.semantic.recipe == "" { + panic("coroutine semantic recipe emission has no exact physical SitePlan") + } + if observer.seenSemantic { + panic(fmt.Errorf("coroutine emission site %q emitted its semantic recipe more than once", instruction.String())) + } + observer.seenSemantic = true +} + +func (p *context) observeCoroPhysicalNilGuard(instruction ssa.Instruction) { + p.observeCoroPhysicalGuard(instruction, true) +} + +func (p *context) observeCoroPhysicalBoundsGuard(instruction ssa.Instruction) { + p.observeCoroPhysicalGuard(instruction, false) +} + +func (p *context) observeCoroPhysicalGuard(instruction ssa.Instruction, nilGuard bool) { + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical guard emission has no exact source SitePlan") + } + observer := current + expected, seen, name := observer.expectedPhysical.boundsGuard, &observer.seenPhysicalBoundsGuard, "bounds" + if nilGuard { + expected, seen, name = observer.expectedPhysical.nilGuard, &observer.seenPhysicalNilGuard, "nil" + } + if !expected { + panic(fmt.Errorf("coroutine emission site %q emitted an unplanned physical %s guard", instruction.String(), name)) + } + if *seen { + panic(fmt.Errorf("coroutine emission site %q emitted its physical %s guard more than once", instruction.String(), name)) + } + *seen = true +} + +func (p *context) plannedCoroPhysicalInstruction(instruction ssa.Instruction) (coroPhysicalInstructionPlan, bool) { + if p == nil || p.coroEmissionPlan() == nil { + return coroPhysicalInstructionPlan{}, false + } + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical recipe selection has no exact source SitePlan") + } + return current.expectedPhysical, true +} + +func (p *context) plannedCoroPhysicalControl(instruction ssa.Instruction) (coroPhysicalInstructionPlan, bool) { + if p == nil || p.coroEmissionPlan() == nil { + return coroPhysicalInstructionPlan{}, false + } + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical control selection has no exact source SitePlan") + } + return current.expectedPhysical, true +} + +func (p *context) plannedCoroPhysicalOperation(instruction ssa.Instruction) (coroPhysicalInstructionPlan, bool) { + if p == nil || p.coroEmissionPlan() == nil { + return coroPhysicalInstructionPlan{}, false + } + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical operation selection has no exact source SitePlan") + } + return current.expectedPhysical, true +} + +func (p *context) plannedCoroPhysicalOutcome(instruction ssa.Instruction) (coroPhysicalInstructionPlan, bool) { + if p == nil || p.coroEmissionPlan() == nil { + return coroPhysicalInstructionPlan{}, false + } + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical outcome selection has no exact source SitePlan") + } + return current.expectedPhysical, true +} + +func (p *context) observeCoroPhysicalInstruction(instruction ssa.Instruction, actual coroPhysicalInstructionRecipe) { + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical recipe emission has no exact source SitePlan") + } + observer := current + if observer.expectedPhysical.recipe != actual { + panic(fmt.Errorf( + "coroutine emission site %q emitted physical recipe %s, frozen SitePlan requires %s", + instruction.String(), actual, observer.expectedPhysical.recipe, + )) + } + if observer.seenPhysical { + panic(fmt.Errorf("coroutine emission site %q emitted its physical recipe more than once", instruction.String())) + } + observer.seenPhysical = true +} + +func (p *context) observeCoroPhysicalControl(instruction ssa.Instruction, actual coroPhysicalControlRecipe) { + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical control emission has no exact source SitePlan") + } + observer := current + if actual == coroPhysicalControlNone || observer.expectedPhysical.control != actual { + panic(fmt.Errorf( + "coroutine emission site %q emitted physical control recipe %s, frozen SitePlan requires %s", + instruction.String(), actual, observer.expectedPhysical.control, + )) + } + if observer.seenPhysicalControl { + panic(fmt.Errorf("coroutine emission site %q emitted its physical control recipe more than once", instruction.String())) + } + observer.seenPhysicalControl = true +} + +func (p *context) observeCoroPhysicalOperation(instruction ssa.Instruction, actual coroPhysicalOperationRecipe) { + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical operation emission has no exact source SitePlan") + } + observer := current + if actual == coroPhysicalOperationNone || observer.expectedPhysical.operation != actual { + panic(fmt.Errorf( + "coroutine emission site %q emitted physical operation recipe %s, frozen SitePlan requires %s", + instruction.String(), actual, observer.expectedPhysical.operation, + )) + } + if observer.seenPhysicalOperation { + panic(fmt.Errorf("coroutine emission site %q emitted its physical operation recipe more than once", instruction.String())) + } + observer.seenPhysicalOperation = true +} + +func (p *context) observeCoroPhysicalOutcome(instruction ssa.Instruction, actual coroPhysicalOutcomeRecipe) { + current := p.coroEmissionSite() + if current == nil || !current.hasExpectedPhysical || current.instruction != instruction { + panic("coroutine physical outcome emission has no exact source SitePlan") + } + observer := current + if actual == coroPhysicalOutcomeNone || observer.expectedPhysical.outcome != actual { + panic(fmt.Errorf( + "coroutine emission site %q emitted physical outcome recipe %s, frozen SitePlan requires %s", + instruction.String(), actual, observer.expectedPhysical.outcome, + )) + } + if observer.seenPhysicalOutcome { + panic(fmt.Errorf("coroutine emission site %q emitted its physical outcome recipe more than once", instruction.String())) + } + observer.seenPhysicalOutcome = true +} + +func (p *context) observeCoroCallElision(actual CoroCallElisionKind) { + observer := p.coroEmissionSite() + if observer == nil || !observer.observeFrozenSite { + return + } + if observer.expectedElision == CoroCallNotElided || observer.expectedElision != actual { + panic(fmt.Errorf( + "coroutine emission site %q emitted call elision %d, frozen SitePlan requires %d", + observer.instruction.String(), actual, observer.expectedElision, + )) + } + if observer.seenElision { + panic(fmt.Errorf("coroutine emission site %q emitted its call elision more than once", observer.instruction.String())) + } + observer.seenElision = true +} + +func (p *context) plannedCoroCallElision() (CoroCallElisionKind, bool) { + observer := p.coroEmissionSite() + if observer == nil || !observer.observeFrozenSite { + return CoroCallNotElided, false + } + return observer.expectedElision, true +} + +func (p *context) plannedCoroIntrinsicCall(opcode int) (CoroIntrinsicCallSemantics, bool) { + observer := p.coroEmissionSite() + if observer == nil || !observer.observeFrozenSite || !observer.expectedIntrinsic || + observer.expectedIntrinsicOpcode != opcode { + return CoroIntrinsicCallUnsupported, false + } + return observer.expectedIntrinsicSemantics, true +} + +func (p *context) observeCoroIntrinsicCallEmission(opcode int, actual CoroIntrinsicCallSemantics) { + observer := p.coroEmissionSite() + if observer == nil || !observer.observeFrozenSite || !isLLGoIntrinsicInstructionOpcode(opcode) { + return + } + if !observer.expectedIntrinsic { + panic(fmt.Errorf( + "coroutine emission site %q emitted an intrinsic recipe absent from its frozen SitePlan", + observer.instruction.String(), + )) + } + if opcode != observer.expectedIntrinsicOpcode { + panic(fmt.Errorf( + "coroutine emission site %q emitted intrinsic opcode %d, frozen SitePlan requires %d", + observer.instruction.String(), opcode, observer.expectedIntrinsicOpcode, + )) + } + p.observeCoroCallElision(CoroCallElidedIntrinsic) + if actual != observer.expectedIntrinsicSemantics { + panic(fmt.Errorf( + "coroutine emission site %q emitted intrinsic recipe %d, frozen SitePlan requires %d", + observer.instruction.String(), actual, observer.expectedIntrinsicSemantics, + )) + } + if observer.seenIntrinsic { + panic(fmt.Errorf("coroutine emission site %q emitted its intrinsic recipe more than once", observer.instruction.String())) + } + observer.seenIntrinsic = true +} + +func isLLGoIntrinsicInstructionOpcode(opcode int) bool { + for _, candidate := range llgoInstrs { + if candidate == opcode { + return true + } + } + return false +} + +func (p *context) observeCoroSiteRuntimeHelper(helper string) { + observer := p.coroEmissionSite() + if observer == nil || !observer.observeFrozenSite { + return + } + if _, expected := observer.expected[helper]; !expected { + panic(fmt.Errorf( + "coroutine emission site %q emitted runtime helper %q absent from its frozen SitePlan", + observer.instruction.String(), helper, + )) + } + observer.seen[helper] = none{} +} + +func (o *coroSiteEmissionObserver) missing() []string { + if o == nil { + return nil + } + missing := make([]string, 0, len(o.expected)) + for helper := range o.expected { + if _, seen := o.seen[helper]; !seen { + missing = append(missing, helper) + } + } + sort.Strings(missing) + return missing +} diff --git a/cl/coro_site_plan_test.go b/cl/coro_site_plan_test.go new file mode 100644 index 0000000000..52a6694c85 --- /dev/null +++ b/cl/coro_site_plan_test.go @@ -0,0 +1,185 @@ +/* + * 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 ( + "fmt" + "go/ast" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + "golang.org/x/tools/go/ssa" +) + +// loweredRuntimeHelpers keeps older focused tests source-compatible while +// making them exercise the frozen SitePlan rather than the removed raw-SSA +// production classifier. +func (u *EmissionUniverse) loweredRuntimeHelpers(ctx *context, instruction ssa.Instruction) []string { + if u.coroProgramIR == nil { + shape, _ := prepareCoroEmissionFunctionShape(instruction.Parent()) + return u.classifyCoroRuntimeHelpers(ctx, shape, instruction) + } + helpers, err := u.coroProgramIR.plannedRuntimeHelpers(ctx, instruction) + if err != nil { + shape, _ := prepareCoroEmissionFunctionShape(instruction.Parent()) + return u.classifyCoroRuntimeHelpers(ctx, shape, instruction) + } + return helpers +} + +func (u *EmissionUniverse) plainRepresentationRuntimeHelpers(ctx *context, instruction ssa.Instruction) []string { + if u.coroProgramIR == nil { + shape, _ := prepareCoroEmissionFunctionShape(instruction.Parent()) + managed := u.classifyCoroRuntimeHelpers(ctx, shape, instruction) + return u.classifyPlainRuntimeHelpers(ctx, instruction, managed) + } + plan, err := u.coroProgramIR.sitePlan(ctx, instruction) + if err != nil { + shape, _ := prepareCoroEmissionFunctionShape(instruction.Parent()) + managed := u.classifyCoroRuntimeHelpers(ctx, shape, instruction) + return u.classifyPlainRuntimeHelpers(ctx, instruction, managed) + } + return plan.plainRuntimeHelpers +} + +func TestCoroSitePlanConsumersFailClosed(t *testing.T) { + for _, test := range []struct { + name string + mutate func(coroEmissionSitePlan) coroEmissionSitePlan + want string + }{ + { + name: "unexpected actual helper", + mutate: func(plan coroEmissionSitePlan) coroEmissionSitePlan { + plan.managedRuntimeHelpers = nil + return plan + }, + want: "runtime helper capability validation found no lowered helper", + }, + { + name: "missing planned helper", + mutate: func(plan coroEmissionSitePlan) coroEmissionSitePlan { + plan.managedRuntimeHelpers = append(plan.managedRuntimeHelpers, coroPlannedRuntimeHelper{ + name: "UnemittedHelper", + placement: coroRuntimeHelperAtSource, + }) + return plan + }, + want: "operation lowers through unapproved runtime helper UnemittedHelper", + }, + } { + t.Run(test.name, func(t *testing.T) { + fixture := prepareCoroStringConcatTestPlan(t, nil, true) + defer fixture.prog.Dispose() + owners := fixture.universe.sortedUseOwners(fixture.root) + if len(owners) != 1 { + t.Fatalf("Root owners = %d, want 1", len(owners)) + } + key := emissionFunctionOwnerKey{function: fixture.root, owner: owners[0]} + instruction := fixture.concats[0] + plan, ok := fixture.universe.coroProgramIR.sitePlans[key][instruction] + if !ok { + t.Fatal("string concat has no frozen SitePlan") + } + fixture.universe.coroProgramIR.sitePlans[key][instruction] = test.mutate(cloneCoroEmissionSitePlan(plan)) + + message := compileCoroSitePlanFailure(fixture) + if !strings.Contains(message, test.want) { + t.Fatalf("compile failure = %q, want %q", message, test.want) + } + }) + } +} + +func TestCoroSitePlanEmissionObserverRejectsUnexpectedAndMissing(t *testing.T) { + for _, test := range []struct { + name string + run func(*context, ssa.Instruction) + want string + }{ + { + name: "unexpected", + run: func(ctx *context, instruction ssa.Instruction) { + finish := ctx.beginCoroSiteEmission(instruction) + defer finish() + ctx.observeCoroSiteRuntimeHelper("UnplannedHelper") + }, + want: "emitted runtime helper \"UnplannedHelper\" absent from its frozen SitePlan", + }, + { + name: "missing", + run: func(ctx *context, instruction ssa.Instruction) { + ctx.beginCoroSiteEmission(instruction)() + }, + want: "omitted frozen runtime helper(s) StringCat", + }, + } { + t.Run(test.name, func(t *testing.T) { + fixture := prepareCoroStringConcatTestPlan(t, nil, true) + defer fixture.prog.Dispose() + owners := fixture.universe.sortedUseOwners(fixture.root) + if len(owners) != 1 { + t.Fatalf("Root owners = %d, want 1", len(owners)) + } + ctx, err := fixture.universe.functionABIContext(fixture.root, owners[0]) + if err != nil { + t.Fatal(err) + } + ctx.compilation = &Compilation{CoroPlan: fixture.plan, EmissionUniverse: fixture.universe} + ctx.coroEmission = &coroPhysicalEmissionSession{phase: coroPhysicalEmissionPrologue} + message := captureCoroSitePlanPanic(func() { test.run(ctx, fixture.concats[0]) }) + if !strings.Contains(message, test.want) { + t.Fatalf("observer panic = %q, want %q", message, test.want) + } + }) + } +} + +func captureCoroSitePlanPanic(run func()) (message string) { + defer func() { + if recovered := recover(); recovered != nil { + message = fmt.Sprint(recovered) + } + }() + run() + return "" +} + +func compileCoroSitePlanFailure(fixture coroStringConcatTestPlan) (message string) { + defer func() { + if recovered := recover(); recovered != nil { + message = fmt.Sprint(recovered) + } + }() + compilation := &Compilation{CoroPlan: fixture.plan, EmissionUniverse: fixture.universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + for _, pkg := range []emissionTestPackage{fixture.runtimePkg, fixture.fooPkg} { + compiled, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, pkg.ssa, []*ast.File{pkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + return err.Error() + } + compiled.Module().Dispose() + } + return "" +} diff --git a/cl/coro_slice_bounds_test.go b/cl/coro_slice_bounds_test.go new file mode 100644 index 0000000000..0abef621e0 --- /dev/null +++ b/cl/coro_slice_bounds_test.go @@ -0,0 +1,315 @@ +//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 ( + "bytes" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroSliceBoundsFixture = `package foo + +type Bytes []byte +type Array8 [8]byte +const constantDigits = "0123456789abcdef" + +func Slice2(value Bytes, low, high int) Bytes { return value[low:high] } +func Slice2Suffix(value []byte, low int) []byte { return value[low:] } +func Slice2Wide(value []byte, low, high uint64) []byte { return value[low:high] } +func Slice3(value []byte, low, high, max int) []byte { return value[low:high:max] } +func String2(value string, low, high int) string { return value[low:high] } +func StringConst(low, high int) string { return constantDigits[low:high] } +func Pointer2(value *Array8, low, high int) []byte { return value[low:high] } +` + +func TestCoroDynamicSliceBoundsNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, target := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(target.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroSliceBoundsFixture(t, target.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify structured Slice before CoroSplit: %v\n%s", err, module.String()) + } + for _, test := range []struct { + name string + faults int + boundsKind int + minUGT int + }{ + {name: "Slice2", faults: 1, boundsKind: 1, minUGT: 2}, + {name: "Slice2Suffix", faults: 1, boundsKind: 1, minUGT: 2}, + {name: "Slice2Wide", faults: 1, boundsKind: 1, minUGT: 2}, + {name: "Slice3", faults: 1, boundsKind: 1, minUGT: 3}, + {name: "String2", faults: 1, boundsKind: 1, minUGT: 2}, + {name: "StringConst", faults: 1, boundsKind: 1, minUGT: 2}, + {name: "Pointer2", faults: 2, boundsKind: 1, minUGT: 2}, + } { + function := functions[test.name] + functionPlan, ok := plan.FunctionPlan(function) + if !ok || functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want may-unwind coroutine", test.name, functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo."+test.name).String() + if got := strings.Count(body, "call void @"+coroFaultPrepareHookV1); got != test.faults { + t.Fatalf("%s fault prepare calls = %d, want %d:\n%s", test.name, got, test.faults, body) + } + if got := strings.Count(body, "icmp ugt"); got < test.minUGT { + t.Fatalf("%s inclusive bounds comparisons = %d, want at least %d:\n%s", test.name, got, test.minUGT, body) + } + for _, helper := range []string{"StringSlice2", "NewSlice2", "NewSlice3Bounds"} { + if strings.Contains(body, helper) { + t.Fatalf("%s retained native-stack helper %s:\n%s", test.name, helper, body) + } + } + if got := strings.Count(body, "i32 2"); got < test.boundsKind { + t.Fatalf("%s did not select the index/slice-bounds fault kind:\n%s", test.name, body) + } + if hook, aggregate := strings.Index(body, "call void @"+coroFaultPrepareHookV1), strings.LastIndex(body, "insertvalue"); hook < 0 || aggregate < hook { + t.Fatalf("%s constructed its result before the terminal bounds edge:\n%s", test.name, body) + } + } + + runCoroABITestPipeline(t, prog, module) + for name := range functions { + resume := module.NamedFunction("foo." + name + "$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), coroFaultPrepareHookV1) { + t.Fatalf("post-split %s resume lost its structured slice fault edge:\n%s", name, module.String()) + } + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit structured Slice object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroFaultPrepareHookV1)) { + t.Fatal("post-CoroSplit object lost the structured slice fault hook") + } + }) + } +} + +func TestCoroDynamicSliceBoundsFailClosed(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroSliceBoundsFixture) + for _, name := range []string{"Slice2", "Slice3", "String2", "StringConst"} { + function := ssaPkg.Func(name) + slice := coroOnlySliceInstruction(t, function) + audit := &coroPhysicalPureSSAAudit{ + fn: function, + reachableBlocks: coroPhysicalConstantReachableBlocks(function), + } + if reason := audit.validateSlice(slice); !strings.Contains(reason, "explicit-status panic ABI") { + t.Fatalf("%s legacy rejection = %q", name, reason) + } + } + + function := ssaPkg.Func("Slice3") + slice := coroOnlySliceInstruction(t, function) + audit := &coroPhysicalPureSSAAudit{ + fn: function, + reachableBlocks: coroPhysicalConstantReachableBlocks(function), + allowImplicitNilFault: true, + } + high := slice.High + slice.High = nil + defer func() { slice.High = high }() + if reason := audit.validateSlice(slice); !strings.Contains(reason, "requires explicit high and max") { + t.Fatalf("malformed slice3 rejection = %q", reason) + } +} + +func TestCoroDynamicSliceBoundsGoRuleMatrix(t *testing.T) { + storage := make([]byte, 2, 4) + for _, test := range []struct { + name string + low, high int + wantPanic bool + wantLenCap [2]int + }{ + {name: "length view", low: 0, high: 2, wantLenCap: [2]int{2, 4}}, + {name: "two-index uses cap", low: 1, high: 4, wantLenCap: [2]int{3, 3}}, + {name: "empty cap suffix", low: 4, high: 4, wantLenCap: [2]int{0, 0}}, + {name: "negative low", low: -1, high: 0, wantPanic: true}, + {name: "high above cap", low: 0, high: 5, wantPanic: true}, + {name: "low above high", low: 3, high: 2, wantPanic: true}, + } { + t.Run("slice2/"+test.name, func(t *testing.T) { + result, panicked := recoverSlice2(storage, test.low, test.high) + if panicked != test.wantPanic { + t.Fatalf("panic = %t, want %t", panicked, test.wantPanic) + } + if !panicked && [2]int{len(result), cap(result)} != test.wantLenCap { + t.Fatalf("len/cap = %v, want %v", [2]int{len(result), cap(result)}, test.wantLenCap) + } + }) + } + + for _, test := range []struct { + name string + low, high, max int + wantPanic bool + wantLength, cap int + }{ + {name: "cap extension", low: 1, high: 3, max: 4, wantLength: 2, cap: 3}, + {name: "max above cap", low: 0, high: 2, max: 5, wantPanic: true}, + {name: "high above max", low: 0, high: 4, max: 3, wantPanic: true}, + {name: "low above high", low: 3, high: 2, max: 4, wantPanic: true}, + } { + t.Run("slice3/"+test.name, func(t *testing.T) { + result, panicked := recoverSlice3(storage, test.low, test.high, test.max) + if panicked != test.wantPanic { + t.Fatalf("panic = %t, want %t", panicked, test.wantPanic) + } + if !panicked && (len(result) != test.wantLength || cap(result) != test.cap) { + t.Fatalf("len/cap = %d/%d, want %d/%d", len(result), cap(result), test.wantLength, test.cap) + } + }) + } + + if _, panicked := recoverStringSlice("ab", 0, 3); !panicked { + t.Fatal("string slice accepted high above len") + } + if _, panicked := recoverWideSlice(storage, 0, ^uint64(0)); !panicked { + t.Fatal("slice accepted a uint64 bound that cannot fit target int") + } +} + +func compileCoroSliceBoundsFixture( + t *testing.T, + target *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, map[string]*ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroSliceBoundsFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := make(map[string]*ssa.Function) + var roots coro.Roots + for _, name := range []string{"Slice2", "Slice2Suffix", "Slice2Wide", "Slice3", "String2", "StringConst", "Pointer2"} { + function := ssaPkg.Func(name) + functions[name] = function + roots = append(roots, coro.Root{Function: function, Demand: coro.AsyncDemand}) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if _, ok := functions[function.Name()]; ok && functions[function.Name()] == function { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, functions +} + +func coroOnlySliceInstruction(t *testing.T, function *ssa.Function) *ssa.Slice { + t.Helper() + var found *ssa.Slice + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + candidate, ok := instruction.(*ssa.Slice) + if !ok { + continue + } + if found != nil { + t.Fatalf("%s has more than one Slice instruction", function) + } + found = candidate + } + } + if found == nil { + t.Fatalf("%s has no Slice instruction", function) + } + return found +} + +func recoverSlice2(value []byte, low, high int) (result []byte, panicked bool) { + defer func() { panicked = recover() != nil }() + result = value[low:high] + return +} + +func recoverSlice3(value []byte, low, high, max int) (result []byte, panicked bool) { + defer func() { panicked = recover() != nil }() + result = value[low:high:max] + return +} + +func recoverStringSlice(value string, low, high int) (result string, panicked bool) { + defer func() { panicked = recover() != nil }() + result = value[low:high] + return +} + +func recoverWideSlice(value []byte, low, high uint64) (result []byte, panicked bool) { + defer func() { panicked = recover() != nil }() + result = value[low:high] + return +} diff --git a/cl/coro_slice_managed_test.go b/cl/coro_slice_managed_test.go new file mode 100644 index 0000000000..4df565d147 --- /dev/null +++ b/cl/coro_slice_managed_test.go @@ -0,0 +1,355 @@ +//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 ( + "bytes" + "go/ast" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroManagedSliceRuntimeFixture = `package runtime +import "unsafe" + +type Slice struct { + Data unsafe.Pointer + Len int + Cap int +} + +func MakeSlice(length, capacity, elementSize int) Slice { + return Slice{nil, length, capacity} +} + +func SliceAppend(source Slice, data unsafe.Pointer, count, elementSize int) Slice { + source.Len += count + return source +} +` + +const coroManagedSliceFixture = `package foo + +func Root(source []byte, length, capacity int) []byte { + result := make([]byte, length, capacity) + return append(result, source...) +} +` + +type coroManagedSlicePlanOptions struct { + outcome coro.OutcomeMode + loweredCalls bool + forceRootCoro bool +} + +type coroManagedSliceTestPlan struct { + prog llssa.Program + runtimePkg emissionTestPackage + fooPkg emissionTestPackage + universe *EmissionUniverse + plan *coro.SSAPlan + root *ssa.Function + makeSlice *ssa.MakeSlice + appendCall *ssa.Call +} + +func TestCoroManagedSliceHelpersNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + fixture := prepareCoroManagedSliceTestPlan(t, test.target, coroManagedSlicePlanOptions{ + outcome: coro.OutcomeExplicitStatus, + loweredCalls: true, + }) + defer fixture.prog.Dispose() + + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + if reason := audit.validateMakeSlice(fixture.makeSlice); reason != "" { + t.Fatalf("MakeSlice rejected: %s", reason) + } + if reason := audit.validateAppendBuiltin(fixture.appendCall); reason != "" { + t.Fatalf("append rejected: %s", reason) + } + for name, helper := range map[string]*ssa.Function{ + "MakeSlice": fixture.runtimePkg.ssa.Func("MakeSlice"), + "SliceAppend": fixture.runtimePkg.ssa.Func("SliceAppend"), + } { + plan, ok := fixture.plan.FunctionPlan(helper) + if !ok || plan.External != coro.Defined || plan.Emission != coro.EmitCoroutine || + plan.Primary != coro.PrimaryCoroutine || plan.FuncRep != coro.DirectCoro || + !plan.Demand.Contains(coro.AsyncDemand) || !plan.Effect.Contains(coro.OutcomeStructured) || + !plan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want demanded ExplicitStatus coroutine", name, plan, ok) + } + } + + compilation := &Compilation{CoroPlan: fixture.plan, EmissionUniverse: fixture.universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + runtimeLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.runtimePkg.ssa, []*ast.File{fixture.runtimePkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile runtime helpers: %v", err) + } + runtimeModule := runtimeLL.Module() + defer runtimeModule.Dispose() + fooLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.fooPkg.ssa, []*ast.File{fixture.fooPkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile append owner: %v", err) + } + fooModule := fooLL.Module() + defer fooModule.Dispose() + for name, module := range map[string]llvm.Module{"runtime": runtimeModule, "foo": fooModule} { + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify %s before CoroSplit: %v\n%s", name, err, module.String()) + } + } + rootIR := requireCoroPhysicalFunction(t, fooModule, "foo.Root").String() + for _, required := range []string{ + "runtime.MakeSlice$coro", + "runtime.SliceAppend$coro", + "call void @" + coroAwaitPrepareHookV1, + "call i32 @" + coroAwaitConsumeHookV1, + } { + if !strings.Contains(rootIR, required) { + t.Fatalf("managed slice owner lacks %q:\n%s", required, rootIR) + } + } + if got := strings.Count(rootIR, "call void @"+coroAwaitPrepareHookV1); got != 2 { + t.Fatalf("managed slice awaits = %d, want MakeSlice + SliceAppend:\n%s", got, rootIR) + } + + for _, module := range []llvm.Module{runtimeModule, fooModule} { + runCoroABITestPipeline(t, fixture.prog, module) + object, err := fixture.prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit object: %v\n%s", err, module.String()) + } + if len(object.Bytes()) == 0 { + object.Dispose() + t.Fatal("post-CoroSplit managed slice object is empty") + } + object.Dispose() + } + if !bytes.Contains([]byte(fooModule.String()), []byte("foo.Root$coro.resume")) { + t.Fatalf("CoroSplit lost the managed slice resume entry:\n%s", fooModule.String()) + } + }) + } +} + +func TestCoroManagedSliceHelpersFailClosed(t *testing.T) { + t.Run("explicit status required", func(t *testing.T) { + fixture := prepareCoroManagedSliceTestPlan(t, nil, coroManagedSlicePlanOptions{ + outcome: coro.OutcomeExplicitStatus, + loweredCalls: true, + }) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + for name, reason := range map[string]string{ + "append": audit.validateAppendBuiltin(fixture.appendCall), + "MakeSlice": audit.validateMakeSlice(fixture.makeSlice), + } { + if !strings.Contains(reason, "explicit-status panic ABI") { + t.Fatalf("%s rejection = %q", name, reason) + } + } + }) + + t.Run("lowered fact required", func(t *testing.T) { + fixture := prepareCoroManagedSliceTestPlan(t, nil, coroManagedSlicePlanOptions{ + outcome: coro.OutcomeExplicitStatus, + }) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + for name, reason := range map[string]string{ + "append": audit.validateAppendBuiltin(fixture.appendCall), + "MakeSlice": audit.validateMakeSlice(fixture.makeSlice), + } { + if !strings.Contains(reason, "exact coroutine-safe lowered-call plan") { + t.Fatalf("%s missing-fact rejection = %q", name, reason) + } + } + }) + + t.Run("plain MayUnwind helper rejected", func(t *testing.T) { + fixture := prepareCoroManagedSliceTestPlan(t, nil, coroManagedSlicePlanOptions{ + loweredCalls: true, + forceRootCoro: true, + }) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + for name, reason := range map[string]string{ + "append": audit.validateAppendBuiltin(fixture.appendCall), + "MakeSlice": audit.validateMakeSlice(fixture.makeSlice), + } { + if !strings.Contains(reason, "exact coroutine-safe lowered-call plan") { + t.Fatalf("%s plain-MayUnwind rejection = %q", name, reason) + } + } + }) + + t.Run("malformed append shape", func(t *testing.T) { + fixture := prepareCoroManagedSliceTestPlan(t, nil, coroManagedSlicePlanOptions{ + outcome: coro.OutcomeExplicitStatus, + loweredCalls: true, + }) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + args := fixture.appendCall.Call.Args + fixture.appendCall.Call.Args = args[:1] + defer func() { fixture.appendCall.Call.Args = args }() + if reason := audit.validateAppendBuiltin(fixture.appendCall); !strings.Contains(reason, "invalid argument/result shape") { + t.Fatalf("malformed append rejection = %q", reason) + } + }) +} + +func prepareCoroManagedSliceTestPlan( + t *testing.T, target *llssa.Target, options coroManagedSlicePlanOptions, +) coroManagedSliceTestPlan { + t.Helper() + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, coroManagedSliceRuntimeFixture) + fooPkg := testProg.addPackage(t, "foo", coroManagedSliceFixture) + testProg.ssa.Build() + + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + prog.SetRuntime(runtimePkg.types) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: fooPkg.ssa, Files: []*ast.File{fooPkg.file}}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(fooPkg.ssa.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := fooPkg.ssa.Func("Root") + makeSlice, appendCall := coroManagedSliceInstructions(t, root) + makeSliceHelper := runtimePkg.ssa.Func("MakeSlice") + appendHelper := runtimePkg.ssa.Func("SliceAppend") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + config := coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + OutcomeMode: options.outcome, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == makeSliceHelper || fn == appendHelper { + return coro.SSAFunctionPolicy{Exec: coro.MayUnwind}, nil + } + if fn == root && options.forceRootCoro { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + } + if options.loweredCalls { + config.ClassifyLoweredCalls = universe.CoroLoweredCalls + } + plan, err := coro.AnalyzeSSA(fooPkg.ssa.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, config) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return coroManagedSliceTestPlan{ + prog: prog, + runtimePkg: runtimePkg, + fooPkg: fooPkg, + universe: universe, + plan: plan, + root: root, + makeSlice: makeSlice, + appendCall: appendCall, + } +} + +func coroManagedSliceInstructions(t *testing.T, root *ssa.Function) (*ssa.MakeSlice, *ssa.Call) { + t.Helper() + var makeSlice *ssa.MakeSlice + var appendCall *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + switch instruction := instruction.(type) { + case *ssa.MakeSlice: + makeSlice = instruction + case *ssa.Call: + if builtin, ok := instruction.Call.Value.(*ssa.Builtin); ok && builtin.Name() == "append" { + appendCall = instruction + } + } + } + } + if makeSlice == nil || appendCall == nil { + t.Fatalf("managed slice fixture lacks MakeSlice/append:\n%s", root.String()) + } + return makeSlice, appendCall +} diff --git a/cl/coro_slice_to_array.go b/cl/coro_slice_to_array.go new file mode 100644 index 0000000000..8bed554064 --- /dev/null +++ b/cl/coro_slice_to_array.go @@ -0,0 +1,117 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// coroSliceToArrayPointerShape validates the exact language conversion shape +// shared by helper inventory, physical-ABI preflight, frame retention, and +// code generation. Keeping the array length derived from the result type +// avoids an instruction-name or source-pattern exception. +func coroSliceToArrayPointerShape(source, result types.Type) (*types.Array, string) { + slice, ok := types.Unalias(source).Underlying().(*types.Slice) + if !ok { + return nil, "source is not a slice" + } + pointer, ok := types.Unalias(result).Underlying().(*types.Pointer) + if !ok { + return nil, "result is not a pointer" + } + array, ok := types.Unalias(pointer.Elem()).Underlying().(*types.Array) + if !ok { + return nil, "result does not point to an array" + } + if !types.Identical(slice.Elem(), array.Elem()) { + return nil, "slice and array element types differ" + } + return array, "" +} + +func coroSliceToArrayPointerLen(conversion *ssa.SliceToArrayPointer, typeOf func(types.Type) types.Type) (int64, bool) { + if conversion == nil || conversion.X == nil || conversion.Type() == nil { + return 0, false + } + source, result := conversion.X.Type(), conversion.Type() + if typeOf != nil { + source, result = typeOf(source), typeOf(result) + } + array, reason := coroSliceToArrayPointerShape(source, result) + if reason != "" { + return 0, false + } + return array.Len(), true +} + +// coroSliceToArrayValueDeref recognizes the synthetic load used by x/tools for +// the value conversion [N]T(s). The preceding SliceToArrayPointer owns the +// length fault. Consequently N>0 is non-nil on its continuation, while N==0 +// must not acquire a spurious nil fault for the legal nil-slice conversion. +func coroSliceToArrayValueDeref(deref *ssa.UnOp, typeOf func(types.Type) types.Type) (*ssa.SliceToArrayPointer, int64, bool) { + if deref == nil || deref.Op != token.MUL || deref.X == nil { + return nil, 0, false + } + conversion, ok := deref.X.(*ssa.SliceToArrayPointer) + if !ok || conversion.Type() == nil || deref.Type() == nil || + conversion.Pos() != token.NoPos || deref.Pos() == token.NoPos { + return nil, 0, false + } + pointerType, valueType := conversion.Type(), deref.Type() + if typeOf != nil { + pointerType, valueType = typeOf(pointerType), typeOf(valueType) + } + pointer, ok := types.Unalias(pointerType).Underlying().(*types.Pointer) + if !ok || !types.Identical(pointer.Elem(), valueType) { + return nil, 0, false + } + length, exact := coroSliceToArrayPointerLen(conversion, typeOf) + return conversion, length, exact +} + +func (p *context) compileCoroSliceToArrayPointer( + b llssa.Builder, + conversion *ssa.SliceToArrayPointer, + x llssa.Expr, + typ llssa.Type, + plan coroPhysicalInstructionPlan, +) llssa.Expr { + body := p.coroBody() + if body == nil || conversion == nil || b == nil || b.Func != p.fn { + panic("structured slice-to-array-pointer conversion escaped its physical coroutine body") + } + if !p.coroEmissionExplicitStatus() || + body.abi.version < coroPhysicalABIVersionV1 { + panic("slice-to-array-pointer fault requires the PhysicalABIV1 explicit-status panic ABI") + } + if plan.recipe != coroPhysicalInstructionSliceToArrayPointer || plan.bound < 0 || + plan.boundsGuard != (plan.bound != 0) { + panic(fmt.Sprintf("invalid frozen slice-to-array-pointer recipe for %s", conversion)) + } + if plan.boundsGuard { + p.observeCoroPhysicalBoundsGuard(conversion) + limit := b.Prog.IntVal(uint64(plan.bound), b.Prog.Int()) + tooShort := b.BinOp(token.LSS, b.SliceLen(x), limit) + p.compileCoroFaultConditionGuard(b, tooShort, coroFaultSliceConvertV1) + } + return b.SliceToArrayPointerUnchecked(x, typ) +} diff --git a/cl/coro_slice_to_array_test.go b/cl/coro_slice_to_array_test.go new file mode 100644 index 0000000000..79fd314702 --- /dev/null +++ b/cl/coro_slice_to_array_test.go @@ -0,0 +1,403 @@ +//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 ( + "bytes" + "fmt" + "go/token" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroSliceToArrayFixture = `package foo + +type Octet byte +type Octets []Octet +type FourOctets [4]Octet + +func Pointer4(value []byte) *[4]byte { return (*[4]byte)(value) } +func Value4(value []byte) [4]byte { return [4]byte(value) } +func ExplicitValue4(value []byte) [4]byte { return *(*[4]byte)(value) } +func Pointer0(value []byte) *[0]byte { return (*[0]byte)(value) } +func Value0(value []byte) [0]byte { return [0]byte(value) } +func ExplicitValue0(value []byte) [0]byte { return *(*[0]byte)(value) } +func GuardedExplicitValue0(value []byte) [0]byte { + pointer := (*[0]byte)(value) + if pointer == nil { return [0]byte{} } + return *pointer +} +func NamedPointer4(value Octets) *FourOctets { return (*FourOctets)(value) } +` + +func TestCoroSliceToArrayPointerNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, target := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(target.name, func(t *testing.T) { + prog, pkg, universe, plan, functions := compileCoroSliceToArrayFixture(t, target.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify slice-to-array conversion before CoroSplit: %v\n%s", err, module.String()) + } + for _, name := range []string{"Pointer4", "Value4", "ExplicitValue4", "NamedPointer4"} { + function := functions[name] + functionPlan, ok := plan.FunctionPlan(function) + if !ok || functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want may-unwind coroutine", name, functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + requireCoroSliceToArrayFault(t, name, body, coroFaultSliceConvertV1) + } + + for _, name := range []string{"Pointer0", "GuardedExplicitValue0"} { + functionPlan, ok := plan.FunctionPlan(functions[name]) + if !ok || functionPlan.Emission != coro.EmitCoroutine || functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want exact no-unwind coroutine", name, functionPlan, ok) + } + } + explicit0Plan, ok := plan.FunctionPlan(functions["ExplicitValue0"]) + if !ok || !explicit0Plan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("ExplicitValue0 plan = %+v, present=%t; want nullable explicit deref", explicit0Plan, ok) + } + for _, name := range []string{"Pointer0", "Value0", "GuardedExplicitValue0"} { + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if strings.Contains(body, coroFaultPrepareHookV1) || strings.Contains(body, "PanicSliceConvert") || + strings.Contains(body, "AssertNilDeref") { + t.Fatalf("%s retained a zero-length fault edge:\n%s", name, body) + } + } + pointer0 := requireCoroPhysicalFunction(t, module, "foo.Pointer0").String() + if !strings.Contains(pointer0, "extractvalue") { + t.Fatalf("Pointer0 did not preserve the input slice data projection:\n%s", pointer0) + } + explicit0 := requireCoroPhysicalFunction(t, module, "foo.ExplicitValue0").String() + requireCoroSliceToArrayFault(t, "ExplicitValue0", explicit0, coroFaultNilV1) + if strings.Contains(explicit0, "i32 10") { + t.Fatalf("ExplicitValue0 incorrectly used the slice-length fault:\n%s", explicit0) + } + + for name, function := range functions { + conversion := coroOnlySliceToArrayPointer(function) + if conversion == nil { + if name != "Value0" { + t.Fatalf("%s fixture has no SliceToArrayPointer", name) + } + continue + } + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, function, CoroFrameRetentionParkABIV2) + if err != nil { + t.Fatalf("%s audit: %v", name, err) + } + helpers := strings.Join(universe.loweredRuntimeHelpers(audit.ctx, conversion), ",") + length, exact := coroSliceToArrayPointerLen(conversion, audit.typeOf) + if !exact { + t.Fatalf("%s conversion has no exact array length", name) + } + if length == 0 && helpers != "" || length != 0 && helpers != "PanicSliceConvert" { + t.Fatalf("%s length=%d helpers=%q", name, length, helpers) + } + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{"Pointer4", "Value4", "ExplicitValue4", "NamedPointer4"} { + resume := module.NamedFunction("foo." + name + "$coro.resume") + if resume.IsNil() { + t.Fatalf("post-split %s has no resume function", name) + } + requireCoroSliceToArrayFault(t, name+" resume", resume.String(), coroFaultSliceConvertV1) + } + for _, name := range []string{"Pointer0", "Value0", "GuardedExplicitValue0"} { + resume := module.NamedFunction("foo." + name + "$coro.resume") + if resume.IsNil() || strings.Contains(resume.String(), coroFaultPrepareHookV1) { + t.Fatalf("post-split %s acquired a zero-length fault edge:\n%s", name, module.String()) + } + } + explicit0Resume := module.NamedFunction("foo.ExplicitValue0$coro.resume") + if explicit0Resume.IsNil() { + t.Fatal("post-split ExplicitValue0 has no resume function") + } + requireCoroSliceToArrayFault(t, "ExplicitValue0 resume", explicit0Resume.String(), coroFaultNilV1) + + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit slice-to-array conversion object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroFaultPrepareHookV1)) { + t.Fatal("post-CoroSplit object lost the slice-to-array fault hook") + } + }) + } +} + +func TestCoroSliceToArrayPointerSSAAndFailClosedBoundary(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroSliceToArrayFixture) + for _, name := range []string{"Pointer4", "Pointer0"} { + conversion := coroOnlySliceToArrayPointer(ssaPkg.Func(name)) + if conversion == nil || conversion.Pos() == token.NoPos { + t.Fatalf("%s is not an explicit pointer conversion: %v", name, conversion) + } + } + for _, name := range []string{"Value4", "ExplicitValue4", "ExplicitValue0", "GuardedExplicitValue0"} { + function := ssaPkg.Func(name) + conversion := coroOnlySliceToArrayPointer(function) + deref := coroOnlySliceToArrayDeref(function) + if conversion == nil || deref == nil { + t.Fatalf("%s lacks conversion/deref shape:\n%s", name, function.String()) + } + _, _, synthetic := coroSliceToArrayValueDeref(deref, nil) + wantSynthetic := name == "Value4" + if synthetic != wantSynthetic { + t.Fatalf("%s synthetic deref = %t, want %t (conversion pos=%v, deref pos=%v)", + name, synthetic, wantSynthetic, conversion.Pos(), deref.Pos()) + } + } + if conversion := coroOnlySliceToArrayPointer(ssaPkg.Func("Value0")); conversion != nil { + t.Fatalf("[0]byte(value) unexpectedly emitted %s", conversion) + } + + for _, test := range []struct { + name string + wantFail bool + }{ + {name: "Pointer4", wantFail: true}, + {name: "Pointer0"}, + } { + function := ssaPkg.Func(test.name) + conversion := coroOnlySliceToArrayPointer(function) + audit := &coroPhysicalPureSSAAudit{ + fn: function, + reachableBlocks: coroPhysicalConstantReachableBlocks(function), + } + reason := audit.validateSliceToArrayPointer(conversion) + if test.wantFail && !strings.Contains(reason, "explicit-status panic ABI") { + t.Fatalf("%s legacy rejection = %q", test.name, reason) + } + if !test.wantFail && reason != "" { + t.Fatalf("%s zero-length conversion rejected: %s", test.name, reason) + } + } +} + +func TestSliceToArrayPointerZeroLengthPlainLowering(t *testing.T) { + llssa.Initialize(llssa.InitAll) + ssaPkg, _, files := buildGoSSAPkg(t, coroSliceToArrayFixture) + prog := newLLSSAProg(t) + defer prog.Dispose() + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + pointer0 := module.NamedFunction("foo.Pointer0") + if pointer0.IsNil() || strings.Contains(pointer0.String(), "PanicSliceConvert") { + t.Fatalf("plain Pointer0 retained PanicSliceConvert:\n%s", module.String()) + } + pointer4 := module.NamedFunction("foo.Pointer4") + if pointer4.IsNil() || !strings.Contains(pointer4.String(), "PanicSliceConvert") { + t.Fatalf("plain Pointer4 lost its checked lowering:\n%s", module.String()) + } +} + +func TestSliceToArrayPointerZeroLengthReferenceSemantics(t *testing.T) { + var nilSlice []byte + if pointer := (*[0]byte)(nilSlice); pointer != nil { + t.Fatalf("nil slice converted to non-nil *[0]byte: %p", pointer) + } + empty := make([]byte, 0) + if pointer := (*[0]byte)(empty); pointer == nil { + t.Fatal("empty non-nil slice converted to nil *[0]byte") + } + if panicked := func() (panicked bool) { + defer func() { panicked = recover() != nil }() + _ = *(*[0]byte)(nilSlice) + return false + }(); !panicked { + t.Fatal("explicit dereference of nil *[0]byte did not panic") + } + _ = [0]byte(nilSlice) + + shortWithCapacity := make([]byte, 2, 4) + for _, convert := range []struct { + name string + call func() + }{ + {name: "pointer", call: func() { _ = (*[4]byte)(shortWithCapacity) }}, + {name: "value", call: func() { _ = [4]byte(shortWithCapacity) }}, + } { + if panicked := func() (panicked bool) { + defer func() { panicked = recover() != nil }() + convert.call() + return false + }(); !panicked { + t.Fatalf("%s conversion used cap instead of len", convert.name) + } + } + + storage := []byte{1, 2, 3, 4} + pointer4 := (*[4]byte)(storage) + pointer4[0] = 9 + if storage[0] != 9 { + t.Fatal("slice-to-array-pointer conversion did not alias the backing storage") + } + value4 := [4]byte(storage) + value4[0] = 7 + if storage[0] != 9 { + t.Fatal("slice-to-array-value conversion did not copy the array value") + } +} + +func requireCoroSliceToArrayFault(t *testing.T, name, body string, kind uint32) { + t.Helper() + if got := strings.Count(body, "call void @"+coroFaultPrepareHookV1); got != 1 { + t.Fatalf("%s fault prepare calls = %d, want one:\n%s", name, got, body) + } + if strings.Contains(body, "PanicSliceConvert") || strings.Contains(body, "AssertNilDeref") { + t.Fatalf("%s retained a native-stack fault helper:\n%s", name, body) + } + hook := strings.Index(body, "call void @"+coroFaultPrepareHookV1) + line := body[hook:] + if end := strings.IndexByte(line, '\n'); end >= 0 { + line = line[:end] + } + if !strings.Contains(line, fmt.Sprintf("i32 %d", kind)) { + t.Fatalf("%s selected the wrong fault kind; hook=%q", name, line) + } +} + +func coroOnlySliceToArrayPointer(function *ssa.Function) *ssa.SliceToArrayPointer { + if function == nil { + return nil + } + var found *ssa.SliceToArrayPointer + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + if conversion, ok := instruction.(*ssa.SliceToArrayPointer); ok { + if found != nil { + return nil + } + found = conversion + } + } + } + return found +} + +func coroOnlySliceToArrayDeref(function *ssa.Function) *ssa.UnOp { + if function == nil { + return nil + } + var found *ssa.UnOp + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + deref, ok := instruction.(*ssa.UnOp) + if !ok || deref.Op != token.MUL { + continue + } + if _, conversion := deref.X.(*ssa.SliceToArrayPointer); !conversion || found != nil { + continue + } + found = deref + } + } + return found +} + +func compileCoroSliceToArrayFixture( + t *testing.T, + target *llssa.Target, +) (llssa.Program, llssa.Package, *EmissionUniverse, *coro.SSAPlan, map[string]*ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroSliceToArrayFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := make(map[string]*ssa.Function) + var roots coro.Roots + for _, name := range []string{ + "Pointer4", "Value4", "ExplicitValue4", "Pointer0", "Value0", "ExplicitValue0", "GuardedExplicitValue0", "NamedPointer4", + } { + function := ssaPkg.Func(name) + functions[name] = function + roots = append(roots, coro.Root{Function: function, Demand: coro.AsyncDemand}) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if root, ok := functions[function.Name()]; ok && root == function { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, universe, plan, functions +} diff --git a/cl/coro_spawn.go b/cl/coro_spawn.go new file mode 100644 index 0000000000..b5dd1b247f --- /dev/null +++ b/cl/coro_spawn.go @@ -0,0 +1,294 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +func coroSpawnBeginSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + return types.NewSignatureType(nil, nil, nil, + types.NewTuple(types.NewParam(token.NoPos, nil, "parent", pointer)), + types.NewTuple(types.NewParam(token.NoPos, nil, "child", pointer)), + false, + ) +} + +func coroSpawnCommitSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + return types.NewSignatureType(nil, nil, nil, + types.NewTuple( + types.NewParam(token.NoPos, nil, "parent", pointer), + types.NewParam(token.NoPos, nil, "child", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + ), nil, false, + ) +} + +func resolveCoroDirectStaticSpawn( + plan *coro.SSAPlan, + spawn *ssa.Go, + managedDispatch bool, +) (*ssa.Function, coro.FunctionPlan, error) { + if plan == nil || spawn == nil || spawn.Common() == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("requires a compilation CallPlan") + } + callPlan, found := plan.CallPlan(spawn) + if !found { + return nil, coro.FunctionPlan{}, fmt.Errorf("spawn has no compilation CallPlan") + } + if callPlan.Transport == coro.RawCCodePointer { + return nil, coro.FunctionPlan{}, fmt.Errorf("raw C code-pointer callee cannot be spawned through the managed coroutine scheduler") + } + target, targetPlan, directErr := plan.ResolveClosedStaticSpawn(spawn) + if directErr == nil { + if err := validateCoroDirectSpawnArgumentTransport(plan, spawn, target, managedDispatch); err != nil { + return nil, coro.FunctionPlan{}, err + } + return target, targetPlan, nil + } + common := spawn.Common() + raw, direct := common.Value.(*ssa.Function) + if direct && raw != nil && raw.Signature != nil && raw.Signature.Recv() == nil { + return nil, coro.FunctionPlan{}, directErr + } + if !direct || raw == nil || common.StaticCallee() != raw || common.IsInvoke() || common.Method != nil || + raw.Signature == nil || raw.Signature.Recv() == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("requires an exact static function or method operand") + } + if callPlan.Kind != coro.CallSpawn || callPlan.Rep != coro.DirectCoro || + callPlan.Open || callPlan.MayBeNil || len(callPlan.Targets) != 1 { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "requires one closed non-nil DirectCoro spawn target, got kind=%v representation=%s open=%t may-be-nil=%t targets=%d", + callPlan.Kind, callPlan.Rep, callPlan.Open, callPlan.MayBeNil, len(callPlan.Targets), + ) + } + target, found = plan.Function(callPlan.Targets[0]) + if !found || target == nil { + return nil, coro.FunctionPlan{}, fmt.Errorf("spawn target %q is absent from the compilation plan", callPlan.Targets[0]) + } + targetPlan, found = plan.FunctionPlan(target) + if !found || targetPlan.ID != callPlan.Targets[0] || targetPlan.External != coro.Defined || + targetPlan.Emission != coro.EmitCoroutine || targetPlan.Primary != coro.PrimaryCoroutine || + targetPlan.FuncRep != coro.DirectCoro || targetPlan.Demand != coro.AsyncDemand || + !targetPlan.Effect.Contains(coro.YieldOnly) { + return nil, coro.FunctionPlan{}, fmt.Errorf( + "spawn method target %q is not one demanded preemptible direct coroutine (external=%s emission=%s primary=%s representation=%s demand=%s effect=%s)", + callPlan.Targets[0], targetPlan.External, targetPlan.Emission, targetPlan.Primary, + targetPlan.FuncRep, targetPlan.Demand, targetPlan.Effect, + ) + } + if target.Signature == nil || target.Signature.Recv() == nil || target.Signature.Variadic() || + target.Signature.Results().Len() != 0 || typeParamCount(target.Signature.TypeParams()) != 0 || + typeParamCount(target.Signature.RecvTypeParams()) != 0 { + return nil, coro.FunctionPlan{}, fmt.Errorf("spawn target %q is not an exact non-generic zero-result method", targetPlan.ID) + } + ownerPlan, found := plan.FunctionPlan(spawn.Parent()) + if !found || ownerPlan.Emission != coro.EmitCoroutine || ownerPlan.Primary != coro.PrimaryCoroutine || + ownerPlan.Demand != coro.AsyncDemand || !ownerPlan.Effect.Contains(coro.YieldOnly) { + return nil, coro.FunctionPlan{}, fmt.Errorf("spawn owner is not one demanded preemptible coroutine primary") + } + if err := validateCoroDirectSpawnArgumentTransport(plan, spawn, target, managedDispatch); err != nil { + return nil, coro.FunctionPlan{}, err + } + return target, targetPlan, nil +} + +// validateCoroDirectSpawnArgumentTransport proves the physical receiver/args +// tuple consumed by a direct coroutine ramp. Transport and representation are +// orthogonal per function leaf: a raw C function is one DirectPlain code +// pointer, while a managed Go function is the universal Dispatch descriptor +// closure. Aggregates may contain both and retain that exact recursive physical +// layout. Only managed leaves depend on the descriptor transport capability. +func validateCoroDirectSpawnArgumentTransport( + plan *coro.SSAPlan, + spawn *ssa.Go, + target *ssa.Function, + managedDispatch bool, +) error { + if plan == nil || spawn == nil || spawn.Common() == nil || target == nil || target.Signature == nil { + return fmt.Errorf("direct spawn argument transport requires an exact target signature") + } + physical := coroPhysicalNormalizeSourceSignature(target.Signature) + args := spawn.Common().Args + if physical == nil || physical.Params().Len() != len(args) { + return fmt.Errorf("direct spawn arguments=%d do not match normalized target parameters=%d", len(args), physical.Params().Len()) + } + for index, argument := range args { + parameter := physical.Params().At(index).Type() + if !types.Identical(argument.Type(), parameter) { + return fmt.Errorf("direct spawn argument %d type %s does not match target parameter %s", index, argument.Type(), parameter) + } + if !coroPhysicalTypeContainsFunctionValue(argument.Type(), make(map[types.Type]bool)) { + continue + } + valuePlan, found := plan.ValuePlan(argument) + if !found || valuePlan.Value != argument || len(valuePlan.Funcs) == 0 { + return fmt.Errorf("direct spawn function-containing argument %d has no exact ValuePlan", index) + } + _, scalar := types.Unalias(argument.Type()).Underlying().(*types.Signature) + if scalar && (len(valuePlan.Funcs) != 1 || len(valuePlan.Funcs[0].Path) != 0) { + return fmt.Errorf("direct spawn scalar function argument %d has no exact scalar ValuePlan", index) + } + for leafIndex, leaf := range valuePlan.Funcs { + switch leaf.Transport { + case coro.RawCCodePointer: + if leaf.Rep != coro.DirectPlain { + return fmt.Errorf( + "direct spawn argument %d function leaf %d has raw C transport with representation %s", + index, leafIndex, leaf.Rep, + ) + } + case coro.ManagedTransport: + if leaf.Rep != coro.Dispatch { + return fmt.Errorf( + "direct spawn argument %d function leaf %d has managed transport with representation %s", + index, leafIndex, leaf.Rep, + ) + } + if !managedDispatch { + return fmt.Errorf( + "direct spawn argument %d managed function leaf %d requires the universal descriptor transport capability", + index, leafIndex, + ) + } + default: + return fmt.Errorf( + "direct spawn argument %d function leaf %d has invalid transport %s", + index, leafIndex, leaf.Transport, + ) + } + } + } + return nil +} + +// tryCompileCoroClosedStaticSpawn creates exactly one child root to its LLVM +// initial suspend and commits it to the scheduler. Arguments are fully +// materialized before begin mutates scheduler state. The parent then reaches +// an explicit safepoint using its physical G; there is no TLS/current-G +// fallback anywhere in this path. +func (p *context) tryCompileCoroClosedStaticSpawn(b llssa.Builder, spawn *ssa.Go) bool { + if spawn == nil || p == nil || p.compilation == nil || !p.compilation.CoroClosedStaticSpawnActive() { + return false + } + body := p.coroBody() + if body == nil || b.Func != p.fn { + panic("closed static spawn requires an active planned physical coroutine body") + } + instructionPlan, planned := p.plannedCoroPhysicalControl(spawn) + if !planned { + panic("coroutine spawn has no frozen physical instruction plan") + } + switch instructionPlan.control { + case coroPhysicalControlDispatchSpawn: + p.observeCoroPhysicalControl(spawn, coroPhysicalControlDispatchSpawn) + p.compileCoroManagedDispatchSpawn(b, spawn) + return true + case coroPhysicalControlDirectSpawn: + p.observeCoroPhysicalControl(spawn, coroPhysicalControlDirectSpawn) + case coroPhysicalControlNone: + return false + default: + panic(fmt.Sprintf("coroutine spawn has mismatched frozen physical control recipe %s", instructionPlan.control)) + } + target := instructionPlan.controlTarget + if target == nil || instructionPlan.controlTargetID == "" { + panic("closed static spawn has an incomplete frozen physical control recipe") + } + + p.recordCallerLocationForCall(b, &spawn.Call) + p.emitPCLineLabel(b, spawn.Pos()) + // Go SSA already sequences argument-producing instructions. Re-materialize + // every exact operand here, in source order, before the begin transaction. + args := p.compileValues(b, spawn.Call.Args, fnNormal) + + parent := body.task + begin := p.pkg.NewFunc(coroSpawnBeginHookV1, coroSpawnBeginSignature(), llssa.InC) + childG := b.Call(begin.Expr, parent) + null := p.prog.Nil(p.prog.VoidPtr()) + physicalArgs := make([]llssa.Expr, 0, len(args)+2) + physicalArgs = append(physicalArgs, childG, null) + physicalArgs = append(physicalArgs, args...) + + root, _, kind := p.compileFunction(target) + if kind != goFunc { + panic(fmt.Sprintf("closed static spawn: target %q did not resolve to a Go coroutine entry", instructionPlan.controlTargetID)) + } + if root == nil { + panic(fmt.Sprintf("closed static spawn: target %q has no physical root", instructionPlan.controlTargetID)) + } + handle := b.Call(root.Expr, physicalArgs...) + commit := p.pkg.NewFunc(coroSpawnCommitHookV1, coroSpawnCommitSignature(), llssa.InC) + b.Call(commit.Expr, parent, childG, handle) + body.pollAndSuspendForPreempt(b) + return true +} + +// compileCoroManagedDispatchSpawn creates an independent scheduler G from the +// universal descriptor's coroutine entry. Callee and arguments are fully +// materialized in Go order before begin publishes scheduler state. The child G +// and nil result slot are then passed to the typed descriptor thunk, which +// returns an LLVM initial-suspended handle for the existing commit transaction. +// CallCoroDispatchCoro performs the fail-closed descriptor/version/hash/result +// and HasCoro checks; plain-only or corrupt values never fall back to a native +// callback, TLS, or a synchronous adapter. +func (p *context) compileCoroManagedDispatchSpawn(b llssa.Builder, spawn *ssa.Go) { + body := p.coroBody() + if body == nil { + panic("managed dispatch spawn requires an active physical coroutine body") + } + p.recordCallerLocationForCall(b, &spawn.Call) + p.emitPCLineLabel(b, spawn.Pos()) + // Preserve Go's evaluation order at the scheduler transaction boundary: + // first the function value, then every explicit argument left-to-right. + fn := p.compileValue(b, spawn.Call.Value) + args := p.compileValues(b, spawn.Call.Args, fnNormal) + abi, err := newCoroPlainDispatchABI(p, spawn.Call.Signature()) + if err != nil { + panic(fmt.Errorf("managed descriptor spawn: %w", err)) + } + if abi.signature.Results().Len() != 0 { + panic("managed descriptor spawn requires a zero-result signature") + } + opts := llssa.CoroDispatchCallOptions{ + Version: coroPlainDispatchVersion, + ABIHash: abi.hash, + Result: p.prog.Type(abi.resultSlotType, llssa.InC), + } + // Preserve Go evaluation order: the callee and arguments are complete + // before the nil-call check. The physical parent then owns the structured + // panic edge, so descriptor validation needs no hidden runtime helper. + p.compileCoroImplicitNilAccessGuard(b, b.Field(fn, 0)) + opts.DescriptorNonNil = true + + parent := body.task + begin := p.pkg.NewFunc(coroSpawnBeginHookV1, coroSpawnBeginSignature(), llssa.InC) + childG := b.Call(begin.Expr, parent) + null := p.prog.Nil(p.prog.VoidPtr()) + handle := b.CallCoroDispatchCoro(fn, childG, null, args, opts) + commit := p.pkg.NewFunc(coroSpawnCommitHookV1, coroSpawnCommitSignature(), llssa.InC) + b.Call(commit.Expr, parent, childG, handle) + body.pollAndSuspendForPreempt(b) +} diff --git a/cl/coro_spawn_test.go b/cl/coro_spawn_test.go new file mode 100644 index 0000000000..acb78caac7 --- /dev/null +++ b/cl/coro_spawn_test.go @@ -0,0 +1,823 @@ +//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 ( + "bytes" + "go/types" + "regexp" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroClosedStaticSpawnTestSource = `package foo + +var Sink uint32 + +func ArgFirst(value uint32) uint32 { return value + 1 } +func ArgSecond(value uint32) uint32 { return value + 2 } +func Plain(first, second uint32) { Sink = first + second } +func Async(value uint32) { Sink = value } + +func Parent(value uint32) { + Plain(value, value) + go Plain(ArgFirst(value), ArgSecond(value)) + go Async(value) +} +` + +const coroManagedDispatchSpawnTestSource = `package foo + +var Sink int + +func MakeCallback(seed int) func(int) { + return func(value int) { Sink = seed + value } +} + +func MakeLauncher(callback func(int), base int) func(int) { + return func(value int) { + go callback(base + value) + } +} +` + +const coroClosedStaticMethodSpawnTestSource = `package foo + +var Sink int + +type Worker int + +func (receiver Worker) Run(callback func(int), value int) { + Sink = int(receiver) + value + _ = callback +} + +func Receiver(value int) Worker { return Worker(value + 1) } +func Argument(value int) int { return value + 2 } + +func Parent(callback func(int), value int) { + go Receiver(value).Run(callback, Argument(value)) +} +` + +const coroStaticSpawnTransportTestSource = `package foo + +//llgo:type C +type CFunc func(int) int + +type Mixed struct { + Raw CFunc + Managed func(int) +} + +func RawTarget(raw CFunc) { _ = raw } +func MixedTarget(raw CFunc, managed func(int), mixed Mixed) { + _, _, _ = raw, managed, mixed +} + +func Parent(raw CFunc, managed func(int), mixed Mixed) { + go RawTarget(raw) + go MixedTarget(raw, managed, mixed) +} + +func RawCallee(raw CFunc) { go raw(1) } +` + +func TestCoroClosedStaticSpawnNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, ssaPkg := compileCoroClosedStaticSpawnFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify closed static spawn before CoroSplit: %v\n%s", err, module.String()) + } + parentPlan, _ := plan.FunctionPlan(ssaPkg.Func("Parent")) + if parentPlan.DeclaredEffect != coro.YieldOnly || !parentPlan.LocalEffect.Contains(coro.YieldOnly) || + !parentPlan.Effect.Contains(coro.YieldOnly) || parentPlan.Emission != coro.EmitCoroutine || + parentPlan.Primary != coro.PrimaryCoroutine || parentPlan.FuncRep != coro.DirectCoro || parentPlan.Demand != coro.AsyncDemand { + t.Fatalf("Parent plan = %+v", parentPlan) + } + plainPlan, _ := plan.FunctionPlan(ssaPkg.Func("Plain")) + if plainPlan.Emission != coro.EmitCoroutine || plainPlan.Primary != coro.PrimaryCoroutine || plainPlan.FuncRep != coro.DirectCoro || + !plainPlan.Effect.Contains(coro.YieldOnly) || plainPlan.Demand != coro.AsyncDemand { + t.Fatalf("Plain sync+spawn plan = %+v", plainPlan) + } + asyncPlan, _ := plan.FunctionPlan(ssaPkg.Func("Async")) + if asyncPlan.Emission != coro.EmitCoroutine || asyncPlan.Primary != coro.PrimaryCoroutine || + asyncPlan.FuncRep != coro.DirectCoro || asyncPlan.Demand != coro.AsyncDemand { + t.Fatalf("Async spawn plan = %+v", asyncPlan) + } + + ir := module.String() + parent := requireCoroPhysicalFunction(t, module, "foo.Parent").String() + if !module.NamedFunction("foo.Plain").IsNil() || module.NamedFunction("foo.Plain"+coroPrimarySuffix).IsNil() { + t.Fatalf("bounded sync+spawn target did not retain exactly one preemptible coroutine primary:\n%s", ir) + } + if !module.NamedFunction("foo.Async").IsNil() || module.NamedFunction("foo.Async"+coroPrimarySuffix).IsNil() { + t.Fatalf("Async did not retain exactly one coroutine primary:\n%s", ir) + } + if strings.Contains(ir, "__llgo_coro_spawn_plain_adapter") { + t.Fatalf("spawn target incorrectly gained a second plain-root adapter body:\n%s", ir) + } + + index := func(pattern string) int { + match := regexp.MustCompile(pattern).FindStringIndex(parent) + if match == nil { + return -1 + } + return match[0] + } + first := index(`call i32 @"?foo\.ArgFirst"?`) + second := index(`call i32 @"?foo\.ArgSecond"?`) + begin := strings.Index(parent, "call ptr @"+coroSpawnBeginHookV1) + plainRoot := -1 + if begin >= 0 { + if relative := regexp.MustCompile(`call ptr @"?foo\.Plain\$coro"?\(`).FindStringIndex(parent[begin:]); relative != nil { + plainRoot = begin + relative[0] + } + } + commit := strings.Index(parent, "call void @"+coroSpawnCommitHookV1) + poll := strings.Index(parent, "call i1 @"+coroPreemptPollHookV1) + if first < 0 || second < 0 || begin < 0 || plainRoot < 0 || commit < 0 || poll < 0 || + !(first < second && second < begin && begin < plainRoot && plainRoot < commit && commit < poll) { + t.Fatalf("argument/begin/root/commit/safepoint order is invalid:\n%s", parent) + } + if got := strings.Count(parent, "call ptr @"+coroSpawnBeginHookV1); got != 2 { + t.Fatalf("spawn begin calls = %d, want two:\n%s", got, parent) + } + if got := strings.Count(parent, "call void @"+coroSpawnCommitHookV1); got != 2 { + t.Fatalf("spawn commit calls = %d, want two:\n%s", got, parent) + } + if got := strings.Count(parent, "call i1 @"+coroPreemptPollHookV1); got != 2 { + t.Fatalf("post-commit explicit preempt polls = %d, want two:\n%s", got, parent) + } + if got := strings.Count(parent, "call void @"+coroYieldPrepareHookV1); got != 2 { + t.Fatalf("post-commit parent yield handoffs = %d, want two:\n%s", got, parent) + } + if !regexp.MustCompile(`call ptr @"?foo\.Async\$coro"?\(`).MatchString(parent) { + t.Fatalf("suspendable target is not called through its unique physical root:\n%s", parent) + } + if strings.Contains(parent[begin:commit], "@llvm.coro.promise") { + t.Fatalf("independent spawned G incorrectly received an await parent-handle link:\n%s", parent[begin:commit]) + } + if got := len(regexp.MustCompile(`call ptr @"?foo\.Plain\$coro"?\(`).FindAllStringIndex(parent, -1)); got != 2 { + t.Fatalf("sync await + spawn calls to the one Plain primary = %d, want two:\n%s", got, parent) + } + for _, forbidden := range []string{"CreateThread", "InitThreadAttr", "DestroyThreadAttr", "._llgo_routine$", "pthread", "AllocRoot"} { + if strings.Contains(ir, forbidden) { + t.Fatalf("closed static spawn leaked legacy native-stack lowering %q:\n%s", forbidden, ir) + } + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{"foo.Parent$coro", "foo.Plain$coro", "foo.Async$coro"} { + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(name + suffix).IsNil() { + t.Fatalf("CoroSplit did not create %s%s:\n%s", name, suffix, module.String()) + } + } + } + for _, intrinsic := range []string{"llvm.coro.id", "llvm.coro.begin", "llvm.coro.suspend", "llvm.coro.end"} { + if hasLLVMCall(module.String(), intrinsic) { + t.Fatalf("post-split spawn module still calls %s:\n%s", intrinsic, module.String()) + } + } + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit spawn object: %v\n%s", err, module.String()) + } + defer object.Dispose() + for _, symbol := range []string{coroSpawnBeginHookV1, coroSpawnCommitHookV1, "foo.Plain$coro"} { + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(symbol)) { + t.Fatalf("post-CoroSplit object lost spawn symbol %q", symbol) + } + } + }) + } +} + +func TestCoroManagedDispatchSpawnNativeAndWasm32CoroSplit(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, _, launcherTarget, callbackTarget := compileCoroManagedDispatchSpawnFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed descriptor spawn before CoroSplit: %v\n%s", err, module.String()) + } + for _, target := range []*ssa.Function{launcherTarget, callbackTarget} { + targetPlan, _ := plan.FunctionPlan(target) + if targetPlan.Emission != coro.EmitCoroutine || targetPlan.Primary != coro.PrimaryCoroutine || + targetPlan.FuncRep != coro.Dispatch || targetPlan.Demand != coro.AsyncDemand || + !targetPlan.Effect.Contains(coro.YieldOnly) { + t.Fatalf("captured descriptor target %s plan = %+v", target, targetPlan) + } + } + + launcherIR := requireCoroPhysicalFunction(t, module, launcherTarget.String()).String() + indirectCoro := regexp.MustCompile(`call ptr %[-a-zA-Z$._0-9]+\(ptr [^,]+, ptr null, ptr [^,]+, i(?:32|64) [^)]+\)`) + argumentMatch := regexp.MustCompile(`add i(?:32|64)`).FindStringIndex(launcherIR) + argument := -1 + if argumentMatch != nil { + argument = argumentMatch[0] + } + begin := strings.Index(launcherIR, "call ptr @"+coroSpawnBeginHookV1) + indirect := indirectCoro.FindStringIndex(launcherIR) + commit := strings.Index(launcherIR, "call void @"+coroSpawnCommitHookV1) + poll := strings.Index(launcherIR, "call i1 @"+coroPreemptPollHookV1) + if argument < 0 || begin < 0 || indirect == nil || commit < 0 || poll < 0 || + !(argument < begin && begin < indirect[0] && indirect[0] < commit && commit < poll) { + t.Fatalf("captured launcher callee/argument/begin/descriptor/commit/poll order is invalid:\n%s", launcherIR) + } + if got := strings.Count(launcherIR, "call ptr @"+coroSpawnBeginHookV1); got != 1 { + t.Fatalf("captured launcher spawn begin calls = %d, want one:\n%s", got, launcherIR) + } + if got := strings.Count(launcherIR, "call void @"+coroSpawnCommitHookV1); got != 1 { + t.Fatalf("captured launcher spawn commit calls = %d, want one:\n%s", got, launcherIR) + } + if strings.Count(launcherIR, "call void @"+coroFaultPrepareHookV1) < 2 { + t.Fatalf("captured launcher FreeVar cell loads lack explicit nil-fault edges:\n%s", launcherIR) + } + if !strings.Contains(launcherIR, "coro.dispatch.capability.missing") || + !strings.Contains(launcherIR, "call void @llvm.trap()") { + t.Fatalf("managed spawn does not fail closed on a plain-only/corrupt descriptor:\n%s", launcherIR) + } + for _, forbidden := range []string{"CreateThread", "pthread", "._llgo_routine$", "@llvm.coro.promise"} { + if strings.Contains(launcherIR, forbidden) { + t.Fatalf("captured launcher managed spawn leaked forbidden path %q:\n%s", forbidden, launcherIR) + } + } + if !strings.Contains(module.String(), coroCoroDispatchThunkPrefix) { + t.Fatalf("captured goroutine descriptor has no coroutine thunk:\n%s", module.String()) + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{launcherTarget.String() + coroPrimarySuffix, callbackTarget.String() + coroPrimarySuffix} { + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(name + suffix).IsNil() { + t.Fatalf("CoroSplit did not create %s%s:\n%s", name, suffix, module.String()) + } + } + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify managed descriptor spawn after CoroSplit: %v\n%s", err, module.String()) + } + }) + } +} + +func TestCoroClosedStaticMethodSpawnNativeAndWasm32CoroSplit(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, ssaPkg, method, spawn := compileCoroClosedStaticMethodSpawnFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify closed static method spawn before CoroSplit: %v\n%s", err, module.String()) + } + parentPlan, _ := plan.FunctionPlan(ssaPkg.Func("Parent")) + methodPlan, _ := plan.FunctionPlan(method) + for name, function := range map[string]coro.FunctionPlan{"Parent": parentPlan, "Run": methodPlan} { + if function.Emission != coro.EmitCoroutine || function.Primary != coro.PrimaryCoroutine || + function.FuncRep != coro.DirectCoro || function.Demand != coro.AsyncDemand || + !function.Effect.Contains(coro.YieldOnly) { + t.Fatalf("%s plan = %+v", name, function) + } + } + if _, _, err := resolveCoroDirectStaticSpawn(plan, spawn, false); err == nil || + !strings.Contains(err.Error(), "universal descriptor transport") { + t.Fatalf("method callback gate-off error = %v", err) + } + if resolved, _, err := resolveCoroDirectStaticSpawn(plan, spawn, true); err != nil || resolved != method { + t.Fatalf("resolve method spawn with descriptor transport = %v, %v", resolved, err) + } + callbackPlan, found := plan.ValuePlan(spawn.Common().Args[1]) + if !found || len(callbackPlan.Funcs) != 1 || len(callbackPlan.Funcs[0].Path) != 0 || + callbackPlan.Funcs[0].Rep != coro.Dispatch { + t.Fatalf("method callback ValuePlan = %+v, present=%t", callbackPlan, found) + } + + parentIR := requireCoroPhysicalFunction(t, module, "foo.Parent").String() + methodName := funcName(ssaPkg.Pkg, method, false) + coroPrimarySuffix + methodIR := module.NamedFunction(methodName) + if methodIR.IsNil() { + t.Fatalf("method spawn target %q is absent:\n%s", methodName, module.String()) + } + if !regexp.MustCompile(`define ptr @"?` + regexp.QuoteMeta(methodName) + `"?\(ptr [^,]+, ptr [^,]+, i(?:32|64) [^,]+, \{ ptr, ptr \} [^,]+, i(?:32|64) `).MatchString(methodIR.String()) { + t.Fatalf("method physical receiver/callback/argument ABI is not normalized descriptor transport:\n%s", methodIR.String()) + } + index := func(pattern string) int { + match := regexp.MustCompile(pattern).FindStringIndex(parentIR) + if match == nil { + return -1 + } + return match[0] + } + receiver := index(`call i(?:32|64) @"?foo\.Receiver"?`) + argument := index(`call i(?:32|64) @"?foo\.Argument"?`) + begin := strings.Index(parentIR, "call ptr @"+coroSpawnBeginHookV1) + methodCall := index(`call ptr @"?` + regexp.QuoteMeta(methodName) + `"?\([^\n]*\{ ptr, ptr \}`) + commit := strings.Index(parentIR, "call void @"+coroSpawnCommitHookV1) + poll := strings.Index(parentIR, "call i1 @"+coroPreemptPollHookV1) + if receiver < 0 || argument < 0 || begin < 0 || methodCall < 0 || commit < 0 || poll < 0 || + !(receiver < argument && argument < begin && begin < methodCall && methodCall < commit && commit < poll) { + t.Fatalf("receiver/arguments/begin/method/commit/poll order is invalid:\n%s", parentIR) + } + for _, forbidden := range []string{"CreateThread", "pthread", "._llgo_routine$", "@llvm.coro.promise"} { + if strings.Contains(parentIR, forbidden) { + t.Fatalf("method spawn leaked forbidden path %q:\n%s", forbidden, parentIR) + } + } + + runCoroABITestPipeline(t, prog, module) + for _, name := range []string{"foo.Parent" + coroPrimarySuffix, methodName} { + for _, suffix := range []string{".resume", ".destroy"} { + if module.NamedFunction(name + suffix).IsNil() { + t.Fatalf("CoroSplit did not create %s%s:\n%s", name, suffix, module.String()) + } + } + } + }) + } +} + +func TestCoroClosedStaticSpawnFunctionArgumentsAreTransportAware(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, ssaPkg, rawSpawn, mixedSpawn := compileCoroStaticSpawnTransportFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if resolved, _, err := resolveCoroDirectStaticSpawn(plan, rawSpawn, false); err != nil || resolved != ssaPkg.Func("RawTarget") { + t.Fatalf("raw-only static spawn without descriptor capability = %v, %v", resolved, err) + } + if _, _, err := resolveCoroDirectStaticSpawn(plan, mixedSpawn, false); err == nil || + !strings.Contains(err.Error(), "managed function leaf") { + t.Fatalf("mixed static spawn gate-off error = %v", err) + } + if resolved, _, err := resolveCoroDirectStaticSpawn(plan, mixedSpawn, true); err != nil || resolved != ssaPkg.Func("MixedTarget") { + t.Fatalf("mixed static spawn with descriptor capability = %v, %v", resolved, err) + } + + rawArgumentPlan, found := plan.ValuePlan(rawSpawn.Common().Args[0]) + if !found || len(rawArgumentPlan.Funcs) != 1 || + rawArgumentPlan.Funcs[0].Transport != coro.RawCCodePointer || + rawArgumentPlan.Funcs[0].Rep != coro.DirectPlain { + t.Fatalf("raw spawn argument ValuePlan = %+v, present=%t", rawArgumentPlan, found) + } + mixedArgumentPlan, found := plan.ValuePlan(mixedSpawn.Common().Args[2]) + if !found || len(mixedArgumentPlan.Funcs) != 2 || + mixedArgumentPlan.Funcs[0].Transport != coro.RawCCodePointer || + mixedArgumentPlan.Funcs[0].Rep != coro.DirectPlain || + mixedArgumentPlan.Funcs[1].Transport != coro.ManagedTransport || + mixedArgumentPlan.Funcs[1].Rep != coro.Dispatch { + t.Fatalf("mixed spawn argument ValuePlan = %+v, present=%t", mixedArgumentPlan, found) + } + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify transport-aware static spawn before CoroSplit: %v\n%s", err, module.String()) + } + rawTargetIR := requireCoroPhysicalFunction(t, module, "foo.RawTarget").String() + mixedTargetIR := requireCoroPhysicalFunction(t, module, "foo.MixedTarget").String() + parentIR := requireCoroPhysicalFunction(t, module, "foo.Parent").String() + if !regexp.MustCompile(`define ptr @"?foo\.RawTarget\$coro"?\(ptr [^,]+, ptr [^,]+, ptr `).MatchString(rawTargetIR) { + t.Fatalf("raw C spawn parameter is not one physical code pointer:\n%s", rawTargetIR) + } + if !regexp.MustCompile(`%"?foo\.Mixed"? = type \{ ptr, \{ ptr, ptr \} \}`).MatchString(module.String()) || + !regexp.MustCompile(`define ptr @"?foo\.MixedTarget\$coro"?\(ptr [^,]+, ptr [^,]+, ptr [^,]+, \{ ptr, ptr \} [^,]+, %"?foo\.Mixed"? `).MatchString(mixedTargetIR) { + t.Fatalf("mixed spawn target did not preserve raw/managed leaf layout:\n%s", mixedTargetIR) + } + if !regexp.MustCompile(`call ptr @"?foo\.RawTarget\$coro"?\(ptr [^,]+, ptr null, ptr `).MatchString(parentIR) || + !regexp.MustCompile(`call ptr @"?foo\.MixedTarget\$coro"?\(ptr [^,]+, ptr null, ptr [^,]+, \{ ptr, ptr \} [^,]+, %"?foo\.Mixed"? `).MatchString(parentIR) { + t.Fatalf("static spawn calls did not pass the planned physical layouts:\n%s", parentIR) + } + + runCoroABITestPipeline(t, prog, module) + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify transport-aware static spawn after CoroSplit: %v\n%s", err, module.String()) + } + }) + } +} + +func compileCoroStaticSpawnTransportFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Package, *ssa.Go, *ssa.Go, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroStaticSpawnTransportTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + // Mirror production import ordering: //llgo:type metadata must be installed + // before the emission universe freezes the C function-value transport. + ParsePkgSyntax(prog, ssaPkg.Pkg, files) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + parent := ssaPkg.Func("Parent") + rawTarget := ssaPkg.Func("RawTarget") + mixedTarget := ssaPkg.Func("MixedTarget") + rawCallee := ssaPkg.Func("RawCallee") + var rawSpawn, mixedSpawn, rawCalleeSpawn *ssa.Go + for _, block := range parent.Blocks { + for _, instruction := range block.Instrs { + spawn, ok := instruction.(*ssa.Go) + if !ok || spawn.Common() == nil || spawn.Common().StaticCallee() == nil { + continue + } + switch spawn.Common().StaticCallee() { + case rawTarget: + rawSpawn = spawn + case mixedTarget: + mixedSpawn = spawn + } + } + } + for _, block := range rawCallee.Blocks { + for _, instruction := range block.Instrs { + if spawn, ok := instruction.(*ssa.Go); ok { + rawCalleeSpawn = spawn + } + } + } + if rawSpawn == nil || mixedSpawn == nil || rawCalleeSpawn == nil { + prog.Dispose() + t.Fatal("transport-aware static spawn fixture is incomplete") + } + + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + config := coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == parent || fn == rawTarget || fn == mixedTarget || fn == rawCallee { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyUnknownCall: func(caller *ssa.Function, call ssa.CallInstruction) (coro.UnknownTarget, error) { + if caller == rawCallee && call == rawCalleeSpawn { + return coro.UnknownForeign, nil + } + return coro.UnknownManaged, nil + }, + ClassifyRawCFunctionType: func(typ types.Type) (bool, error) { + return prog.TypeBackground(typ) == llssa.InC, nil + }, + } + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: parent, Demand: coro.AsyncDemand}}, config) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + rawCalleePlan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: rawCallee, Demand: coro.AsyncDemand}}, config) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + if callPlan, found := rawCalleePlan.CallPlan(rawCalleeSpawn); !found || + callPlan.Transport != coro.RawCCodePointer || callPlan.Rep != coro.DirectPlain { + prog.Dispose() + t.Fatalf("raw C callee spawn CallPlan = %+v, present=%t", callPlan, found) + } + if _, _, err := resolveCoroDirectStaticSpawn(rawCalleePlan, rawCalleeSpawn, true); err == nil || + !strings.Contains(err.Error(), "raw C code-pointer callee") { + prog.Dispose() + t.Fatalf("raw C callee spawn rejection = %v", err) + } + + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, ssaPkg, rawSpawn, mixedSpawn +} + +func compileCoroClosedStaticMethodSpawnFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Package, *ssa.Function, *ssa.Go, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroClosedStaticMethodSpawnTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + parent := ssaPkg.Func("Parent") + var spawn *ssa.Go + for _, block := range parent.Blocks { + for _, instruction := range block.Instrs { + if candidate, ok := instruction.(*ssa.Go); ok { + spawn = candidate + } + } + } + if spawn == nil || spawn.Common() == nil { + prog.Dispose() + t.Fatal("method spawn fixture has no goroutine call") + } + method := spawn.Common().StaticCallee() + if method == nil || method.Signature == nil || method.Signature.Recv() == nil { + prog.Dispose() + t.Fatalf("method spawn target = %v", method) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: parent, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == parent || fn == method { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, ssaPkg, method, spawn +} + +func compileCoroManagedDispatchSpawnFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Package, *ssa.Function, *ssa.Function, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroManagedDispatchSpawnTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + makeCallback, makeLauncher := ssaPkg.Func("MakeCallback"), ssaPkg.Func("MakeLauncher") + var launcherTarget, callbackTarget *ssa.Function + for _, owner := range []*ssa.Function{makeCallback, makeLauncher} { + for _, block := range owner.Blocks { + for _, instruction := range block.Instrs { + closure, ok := instruction.(*ssa.MakeClosure) + if !ok { + continue + } + closureTarget, ok := closure.Fn.(*ssa.Function) + if !ok { + prog.Dispose() + t.Fatalf("captured descriptor target = %T", closure.Fn) + } + if owner == makeLauncher { + launcherTarget = closureTarget + } else { + callbackTarget = closureTarget + } + } + } + } + var launcherSpawn *ssa.Go + if launcherTarget != nil { + for _, block := range launcherTarget.Blocks { + for _, instruction := range block.Instrs { + if spawn, ok := instruction.(*ssa.Go); ok { + launcherSpawn = spawn + } + } + } + } + if launcherSpawn == nil || launcherTarget == nil || callbackTarget == nil { + prog.Dispose() + t.Fatal("managed descriptor spawn fixture is incomplete") + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{ + {Function: makeCallback, Demand: coro.SyncDemand}, + {Function: makeLauncher, Demand: coro.SyncDemand}, + }, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == launcherTarget || fn == callbackTarget { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyUnknownCall: func(_ *ssa.Function, call ssa.CallInstruction) (coro.UnknownTarget, error) { + if call == launcherSpawn { + return coro.UnknownManagedDispatch, nil + } + return coro.UnknownManaged, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + if _, err := plan.ResolveManagedDispatchSpawn(launcherSpawn); err != nil { + prog.Dispose() + t.Fatalf("resolve managed descriptor spawn: %v", err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroPreemptCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + compilation.FuncRepABI = coro.FuncRepABIV1 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, ssaPkg, launcherTarget, callbackTarget +} + +func compileCoroClosedStaticSpawnFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Package, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroClosedStaticSpawnTestSource) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + parent, plain, async := ssaPkg.Func("Parent"), ssaPkg.Func("Plain"), ssaPkg.Func("Async") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: parent, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == parent || fn == plain || fn == async { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, + } + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, ssaPkg +} + +func TestCoroClosedStaticSpawnCompilationCapabilityFailsClosed(t *testing.T) { + compilation := &Compilation{CoroProfile: CoroProfileStackless} + if !compilation.CoroClosedStaticSpawnActive() || !compilation.CoroProgramBootstrapActive() || !compilation.CoroChildAwaitActive() { + t.Fatal("stackless profile did not activate spawn, bootstrap, and child-await as one contract") + } +} diff --git a/cl/coro_string_concat_test.go b/cl/coro_string_concat_test.go new file mode 100644 index 0000000000..ed47f81f0d --- /dev/null +++ b/cl/coro_string_concat_test.go @@ -0,0 +1,289 @@ +//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 ( + "bytes" + "go/ast" + "go/token" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroStringConcatRuntimeFixture = `package runtime +import "unsafe" + +type String struct { + Data unsafe.Pointer + Len int +} + +func AllocU(uintptr) unsafe.Pointer { return nil } + +// The production helper's possible length panic is represented by the test +// plan's MayUnwind policy so codegen can focus on the exact managed edge. A +// separate core-plan test derives that policy from an actual panic instruction. +func StringCat(left, right String) String { + length := left.Len + right.Len + return String{AllocU(uintptr(length)), length} +} +` + +const coroStringConcatFixture = `package foo + +func Pause() {} + +func Root(left, right string) string { + prefix := left + right + Pause() + return prefix + left +} +` + +type coroStringConcatTestPlan struct { + prog llssa.Program + runtimePkg emissionTestPackage + fooPkg emissionTestPackage + universe *EmissionUniverse + plan *coro.SSAPlan + root *ssa.Function + concats []*ssa.BinOp +} + +func TestCoroStringConcatManagedHelperNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + fixture := prepareCoroStringConcatTestPlan(t, test.target, true) + defer fixture.prog.Dispose() + + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + for index, concat := range fixture.concats { + if reason := audit.validateBinOp(concat); reason != "" { + t.Fatalf("string concat %d rejected: %s", index, reason) + } + } + + helper := fixture.runtimePkg.ssa.Func("StringCat") + helperPlan, ok := fixture.plan.FunctionPlan(helper) + if !ok || helperPlan.External != coro.Defined || helperPlan.Emission != coro.EmitCoroutine || + helperPlan.Primary != coro.PrimaryCoroutine || helperPlan.FuncRep != coro.DirectCoro || + !helperPlan.Demand.Contains(coro.AsyncDemand) || !helperPlan.Effect.Contains(coro.OutcomeStructured) || + !helperPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("StringCat plan = %+v, present=%t; want demanded ExplicitStatus coroutine", helperPlan, ok) + } + + compilation := &Compilation{CoroPlan: fixture.plan, EmissionUniverse: fixture.universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + runtimeLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.runtimePkg.ssa, []*ast.File{fixture.runtimePkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile StringCat helper: %v", err) + } + runtimeModule := runtimeLL.Module() + defer runtimeModule.Dispose() + fooLL, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.fooPkg.ssa, []*ast.File{fixture.fooPkg.file}, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatalf("compile string concat owner: %v", err) + } + fooModule := fooLL.Module() + defer fooModule.Dispose() + for name, module := range map[string]llvm.Module{"runtime": runtimeModule, "foo": fooModule} { + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify %s before CoroSplit: %v\n%s", name, err, module.String()) + } + } + + rootIR := requireCoroPhysicalFunction(t, fooModule, "foo.Root").String() + for _, required := range []string{ + "runtime.StringCat$coro", + "foo.Pause$coro", + "call void @" + coroAwaitPrepareHookV1, + "call i32 @" + coroAwaitConsumeHookV1, + } { + if !strings.Contains(rootIR, required) { + t.Fatalf("managed string concat owner lacks %q:\n%s", required, rootIR) + } + } + if got := strings.Count(rootIR, "runtime.StringCat$coro"); got != 2 { + t.Fatalf("managed StringCat calls = %d, want two across Pause:\n%s", got, rootIR) + } + if got := strings.Count(rootIR, "call void @"+coroAwaitPrepareHookV1); got != 3 { + t.Fatalf("managed awaits = %d, want StringCat + Pause + StringCat:\n%s", got, rootIR) + } + + for _, module := range []llvm.Module{runtimeModule, fooModule} { + runCoroABITestPipeline(t, fixture.prog, module) + object, err := fixture.prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit string concat object: %v\n%s", err, module.String()) + } + if len(object.Bytes()) == 0 { + object.Dispose() + t.Fatal("post-CoroSplit string concat object is empty") + } + object.Dispose() + } + if !bytes.Contains([]byte(fooModule.String()), []byte("foo.Root$coro.resume")) { + t.Fatalf("CoroSplit lost the string concat owner resume entry:\n%s", fooModule.String()) + } + }) + } +} + +func TestCoroStringConcatManagedHelperFailsClosed(t *testing.T) { + t.Run("explicit status required", func(t *testing.T) { + fixture := prepareCoroStringConcatTestPlan(t, nil, true) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + if reason := audit.validateBinOp(fixture.concats[0]); !strings.Contains(reason, "explicit-status panic ABI") { + t.Fatalf("missing explicit-status rejection = %q", reason) + } + }) + + t.Run("lowered fact required", func(t *testing.T) { + fixture := prepareCoroStringConcatTestPlan(t, nil, false) + defer fixture.prog.Dispose() + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + audit.allowImplicitNilFault = true + if reason := audit.validateBinOp(fixture.concats[0]); !strings.Contains(reason, "exact coroutine-safe lowered-call plan") { + t.Fatalf("missing lowered-call rejection = %q", reason) + } + }) +} + +func prepareCoroStringConcatTestPlan(t *testing.T, target *llssa.Target, loweredCalls bool) coroStringConcatTestPlan { + t.Helper() + testProg := newEmissionTestProgram() + testProg.ssa.CreatePackage(types.Unsafe, nil, nil, true) + runtimePkg := testProg.addPackage(t, llssa.PkgRuntime, coroStringConcatRuntimeFixture) + fooPkg := testProg.addPackage(t, "foo", coroStringConcatFixture) + testProg.ssa.Build() + + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + prog.SetRuntime(runtimePkg.types) + universe, err := prepareStacklessEmissionUniverseWithOptions(prog, nil, []EmissionPackage{ + {SSA: runtimePkg.ssa, Files: []*ast.File{runtimePkg.file}}, + {SSA: fooPkg.ssa, Files: []*ast.File{fooPkg.file}}, + }, EmissionUniverseOptions{CompleteRuntimeABI: true}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(fooPkg.ssa.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := fooPkg.ssa.Func("Root") + concats := coroStringConcatBinOps(t, root) + stringCat := runtimePkg.ssa.Func("StringCat") + pause := fooPkg.ssa.Func("Pause") + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + config := coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + switch function { + case stringCat: + return coro.SSAFunctionPolicy{Exec: coro.MayUnwind}, nil + case pause: + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + default: + return coro.SSAFunctionPolicy{}, nil + } + }, + } + if loweredCalls { + config.ClassifyLoweredCalls = universe.CoroLoweredCalls + } + plan, err := coro.AnalyzeSSA(fooPkg.ssa.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, config) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return coroStringConcatTestPlan{ + prog: prog, + runtimePkg: runtimePkg, + fooPkg: fooPkg, + universe: universe, + plan: plan, + root: root, + concats: concats, + } +} + +func coroStringConcatBinOps(t *testing.T, function *ssa.Function) []*ssa.BinOp { + t.Helper() + var found []*ssa.BinOp + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + operation, ok := instruction.(*ssa.BinOp) + if ok && operation.Op == token.ADD { + if basic, ok := types.Unalias(operation.Type()).Underlying().(*types.Basic); ok && basic.Kind() == types.String { + found = append(found, operation) + } + } + } + } + if len(found) != 2 { + t.Fatalf("%s string concatenations = %d, want two\n%s", function, len(found), function.String()) + } + return found +} diff --git a/cl/coro_timer_sleep.go b/cl/coro_timer_sleep.go new file mode 100644 index 0000000000..da94ca5053 --- /dev/null +++ b/cl/coro_timer_sleep.go @@ -0,0 +1,175 @@ +/* + * 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" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroTimerParkHookV2 = "__llgo_coro_timer_park_v2" + coroControlledTimerParkHookV2 = "__llgo_coro_timer_park_controlled_v2" + coroTimerResumeHookV2 = "__llgo_coro_timer_resume_v2" +) + +const ( + coroTimerResumeSuccessV2 uint64 = iota + 1 + coroTimerResumeOperationCanceledV2 + coroTimerResumeTaskAbortV2 + coroTimerResumeShutdownV2 +) + +func coroTimerParkSignatureV2() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + types.NewParam(token.NoPos, nil, "header", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + types.NewParam(token.NoPos, nil, "delay", types.Typ[types.Int64]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func coroTimerResumeSignatureV2() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + ) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "status", types.Typ[types.Uint32])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func coroControlledTimerParkSignatureV2() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + types.NewParam(token.NoPos, nil, "header", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + types.NewParam(token.NoPos, nil, "controller", pointer), + types.NewParam(token.NoPos, nil, "control", types.NewPointer(types.Typ[types.Uint32])), + types.NewParam(token.NoPos, nil, "expected", types.Typ[types.Uint32]), + types.NewParam(token.NoPos, nil, "deadline", types.Typ[types.Int64]), + ) + return types.NewSignatureType(nil, nil, nil, params, nil, false) +} + +func (p *context) requireCoroTimerSleepBody(b llssa.Builder) *coroBodyContext { + return p.requireCoroParkV2Body(b, "timer Sleep") +} + +// compileCoroTimerSleep lowers the synchronous source-style time.Sleep +// intrinsic into one compiler-owned TimerParkV2 transaction. The opaque state +// is a typed local so LLVM's coroutine passes spill its fixed layout into the +// stackless frame; source code never owns a frame pointer or source identity. +func (p *context) compileCoroTimerSleep(b llssa.Builder, args []ssa.Value) { + body := p.requireCoroTimerSleepBody(b) + if len(args) != 1 { + panic("llgo.coroTimerSleep requires exactly one int64 argument") + } + delay := p.compileValue(b, args[0]) + state := b.Alloc(p.prog.RuntimeType("CoroTimerParkV2"), false) + + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.Prog.BoolVal(true), + park: func(suspend llssa.Builder) { + park := p.pkg.NewFunc(coroTimerParkHookV2, coroTimerParkSignatureV2(), llssa.InC) + suspend.Call( + park.Expr, + body.task, + body.coro.Handle(), + suspend.Convert(suspend.Prog.VoidPtr(), body.header), + suspend.Convert(suspend.Prog.VoidPtr(), state), + delay, + ) + }, + resume: func(resume llssa.Builder) llssa.Expr { + resumeHook := p.pkg.NewFunc(coroTimerResumeHookV2, coroTimerResumeSignatureV2(), llssa.InC) + return resume.Call( + resumeHook.Expr, + body.task, + resume.Convert(resume.Prog.VoidPtr(), state), + ) + }, + normal: []uint64{coroTimerResumeSuccessV2}, + abort: coroTimerResumeTaskAbortV2, + shutdown: coroTimerResumeShutdownV2, + }) +} + +// compileCoroControlledTimerWait lowers the standard Timer manager's +// synchronous-style wait into the same source-aware TimerParkV2 transaction as +// Sleep, augmented only with the logical Stop/Reset identity. Completed and +// operation-canceled are returned after exact lease cleanup and recycle; +// task abort/shutdown enter compiler cleanup and never return to the manager. +func (p *context) compileCoroControlledTimerWait(b llssa.Builder, args []ssa.Value) llssa.Expr { + body := p.requireCoroTimerSleepBody(b) + if len(args) != 4 { + panic("llgo.coroControlledTimerWait requires exactly (unsafe.Pointer, *uint32, uint32, int64) arguments") + } + controller := p.compileValue(b, args[0]) + control := p.compileValue(b, args[1]) + expected := p.compileValue(b, args[2]) + deadline := p.compileValue(b, args[3]) + state := b.Alloc(p.prog.RuntimeType("CoroTimerParkV2"), false) + result := b.Alloc(p.prog.Uint32(), false) + + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.Prog.BoolVal(true), + park: func(suspend llssa.Builder) { + park := p.pkg.NewFunc(coroControlledTimerParkHookV2, coroControlledTimerParkSignatureV2(), llssa.InC) + suspend.Call( + park.Expr, + body.task, + body.coro.Handle(), + suspend.Convert(suspend.Prog.VoidPtr(), body.header), + suspend.Convert(suspend.Prog.VoidPtr(), state), + controller, + control, + expected, + deadline, + ) + }, + resume: func(resume llssa.Builder) llssa.Expr { + resumeHook := p.pkg.NewFunc(coroTimerResumeHookV2, coroTimerResumeSignatureV2(), llssa.InC) + status := resume.Call( + resumeHook.Expr, + body.task, + resume.Convert(resume.Prog.VoidPtr(), state), + ) + resume.Store(result, status) + return status + }, + normal: []uint64{ + coroTimerResumeSuccessV2, + coroTimerResumeOperationCanceledV2, + }, + abort: coroTimerResumeTaskAbortV2, + shutdown: coroTimerResumeShutdownV2, + }) + // The timer table deliberately owns only a scalar controller key. This + // post-resume use makes the address-shaped owner and its interior control + // pointer live across llvm.coro.suspend until source retirement completes. + b.KeepAlive(controller, control) + return b.Load(result) +} diff --git a/cl/coro_timer_sleep_test.go b/cl/coro_timer_sleep_test.go new file mode 100644 index 0000000000..2d121861c2 --- /dev/null +++ b/cl/coro_timer_sleep_test.go @@ -0,0 +1,327 @@ +//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 ( + "bytes" + "go/importer" + "go/token" + "go/types" + "regexp" + "strconv" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroTimerSleepTestSource = `package foo + +import _ "unsafe" + +//go:linkname sleep llgo.coroTimerSleep +func sleep(delay int64) + +func Root(delay int64) int64 { + before := delay + 7 + sleep(delay) + return before + delay +} +` + +const coroControlledTimerWaitTestSource = `package foo + +import "unsafe" + +//go:linkname wait llgo.coroControlledTimerWait +func wait(controller unsafe.Pointer, control *uint32, expected uint32, deadline int64) uint32 + +func Root(controller unsafe.Pointer, control *uint32, expected uint32, deadline int64) uint32 { + return wait(controller, control, expected, deadline) +} +` + +func TestCoroTimerSleepCurrentFrameNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root, sleepCall := compileCoroTimerSleepFixture(t, test.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || rootPlan.FuncRep != coro.DirectCoro || + !rootPlan.DeclaredEffect.Contains(coro.MayPark) || !rootPlan.LocalEffect.Contains(coro.MayPark) || + !rootPlan.Effect.Contains(coro.MayPark) { + t.Fatalf("Root plan = %+v, present=%t; want one local timer-park coroutine", rootPlan, ok) + } + if !plan.ElidesCall(sleepCall) { + t.Fatal("coroTimerSleep declaration call is not frozen as a frontend-elided intrinsic site") + } + if _, retained := plan.CallPlan(sleepCall); retained { + t.Fatal("coroTimerSleep declaration unexpectedly retained a managed CallPlan") + } + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify timer Sleep coroutine before CoroSplit: %v\n%s", err, module.String()) + } + physical := requireCoroPhysicalFunction(t, module, "foo.Root") + body := physical.String() + assertCoroCancellationTerminalStatusPublication(t, physical) + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("Root coro.suspend calls = %d, want initial + timer + final:\n%s", got, body) + } + for _, symbol := range []string{coroTimerParkHookV2, coroTimerResumeHookV2} { + if got := strings.Count(body, "@"+symbol); got != 1 { + t.Fatalf("Root references to %q = %d, want 1:\n%s", symbol, got, body) + } + } + for _, forbidden := range []string{"@foo.sleep", "@llgo.coroTimerSleep", "runtime.AllocZ"} { + if strings.Contains(body, forbidden) { + t.Fatalf("timer Sleep lowering retained forbidden call/allocation %q:\n%s", forbidden, body) + } + } + stateAndPark := regexp.MustCompile( + `(?s)store i16 4,.*store i16 3,.*store i32 1,.*call void @` + regexp.QuoteMeta(coroTimerParkHookV2) + + `\(ptr [^,]+, ptr [^,]+, ptr [^,]+, ptr [^,]+, i64 [^)]+\)`, + ) + if !stateAndPark.MatchString(body) { + t.Fatalf("Root does not publish Park/Suspended/stateID=1 before Timer V2 park:\n%s", body) + } + park := strings.Index(body, "call void @"+coroTimerParkHookV2) + suspendRelative := strings.Index(body[park:], "call i8 @llvm.coro.suspend") + resumeRelative := strings.Index(body[park:], "call i32 @"+coroTimerResumeHookV2) + if park < 0 || suspendRelative < 0 || resumeRelative < 0 || suspendRelative >= resumeRelative { + t.Fatalf("Root does not park, suspend, then consume Timer V2 status in order:\n%s", body) + } + dispatch := regexp.MustCompile( + `(?s)call i32 @` + regexp.QuoteMeta(coroTimerResumeHookV2) + `\([^\n]+\)\n\s+switch i32 [^\[]+\[(.*?)\]`, + ).FindStringSubmatch(body) + if len(dispatch) != 2 { + t.Fatalf("Root has no isolated Timer V2 resume switch:\n%s", body) + } + for _, status := range []uint64{coroTimerResumeSuccessV2, coroTimerResumeTaskAbortV2, coroTimerResumeShutdownV2} { + if !regexp.MustCompile(`(?m)^\s+i32 ` + strconv.FormatUint(status, 10) + `, label `).MatchString(dispatch[1]) { + t.Fatalf("Root Timer V2 resume switch lacks status %d:\n%s", status, dispatch[0]) + } + } + if regexp.MustCompile(`(?m)^\s+i32 ` + strconv.FormatUint(coroTimerResumeOperationCanceledV2, 10) + `, label `).MatchString(dispatch[1]) { + t.Fatalf("ordinary Sleep accepts an operation-only cancellation status:\n%s", dispatch[0]) + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), "call i32 @"+coroTimerResumeHookV2) { + t.Fatalf("CoroSplit lost Timer V2 resume dispatch:\n%s", module.String()) + } + assertCoroCancellationTerminalStatusPublication(t, resume) + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit post-CoroSplit timer Sleep object: %v\n%s", err, module.String()) + } + defer object.Dispose() + for _, symbol := range []string{coroTimerParkHookV2, coroTimerResumeHookV2} { + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(symbol)) { + t.Fatalf("post-CoroSplit object lost Timer V2 ABI symbol %q", symbol) + } + } + }) + } +} + +func TestCoroControlledTimerWaitCurrentFrameNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, test := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(test.name, func(t *testing.T) { + prog, pkg, plan, root, waitCall := compileCoroTimerIntrinsicFixture( + t, test.target, coroControlledTimerWaitTestSource, "wait", + ) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + rootPlan, ok := plan.FunctionPlan(root) + if !ok || rootPlan.Emission != coro.EmitCoroutine || !rootPlan.Effect.Contains(coro.MayPark) || + !plan.ElidesCall(waitCall) { + t.Fatalf("controlled Timer Root plan = %+v, present=%t, elided=%t", rootPlan, ok, plan.ElidesCall(waitCall)) + } + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify controlled timer coroutine before CoroSplit: %v\n%s", err, module.String()) + } + physical := requireCoroPhysicalFunction(t, module, "foo.Root") + body := physical.String() + if got := strings.Count(body, "call i8 @llvm.coro.suspend"); got != 3 { + t.Fatalf("controlled Timer coro.suspend calls = %d, want initial + timer + final:\n%s", got, body) + } + for _, symbol := range []string{coroControlledTimerParkHookV2, coroTimerResumeHookV2} { + if got := strings.Count(body, "@"+symbol); got != 1 { + t.Fatalf("controlled Timer references to %q = %d, want 1:\n%s", symbol, got, body) + } + } + for _, forbidden := range []string{"@foo.wait", "@llgo.coroControlledTimerWait", "runtime.AllocZ"} { + if strings.Contains(body, forbidden) { + t.Fatalf("controlled Timer lowering retained forbidden call/allocation %q:\n%s", forbidden, body) + } + } + dispatch := regexp.MustCompile( + `(?s)call i32 @` + regexp.QuoteMeta(coroTimerResumeHookV2) + `\([^\n]+\)\n\s+store i32 [^\n]+\n\s+switch i32 [^\[]+\[(.*?)\]`, + ).FindStringSubmatch(body) + if len(dispatch) != 2 { + t.Fatalf("controlled Timer has no isolated V2 resume switch:\n%s", body) + } + for _, status := range []uint64{ + coroTimerResumeSuccessV2, + coroTimerResumeOperationCanceledV2, + coroTimerResumeTaskAbortV2, + coroTimerResumeShutdownV2, + } { + if !regexp.MustCompile(`(?m)^\s+i32 ` + strconv.FormatUint(status, 10) + `, label `).MatchString(dispatch[1]) { + t.Fatalf("controlled Timer V2 resume switch lacks status %d:\n%s", status, dispatch[0]) + } + } + + runCoroABITestPipeline(t, prog, module) + resume := module.NamedFunction("foo.Root$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), "call i32 @"+coroTimerResumeHookV2) { + t.Fatalf("CoroSplit lost controlled Timer V2 resume dispatch:\n%s", module.String()) + } + }) + } +} + +func compileCoroTimerSleepFixture(t *testing.T, target *llssa.Target) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, *ssa.Call, +) { + return compileCoroTimerIntrinsicFixture(t, target, coroTimerSleepTestSource, "sleep") +} + +func compileCoroTimerIntrinsicFixture(t *testing.T, target *llssa.Target, source, calleeName string) ( + llssa.Program, llssa.Package, *coro.SSAPlan, *ssa.Function, *ssa.Call, +) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + prog.SetRuntime(func() *types.Package { + runtimePackage, err := importer.For("source", nil).Import(llssa.PkgRuntime) + if err != nil { + t.Fatal("load runtime failed:", err) + } + if runtimePackage.Scope().Lookup("CoroTimerParkV2") == nil { + name := types.NewTypeName(token.NoPos, runtimePackage, "CoroTimerParkV2", nil) + types.NewNamed(name, types.NewArray(types.Typ[types.Uintptr], 32), nil) + if previous := runtimePackage.Scope().Insert(name); previous != nil { + t.Fatalf("install Timer V2 test runtime type: duplicate %v", previous) + } + } + return runtimePackage + }) + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func("Root") + var intrinsicCall *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && call.Call.StaticCallee() != nil && call.Call.StaticCallee().Name() == calleeName { + intrinsicCall = call + } + } + } + if intrinsicCall == nil { + prog.Dispose() + t.Fatalf("fixture has no direct %s intrinsic call", calleeName) + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(intrinsicCall) + if err != nil || !intrinsic || semantics != CoroIntrinsicCallInlineSuspend { + prog.Dispose() + t.Fatalf("%s semantics = %v, %t, %v; want InlineSuspend, true, nil", calleeName, semantics, intrinsic, err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root { + return coro.SSAFunctionPolicy{Effect: coro.MayPark}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + if callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(call) + return intrinsic && semantics.ElidesManagedCall(), err + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{ + CoroPlan: plan, + EmissionUniverse: universe, + CoroFrameRetentionABI: CoroFrameRetentionParkABIV2, + } + enableCoroPreemptCompilation(compilation) + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, root, intrinsicCall +} diff --git a/cl/coro_trusted_inline_call.go b/cl/coro_trusted_inline_call.go new file mode 100644 index 0000000000..ab3e4b9e4e --- /dev/null +++ b/cl/coro_trusted_inline_call.go @@ -0,0 +1,166 @@ +/* + * 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 ( + "fmt" + "strconv" + + "github.com/goplus/llgo/internal/coro" + "golang.org/x/tools/go/ssa" +) + +const coroTrustedInlineCallCertificateDomain = "llgo-coro-trusted-inline-call-certificate-v1" + +// freezeCoroTrustedInlineCallCertificates turns one deliberately narrow source +// policy into exact invocation capabilities: +// +// - the caller is an annotated, bodyful Go wrapper that promises +// executor-safe progress; +// - the callee is one exact bodyless C declaration whose conservative +// contract remains may-block; +// - the callee itself owns an executor-safe trusted-inline refinement under +// the same frozen callable ABI; and +// - the source edge is an ordinary static *ssa.Call. +// +// The wrapper annotation is trusted frontend policy, but it cannot upgrade an +// arbitrary target: the target-owned refinement, exact SSA edge and physical +// ABI certificate are all required. The later SSA fixed point independently +// checks that the complete wrapper body actually satisfies its claimed +// executor-safe summary. +func (u *EmissionUniverse) freezeCoroTrustedInlineCallCertificates() error { + if u == nil { + return fmt.Errorf("prepare emission universe: cannot freeze trusted-inline calls in a nil universe") + } + u.trustedInlineCalls = make(map[ssa.CallInstruction]coro.SSATrustedInlineCallCertificate) + + for _, caller := range u.functions { + caller = u.canonicalAlias(caller) + if caller == nil || len(caller.Blocks) == 0 { + continue + } + callerCertificate, ok := u.callableContracts[caller] + if !ok || callerCertificate.Scope != coro.CallableContractScopeWrapper || + callerCertificate.Contract.Progress != coro.ProgressExecutorSafe { + continue + } + callerIdentity := u.finalIdentity(caller) + if callerIdentity == "" || callerIdentity == "" || callerIdentity == "" { + return fmt.Errorf("prepare emission universe: trusted-inline wrapper %q has no exact canonical identity", caller.Name()) + } + + for _, block := range caller.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call == nil || call.Parent() != caller || call.Common() == nil || call.Common().IsInvoke() { + continue + } + target := u.canonicalAlias(call.Common().StaticCallee()) + if target == nil || target == caller { + continue + } + targetCertificate, ok := u.callableContracts[target] + if !ok || !coroTrustedInlineTargetEligible(targetCertificate) { + continue + } + if _, required := u.required[target]; !required { + continue + } + semantic, err := coro.SemanticInstructionOrdinal(call) + if err != nil { + return fmt.Errorf("prepare emission universe: identify trusted-inline call in %q: %w", caller.Name(), err) + } + targetIdentity := u.finalIdentity(target) + if targetIdentity == "" || targetIdentity == "" || targetIdentity == "" { + return fmt.Errorf("prepare emission universe: trusted-inline target %q has no exact canonical identity", target.Name()) + } + certificate := coro.SSATrustedInlineCallCertificate{ + ID: emissionDigest(framedEmissionKey( + coroTrustedInlineCallCertificateDomain, + callerCertificate.ID, + targetCertificate.ID, + callerIdentity, + strconv.Itoa(block.Index), + strconv.Itoa(semantic), + targetIdentity, + string(targetCertificate.TrustedInlineContract.ID), + targetCertificate.CallableABI, + )), + Contract: targetCertificate.TrustedInlineContract.ID, + ABI: targetCertificate.CallableABI, + } + u.trustedInlineCalls[call] = certificate + } + } + } + return nil +} + +// coroTrustedInlineTargetEligible describes what the current direct physical +// path can enforce. The default target remains conservative and may project +// ThreadAffine/OpaqueExec; the exact invocation substitutes the target-owned +// selected projection in graph analysis. The selected refinement itself must +// require no affinity/reentry/lifetime adapter because this path emits one +// direct call on the current runnable executor. +func coroTrustedInlineTargetEligible(certificate CoroCallableContractCertificate) bool { + if certificate.IsZero() || certificate.Scope != coro.CallableContractScopeDeclaration || + !certificate.HasTrustedInlineContract || + certificate.TrustedInlineContract.Progress != coro.ProgressExecutorSafe || + coro.CallableContractExecConstraints(certificate.TrustedInlineContract) != 0 { + return false + } + if err := certificate.Validate(); err != nil { + return false + } + switch certificate.Contract.Progress { + case coro.ProgressUnknown, coro.ProgressMayBlock, coro.ProgressAsyncCompletion: + return true + default: + // ExecutorSafe needs no edge refinement; NoReturn cannot safely refine to + // a returning executor-safe invocation. + return false + } +} + +// CoroTrustedInlineCallCertificate returns the immutable capability for one +// exact wrapper call occurrence. Absence is ordinary Auto policy. The lookup is +// keyed by the SSA instruction itself, never by a code/data address, name or +// reconstructed physical symbol. +func (u *EmissionUniverse) CoroTrustedInlineCallCertificate( + caller *ssa.Function, + call ssa.CallInstruction, +) (certificate coro.SSATrustedInlineCallCertificate, certified bool, err error) { + if u == nil { + return certificate, false, fmt.Errorf("coroutine trusted-inline call certificate: nil emission universe") + } + direct, ok := call.(*ssa.Call) + if !ok || direct == nil || direct.Common() == nil || direct.Common().IsInvoke() { + return certificate, false, nil + } + canonicalCaller := u.canonicalAlias(caller) + if canonicalCaller == nil { + return certificate, false, fmt.Errorf("coroutine trusted-inline call certificate: caller has cyclic canonical aliases") + } + if direct.Parent() != canonicalCaller { + return certificate, false, nil + } + if _, required := u.required[canonicalCaller]; !required { + return certificate, false, nil + } + certificate, certified = u.trustedInlineCalls[direct] + return certificate, certified, nil +} diff --git a/cl/coro_trusted_inline_call_test.go b/cl/coro_trusted_inline_call_test.go new file mode 100644 index 0000000000..622b2d3f2f --- /dev/null +++ b/cl/coro_trusted_inline_call_test.go @@ -0,0 +1,120 @@ +//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/ast" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" +) + +func TestEmissionUniverseFreezesOnlyExactWrapperTrustedInlineCalls(t *testing.T) { + testProg := newEmissionTestProgram() + pkg := testProg.addPackage(t, "example.com/emission/trustedinline", `package trustedinline + +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=borrow-until-return inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none inline-memory=borrow-until-return +//go:linkname Foreign C.trusted_inline_foreign +func Foreign(int) int + +//llgo:coro contract foreign.v1 progress=may-block affinity=unknown reentry=none memory=borrow-until-return inline-progress=executor-safe inline-affinity=owner-thread inline-reentry=none inline-memory=borrow-until-return +//go:linkname NeedsAdapter C.trusted_inline_needs_adapter +func NeedsAdapter(int) int + +//llgo:coro contract foreign.v1 progress=unknown affinity=unknown reentry=unknown memory=unknown inline-progress=executor-safe inline-affinity=any-thread inline-reentry=none inline-memory=borrow-until-return +//go:linkname UnknownDefault C.trusted_inline_unknown_default +func UnknownDefault(int) int + +//llgo:coro contract foreign.v1 progress=may-block affinity=any-thread reentry=none memory=borrow-until-return +//go:linkname NoRefinement C.trusted_inline_no_refinement +func NoRefinement(int) int + +//llgo:coro contract foreign.v1 scope=wrapper progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +func Fast(value int) int { return Foreign(value) } + +//llgo:coro contract foreign.v1 scope=wrapper progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +func UnknownFast(value int) int { return UnknownDefault(value) } + +//llgo:coro contract foreign.v1 scope=wrapper progress=may-block affinity=caller-thread reentry=none memory=borrow-until-return +func Auto(value int) int { return Foreign(value) } + +//llgo:coro contract foreign.v1 scope=wrapper progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +func AdapterMissing(value int) int { return NeedsAdapter(value) } + +//llgo:coro contract foreign.v1 scope=wrapper progress=executor-safe affinity=caller-thread reentry=none memory=borrow-until-return +func RefinementMissing(value int) int { return NoRefinement(value) } + +func root(value int) int { return Fast(value) + UnknownFast(value) + Auto(value) + AdapterMissing(value) + RefinementMissing(value) } +`) + testProg.ssa.Build() + program := llssa.NewProgram(nil) + defer program.Dispose() + universe, err := prepareStacklessEmissionUniverse(program, nil, []EmissionPackage{{ + SSA: pkg.ssa, Files: []*ast.File{pkg.file}, Identity: "trusted-inline-owner", + }}) + if err != nil { + t.Fatal(err) + } + + fast := pkg.ssa.Func("Fast") + fastCall := findStaticCallByName(t, fast, "Foreign") + certificate, certified, err := universe.CoroTrustedInlineCallCertificate(fast, fastCall) + if err != nil || !certified || len(certificate.ID) != 64 { + t.Fatalf("Fast trusted-inline certificate = %+v, %t, %v", certificate, certified, err) + } + target, ok, err := universe.CoroCallableContractCertificate(pkg.ssa.Func("Foreign")) + if err != nil || !ok { + t.Fatalf("Foreign callable certificate = %+v, %t, %v", target, ok, err) + } + if certificate.Contract != target.TrustedInlineContract.ID || certificate.ABI != target.CallableABI { + t.Fatalf("Fast certificate = %+v; target = %+v", certificate, target) + } + unknownFast := pkg.ssa.Func("UnknownFast") + unknownCall := findStaticCallByName(t, unknownFast, "UnknownDefault") + unknownCertificate, certified, err := universe.CoroTrustedInlineCallCertificate(unknownFast, unknownCall) + if err != nil || !certified || len(unknownCertificate.ID) != 64 { + t.Fatalf("UnknownFast trusted-inline certificate = %+v, %t, %v", unknownCertificate, certified, err) + } + unknownTarget, ok, err := universe.CoroCallableContractCertificate(pkg.ssa.Func("UnknownDefault")) + if err != nil || !ok || coro.CallableContractExecConstraints(unknownTarget.Contract) != coro.ThreadAffine|coro.OpaqueExec || + coro.CallableContractExecConstraints(unknownTarget.TrustedInlineContract) != 0 || + unknownCertificate.Contract != unknownTarget.TrustedInlineContract.ID || unknownCertificate.ABI != unknownTarget.CallableABI { + t.Fatalf("UnknownFast certificate = %+v; target = %+v, %t, %v", unknownCertificate, unknownTarget, ok, err) + } + + for _, test := range []struct { + caller string + target string + }{ + {caller: "Auto", target: "Foreign"}, + {caller: "AdapterMissing", target: "NeedsAdapter"}, + {caller: "RefinementMissing", target: "NoRefinement"}, + } { + caller := pkg.ssa.Func(test.caller) + call := findStaticCallByName(t, caller, test.target) + got, ok, err := universe.CoroTrustedInlineCallCertificate(caller, call) + if err != nil || ok || got != (coro.SSATrustedInlineCallCertificate{}) { + t.Fatalf("%s trusted-inline certificate = %+v, %t, %v; want absent", test.caller, got, ok, err) + } + } + if got, ok, err := universe.CoroTrustedInlineCallCertificate(pkg.ssa.Func("Auto"), fastCall); err != nil || ok || got != (coro.SSATrustedInlineCallCertificate{}) { + t.Fatalf("certificate replay under wrong caller = %+v, %t, %v; want absent", got, ok, err) + } +} diff --git a/cl/coro_uintptr_observation_test.go b/cl/coro_uintptr_observation_test.go new file mode 100644 index 0000000000..d2a1247674 --- /dev/null +++ b/cl/coro_uintptr_observation_test.go @@ -0,0 +1,412 @@ +//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 ( + "bytes" + "go/types" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" + "golang.org/x/tools/go/ssa/ssautil" +) + +const coroUintptrObservationFixture = `package foo + +import "unsafe" + +func Endpoint(first, last unsafe.Pointer, offset uintptr) bool { + return uintptr(first) <= uintptr(last)+offset +} + +func Overlaps(a, b []byte) bool { + if len(a) == 0 || len(b) == 0 { return false } + elemSize := unsafe.Sizeof(a[0]) + if elemSize == 0 { return false } + return uintptr(unsafe.Pointer(&a[0])) <= uintptr(unsafe.Pointer(&b[len(b)-1]))+(elemSize-1) && + uintptr(unsafe.Pointer(&b[0])) <= uintptr(unsafe.Pointer(&a[len(a)-1]))+(elemSize-1) +} + +type OverlapRecord struct { + Code uint32 + Text string +} + +func GenericOverlaps[E any](a, b []E) bool { + if len(a) == 0 || len(b) == 0 { return false } + elemSize := unsafe.Sizeof(a[0]) + if elemSize == 0 { return false } + return uintptr(unsafe.Pointer(&a[0])) <= uintptr(unsafe.Pointer(&b[len(b)-1]))+(elemSize-1) && + uintptr(unsafe.Pointer(&b[0])) <= uintptr(unsafe.Pointer(&a[len(a)-1]))+(elemSize-1) +} + +func UseGenericOverlaps(a, b []OverlapRecord) bool { return GenericOverlaps(a, b) } +` + +func TestCoroPointerUintptrGenericAffineObservationShape(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, coroUintptrObservationFixture) + origin := ssaPkg.Func("GenericOverlaps") + var instance *ssa.Function + for function := range ssautil.AllFunctions(ssaPkg.Prog) { + if function == nil || function.Origin() != origin || len(function.TypeArgs()) != 1 { + continue + } + named, ok := types.Unalias(function.TypeArgs()[0]).(*types.Named) + if ok && named.Obj() != nil && named.Obj().Name() == "OverlapRecord" { + instance = function + break + } + } + if instance == nil { + t.Fatal("GenericOverlaps[OverlapRecord] instance was not materialized") + } + found, accepted, instructions := 0, 0, 0 + for _, block := range instance.Blocks { + for _, instruction := range block.Instrs { + if _, debug := instruction.(*ssa.DebugRef); !debug { + instructions++ + } + conversion, ok := instruction.(*ssa.Convert) + if !ok || !coroFrameRetentionPointerToUintptr(conversion) { + continue + } + found++ + if coroPointerUintptrScalarTerminal(conversion) { + accepted++ + } + } + } + if found != 4 || accepted != found { + var dump bytes.Buffer + ssa.WriteFunction(&dump, instance) + t.Fatalf("generic overlaps pointer words found=%d accepted=%d, want four exact scalar terminals\n%s", found, accepted, dump.String()) + } + if instructions > coro.DefaultMaxPlainInstructions { + t.Fatalf("generic overlaps instruction count=%d unexpectedly exceeds default preemption budget=%d", instructions, coro.DefaultMaxPlainInstructions) + } + + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: instance, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == instance { + return coro.SSAFunctionPolicy{Exec: coro.MayUnwind}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + functionPlan, planned := plan.FunctionPlan(instance) + if !planned || functionPlan.Emission != coro.EmitCoroutine || functionPlan.Exec.Contains(coro.NeedsPreempt) || + functionPlan.Effect&^coro.OutcomeStructured != coro.NoSuspend { + t.Fatalf("generic overlaps plan = %+v, present=%t; want non-preempting outcome-only coroutine", functionPlan, planned) + } + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, instance, "") + if err != nil { + t.Fatal(err) + } + for _, block := range instance.Blocks { + for _, instruction := range block.Instrs { + conversion, ok := instruction.(*ssa.Convert) + if !ok || !coroFrameRetentionPointerToUintptr(conversion) { + continue + } + if reason := audit.validateConvert(conversion); reason != "" { + t.Fatalf("generic overlaps active pointer-word validation rejected %q: %s", conversion, reason) + } + } + } +} + +func TestCoroPointerUintptrAffineObservationNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, target := range []struct { + name string + target *llssa.Target + uintptrType string + }{ + {name: "native", uintptrType: "i64"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}, uintptrType: "i32"}, + } { + t.Run(target.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroUintptrObservationFixture(t, target.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify uintptr observation before CoroSplit: %v\n%s", err, module.String()) + } + + for name, function := range functions { + functionPlan, ok := plan.FunctionPlan(function) + if !ok || functionPlan.Emission != coro.EmitCoroutine || + functionPlan.Exec.Contains(coro.NeedsPreempt) || + functionPlan.Effect&^coro.OutcomeStructured != coro.NoSuspend { + t.Fatalf("%s plan = %+v, present=%t; want non-preempting outcome-only coroutine", name, functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if strings.Contains(body, coroAwaitPrepareHookV1) || strings.Contains(body, coroPreemptPollHookV1) { + t.Fatalf("%s scalar observation acquired an await/preempt hook:\n%s", name, body) + } + if got := strings.Count(body, "ptrtoint ptr"); got < 2 || !strings.Contains(body, "to "+target.uintptrType) { + t.Fatalf("%s ptrtoint lowering is incomplete for %s (count=%d):\n%s", name, target.uintptrType, got, body) + } + } + assertCoroUintptrAffineIR(t, "Endpoint", requireCoroPhysicalFunction(t, module, "foo.Endpoint").String()) + + runCoroABITestPipeline(t, prog, module) + for name := range functions { + resume := module.NamedFunction("foo." + name + "$coro.resume") + if resume.IsNil() { + t.Fatalf("post-split %s has no resume function", name) + } + resumeIR := resume.String() + if strings.Contains(resumeIR, coroAwaitPrepareHookV1) || strings.Contains(resumeIR, coroPreemptPollHookV1) { + t.Fatalf("post-split %s acquired an await/preempt hook:\n%s", name, resumeIR) + } + } + assertCoroUintptrAffineIR(t, "Endpoint resume", module.NamedFunction("foo.Endpoint$coro.resume").String()) + + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit uintptr observation object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 { + t.Fatal("uintptr observation emitted an empty object") + } + }) + } +} + +func TestCoroPointerUintptrAffineObservationRemainsFailClosed(t *testing.T) { + for _, test := range []struct { + name string + body string + }{ + {name: "return", body: "return uintptr(pointer) + offset"}, + {name: "store", body: "escaped = uintptr(pointer) + offset; return 0"}, + {name: "call", body: "consume(uintptr(pointer) + offset); return 0"}, + {name: "multiply", body: "return (uintptr(pointer) * offset) == 0"}, + {name: "reconstruct", body: "return uintptr(unsafe.Pointer(uintptr(pointer) + offset)) == 0"}, + {name: "pointer offset", body: "return uintptr(pointer) <= uintptr(other) + uintptr(pointer)"}, + } { + t.Run(test.name, func(t *testing.T) { + result := "uintptr" + if strings.Contains(test.body, "==") || strings.Contains(test.body, "<=") { + result = "bool" + } + source := `package foo +import "unsafe" +var escaped uintptr +func consume(uintptr) +func Root(pointer, other unsafe.Pointer, offset uintptr) ` + result + ` { ` + test.body + ` } +` + ssaPkg, _, _ := buildGoSSAPkg(t, source) + root := ssaPkg.Func("Root") + found := 0 + accepted := 0 + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + conversion, ok := instruction.(*ssa.Convert) + if !ok || !coroFrameRetentionPointerToUintptr(conversion) { + continue + } + found++ + if coroPointerUintptrScalarTerminal(conversion) { + accepted++ + } + } + } + if found == 0 { + t.Fatal("negative fixture has no pointer-to-uintptr conversion") + } + if accepted == found { + t.Fatalf("all %d unsafe pointer words acquired scalar-terminal authority:\n%s", found, root.String()) + } + }) + } +} + +func TestCoroPointerUintptrAffineObservationRequiresNonPreemptingPlan(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, coroUintptrObservationFixture) + root := ssaPkg.Func("Endpoint") + prog := newLLSSAProg(t) + defer prog.Dispose() + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if function == root { + return coro.SSAFunctionPolicy{Exec: coro.MayUnwind | coro.NeedsPreempt}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + t.Fatal(err) + } + functionPlan, ok := plan.FunctionPlan(root) + if !ok || !functionPlan.Exec.Contains(coro.NeedsPreempt) { + t.Fatalf("preempting fixture plan = %+v, present=%t", functionPlan, ok) + } + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "") + if err != nil { + t.Fatal(err) + } + var conversion *ssa.Convert + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + candidate, ok := instruction.(*ssa.Convert) + if ok && coroFrameRetentionPointerToUintptr(candidate) && coroPointerUintptrScalarTerminal(candidate) { + conversion = candidate + } + } + } + if conversion == nil { + t.Fatal("preempting fixture has no structural affine scalar terminal") + } + if reason := audit.validateConvert(conversion); !strings.Contains(reason, "not bound to an exact managed-child/worker") { + t.Fatalf("NeedsPreempt affine observation rejection = %q", reason) + } +} + +func assertCoroUintptrAffineIR(t *testing.T, name, body string) { + t.Helper() + secondPointer := strings.LastIndex(body, "ptrtoint ptr") + if secondPointer < 0 { + t.Fatalf("%s has no affine pointer word:\n%s", name, body) + } + affine := body[secondPointer:] + add, comparison := strings.Index(affine, " add "), strings.Index(affine, "icmp ule") + if add < 0 || comparison < add { + t.Fatalf("%s does not lower pointer+offset before comparison:\n%s", name, body) + } + span := affine[:comparison] + for _, hook := range []string{coroAwaitPrepareHookV1, coroPreemptPollHookV1, "llvm.coro.suspend"} { + if strings.Contains(span, hook) { + t.Fatalf("%s affine pointer lifetime crosses %s:\n%s", name, hook, body) + } + } +} + +func compileCoroUintptrObservationFixture( + t *testing.T, + target *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, map[string]*ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroUintptrObservationFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := map[string]*ssa.Function{ + "Endpoint": ssaPkg.Func("Endpoint"), + "Overlaps": ssaPkg.Func("Overlaps"), + } + var roots coro.Roots + for _, function := range functions { + roots = append(roots, coro.Root{Function: function, Demand: coro.AsyncDemand}) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + OutcomeMode: coro.OutcomeExplicitStatus, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + for _, root := range functions { + if function == root { + // MayUnwind forces an explicit OutcomeStructured physical body + // without adding a real suspension or preemption capability. + return coro.SSAFunctionPolicy{Exec: coro.MayUnwind}, nil + } + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, functions +} diff --git a/cl/coro_unsafe_slice.go b/cl/coro_unsafe_slice.go new file mode 100644 index 0000000000..efe908ab34 --- /dev/null +++ b/cl/coro_unsafe_slice.go @@ -0,0 +1,115 @@ +/* + * 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 ( + "fmt" + "go/types" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// validateUnsafeSliceBuiltin freezes x/tools' exact unsafe.Slice SSA shape. +// The logical AssertRuntimeError edge belongs to ordinary LLSSA lowering; the +// PhysicalABIV1 path below replaces it completely with compiler-owned terminal +// branches, so no native-stack panic helper may remain in the coroutine body. +func (a *coroPhysicalPureSSAAudit) validateUnsafeSliceBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || call.Type() == nil { + return "unsafe.Slice builtin has an incomplete call/result shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "Slice" || len(call.Common().Args) != 2 { + return "unsafe.Slice validation requires the exact two-argument builtin call" + } + pointerValue, lengthValue := call.Common().Args[0], call.Common().Args[1] + if pointerValue == nil || lengthValue == nil { + return "unsafe.Slice has a nil pointer or length SSA operand" + } + pointerType, ok := types.Unalias(a.typeOf(pointerValue.Type())).Underlying().(*types.Pointer) + if !ok { + return "unsafe.Slice first operand is not pointer-shaped" + } + lengthType, ok := types.Unalias(a.typeOf(lengthValue.Type())).Underlying().(*types.Basic) + if !ok || lengthType.Info()&types.IsInteger == 0 || lengthType.Info()&types.IsUntyped != 0 { + return "unsafe.Slice length is not a concrete integer" + } + resultType, ok := types.Unalias(a.typeOf(call.Type())).Underlying().(*types.Slice) + if !ok { + return "unsafe.Slice result is not slice-shaped" + } + if !types.Identical(a.typeOf(pointerType.Elem()), a.typeOf(resultType.Elem())) { + return "unsafe.Slice pointer and result element types differ" + } + for name, typ := range map[string]types.Type{ + "pointer": pointerValue.Type(), + "length": lengthValue.Type(), + "result": call.Type(), + } { + if err := validateCoroPhysicalSSAValueType(a.typeOf(typ)); err != nil { + return fmt.Sprintf("unsafe.Slice %s has unsupported physical type: %v", name, err) + } + } + if !a.allowImplicitNilFault { + return "unsafe.Slice requires the explicit-status panic ABI" + } + return a.requireOnlyCompilerElidedRuntimeHelpers(call, "AssertRuntimeError") +} + +// compileCoroUnsafeSlice lowers unsafe.Slice as pure pointer/integer SSA plus +// ordered explicit-status faults. Both source operands have already been +// evaluated in Go order. The slice aggregate is formed only in the continuation +// dominated by all target-width length, nil, multiplication, and address-span +// checks. +func (p *context) compileCoroUnsafeSlice( + b llssa.Builder, + call *ssa.CallCommon, + pointerValue, lengthValue llssa.Expr, +) llssa.Expr { + body := p.coroBody() + if body == nil || b == nil || b.Func != p.fn || call == nil || + !p.coroEmissionExplicitStatus() || + body.abi.version < coroPhysicalABIVersionV1 { + panic("unsafe.Slice coroutine lowering requires the PhysicalABIV1 explicit-status ABI") + } + results := call.Signature().Results() + if results == nil || results.Len() != 1 || len(call.Args) != 2 || + pointerValue.IsNil() || lengthValue.IsNil() { + panic("unsafe.Slice coroutine lowering lost its exact call shape") + } + resultType := p.patchType(results.At(0).Type()) + resultSlice, ok := types.Unalias(resultType).Underlying().(*types.Slice) + if !ok { + panic("unsafe.Slice coroutine result is not slice-shaped") + } + pointerType, ok := types.Unalias(p.patchType(call.Args[0].Type())).Underlying().(*types.Pointer) + if !ok || !types.Identical(p.patchType(pointerType.Elem()), p.patchType(resultSlice.Elem())) { + panic("unsafe.Slice coroutine pointer/result element types differ") + } + elemSize := p.prog.SizeOf(p.type_(resultSlice.Elem(), llssa.InGo)) + length, preLenFault, nilFault, spanLenFault := b.UnsafeSliceGuardConditions( + pointerValue, + lengthValue, + elemSize, + ) + p.compileCoroFaultConditionGuard(b, preLenFault, coroFaultUnsafeSliceLenV1) + p.compileCoroFaultConditionGuard(b, nilFault, coroFaultUnsafeSliceNilV1) + if elemSize != 0 { + p.compileCoroFaultConditionGuard(b, spanLenFault, coroFaultUnsafeSliceLenV1) + } + return b.Aggregate(p.type_(resultType, llssa.InGo), pointerValue, length, length) +} diff --git a/cl/coro_unsafe_slice_test.go b/cl/coro_unsafe_slice_test.go new file mode 100644 index 0000000000..94da274137 --- /dev/null +++ b/cl/coro_unsafe_slice_test.go @@ -0,0 +1,275 @@ +//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 ( + "bytes" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroUnsafeSliceFixture = `package foo +import "unsafe" + +type Triple struct { A, B, C byte } +type Zero struct{} + +func Bytes(pointer *byte, length int) []byte { return unsafe.Slice(pointer, length) } +func WideUnsigned(pointer *byte, length uint64) []byte { return unsafe.Slice(pointer, length) } +func WideSigned(pointer *byte, length int64) []byte { return unsafe.Slice(pointer, length) } +func Triples(pointer *Triple, length uintptr) []Triple { return unsafe.Slice(pointer, length) } +func Zeros(pointer *Zero, length int) []Zero { return unsafe.Slice(pointer, length) } +func NilZero() []byte { return unsafe.Slice((*byte)(nil), 0) } +func NilOne() []byte { return unsafe.Slice((*byte)(nil), 1) } +func MakeString(pointer *byte, length int) string { return unsafe.String(pointer, length) } +func WideString(pointer *byte, length uint64) string { return unsafe.String(pointer, length) } +func NilStringZero() string { return unsafe.String((*byte)(nil), 0) } +func NilStringOne() string { return unsafe.String((*byte)(nil), 1) } +func StringBytes(value string) *byte { return unsafe.StringData(value) } +func SliceBytes(value []byte) *byte { return unsafe.SliceData(value) } +` + +func TestCoroUnsafeSliceNativeAndWasm32(t *testing.T) { + llssa.Initialize(llssa.InitAll) + for _, target := range []struct { + name string + target *llssa.Target + }{ + {name: "native"}, + {name: "wasm32", target: &llssa.Target{GOOS: "wasip1", GOARCH: "wasm"}}, + } { + t.Run(target.name, func(t *testing.T) { + prog, pkg, plan, functions := compileCoroUnsafeSliceFixture(t, target.target) + defer prog.Dispose() + module := pkg.Module() + defer module.Dispose() + + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify unsafe.Slice before CoroSplit: %v\n%s", err, module.String()) + } + for name, wantFaults := range map[string]int{ + "Bytes": 3, "WideUnsigned": 3, "WideSigned": 3, "Triples": 3, + "Zeros": 2, "NilZero": 3, "NilOne": 3, + } { + functionPlan, ok := plan.FunctionPlan(functions[name]) + if !ok || functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want may-unwind coroutine", name, functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if got := strings.Count(body, "call void @"+coroFaultPrepareHookV1); got != wantFaults { + t.Fatalf("%s fault calls = %d, want %d:\n%s", name, got, wantFaults, body) + } + if strings.Contains(body, "AssertRuntimeError") || !strings.Contains(body, "i32 4") || !strings.Contains(body, "i32 5") { + t.Fatalf("%s retained helper or lost exact unsafe.Slice fault kinds:\n%s", name, body) + } + if name != "NilZero" && name != "NilOne" { + if hook, aggregate := strings.Index(body, "call void @"+coroFaultPrepareHookV1), strings.LastIndex(body, "insertvalue"); hook < 0 || aggregate < hook { + t.Fatalf("%s formed its slice before the terminal fault edges:\n%s", name, body) + } + } + } + for _, name := range []string{"MakeString", "WideString", "NilStringZero", "NilStringOne"} { + functionPlan, ok := plan.FunctionPlan(functions[name]) + if !ok || functionPlan.Emission != coro.EmitCoroutine || !functionPlan.Exec.Contains(coro.MayUnwind) { + t.Fatalf("%s plan = %+v, present=%t; want may-unwind coroutine", name, functionPlan, ok) + } + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if got := strings.Count(body, "call void @"+coroFaultPrepareHookV1); got != 3 { + t.Fatalf("%s fault calls = %d, want 3:\n%s", name, got, body) + } + if strings.Contains(body, "AssertRuntimeError") || !strings.Contains(body, "i32 8") || !strings.Contains(body, "i32 9") { + t.Fatalf("%s retained helper or lost exact unsafe.String fault kinds:\n%s", name, body) + } + } + + triples := requireCoroPhysicalFunction(t, module, "foo.Triples").String() + if !strings.Contains(triples, " mul ") || !strings.Contains(triples, "icmp ugt") { + t.Fatalf("three-byte element did not retain multiplication/span overflow checks:\n%s", triples) + } + zeros := requireCoroPhysicalFunction(t, module, "foo.Zeros").String() + if strings.Contains(zeros, "ptrtoint") || strings.Contains(zeros, " mul ") { + t.Fatalf("zero-sized element emitted an address-span calculation:\n%s", zeros) + } + if target.name == "wasm32" { + for _, name := range []string{"WideUnsigned", "WideSigned"} { + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if !strings.Contains(body, "trunc i64") || !strings.Contains(body, "icmp ne i64") { + t.Fatalf("%s omitted the wasm32 wide-length round trip:\n%s", name, body) + } + } + wideString := requireCoroPhysicalFunction(t, module, "foo.WideString").String() + if !strings.Contains(wideString, "trunc i64") || !strings.Contains(wideString, "icmp ne i64") { + t.Fatalf("WideString omitted the wasm32 wide-length round trip:\n%s", wideString) + } + } + for _, name := range []string{"StringBytes", "SliceBytes"} { + body := requireCoroPhysicalFunction(t, module, "foo."+name).String() + if !strings.Contains(body, "extractvalue") { + t.Fatalf("%s did not remain a pure header projection:\n%s", name, body) + } + } + + runCoroABITestPipeline(t, prog, module) + object, err := prog.TargetMachine().EmitToMemoryBuffer(module, llvm.ObjectFile) + if err != nil { + t.Fatalf("emit unsafe.Slice object: %v\n%s", err, module.String()) + } + defer object.Dispose() + if len(object.Bytes()) == 0 || !bytes.Contains(object.Bytes(), []byte(coroFaultPrepareHookV1)) { + t.Fatal("post-CoroSplit object lost the unsafe.Slice fault hook") + } + }) + } +} + +func TestCoroUnsafeSlicePureAuditFailsClosed(t *testing.T) { + ssaPkg, _, _ := buildGoSSAPkg(t, coroUnsafeSliceFixture) + function := ssaPkg.Func("Bytes") + call := coroUnsafeSliceBuiltinCall(t, function) + audit := &coroPhysicalPureSSAAudit{fn: function, reachableBlocks: coroPhysicalConstantReachableBlocks(function)} + if reason := audit.validateUnsafeSliceBuiltin(call); !strings.Contains(reason, "explicit-status panic ABI") { + t.Fatalf("legacy unsafe.Slice rejection = %q", reason) + } + audit.allowImplicitNilFault = true + if reason := audit.validateUnsafeSliceBuiltin(call); reason != "" { + t.Fatalf("explicit-status unsafe.Slice rejection = %q", reason) + } + stringFunction := ssaPkg.Func("MakeString") + stringCall := coroUnsafeBuiltinCall(t, stringFunction, "String") + stringAudit := &coroPhysicalPureSSAAudit{fn: stringFunction, reachableBlocks: coroPhysicalConstantReachableBlocks(stringFunction)} + if reason := stringAudit.validateUnsafeStringBuiltin(stringCall); !strings.Contains(reason, "explicit-status panic ABI") { + t.Fatalf("legacy unsafe.String rejection = %q", reason) + } + stringAudit.allowImplicitNilFault = true + if reason := stringAudit.validateUnsafeStringBuiltin(stringCall); reason != "" { + t.Fatalf("explicit-status unsafe.String rejection = %q", reason) + } + for _, test := range []struct { + function string + builtin string + }{ + {function: "StringBytes", builtin: "StringData"}, + {function: "SliceBytes", builtin: "SliceData"}, + } { + function := ssaPkg.Func(test.function) + call := coroUnsafeBuiltinCall(t, function, test.builtin) + dataAudit := &coroPhysicalPureSSAAudit{fn: function, reachableBlocks: coroPhysicalConstantReachableBlocks(function)} + if reason := dataAudit.validateUnsafeDataBuiltin(call, test.builtin); reason != "" { + t.Fatalf("unsafe.%s rejection = %q", test.builtin, reason) + } + } +} + +func compileCoroUnsafeSliceFixture( + t *testing.T, + target *llssa.Target, +) (llssa.Program, llssa.Package, *coro.SSAPlan, map[string]*ssa.Function) { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, coroUnsafeSliceFixture) + var prog llssa.Program + if target == nil { + prog = newLLSSAProg(t) + } else { + prog = newLLSSAProgForTarget(t, target) + } + universe, err := prepareStacklessEmissionUniverse(prog, nil, []EmissionPackage{{SSA: ssaPkg, Files: files}}) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + functions := make(map[string]*ssa.Function) + var roots coro.Roots + for _, name := range []string{ + "Bytes", "WideUnsigned", "WideSigned", "Triples", "Zeros", "NilZero", "NilOne", + "MakeString", "WideString", "NilStringZero", "NilStringOne", "StringBytes", "SliceBytes", + } { + function := ssaPkg.Func(name) + functions[name] = function + roots = append(roots, coro.Root{Function: function, Demand: coro.AsyncDemand}) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, roots, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, FunctionIDs: functionIDs, MaxPlainInstructions: -1, + ClassifyFunction: func(function *ssa.Function) (coro.SSAFunctionPolicy, error) { + if functions[function.Name()] == function { + return coro.SSAFunctionPolicy{Effect: coro.YieldOnly}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + compilation := &Compilation{CoroPlan: plan, EmissionUniverse: universe} + enableCoroChildAwaitCompilation(compilation) + compilation.CoroProfile = CoroProfileStackless + compilation.PanicABI = coro.PanicExplicitStatusABIV0 + pkg, _, err := NewPackageExWithEmbedOptions( + prog, nil, nil, nil, ssaPkg, files, goembed.VarMap{}, PackageOptions{Compilation: compilation}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, plan, functions +} + +func coroUnsafeSliceBuiltinCall(t *testing.T, function *ssa.Function) *ssa.Call { + return coroUnsafeBuiltinCall(t, function, "Slice") +} + +func coroUnsafeBuiltinCall(t *testing.T, function *ssa.Function, name string) *ssa.Call { + t.Helper() + var found *ssa.Call + for _, block := range function.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok { + continue + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != name { + continue + } + if found != nil { + t.Fatalf("%s has more than one unsafe.Slice builtin", function) + } + found = call + } + } + if found == nil { + t.Fatalf("%s has no unsafe.%s builtin", function, name) + } + return found +} diff --git a/cl/coro_unsafe_string.go b/cl/coro_unsafe_string.go new file mode 100644 index 0000000000..4e55617e79 --- /dev/null +++ b/cl/coro_unsafe_string.go @@ -0,0 +1,102 @@ +/* + * 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 ( + "fmt" + "go/types" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// validateUnsafeStringBuiltin freezes x/tools' exact unsafe.String SSA shape. +// The ordinary AssertRuntimeError calls are replaced by ordered explicit- +// status terminal edges in a physical coroutine. +func (a *coroPhysicalPureSSAAudit) validateUnsafeStringBuiltin(call *ssa.Call) string { + if call == nil || call.Common() == nil || call.Type() == nil { + return "unsafe.String builtin has an incomplete call/result shape" + } + builtin, ok := call.Common().Value.(*ssa.Builtin) + if !ok || builtin.Name() != "String" || len(call.Common().Args) != 2 { + return "unsafe.String validation requires the exact two-argument builtin call" + } + pointerValue, lengthValue := call.Common().Args[0], call.Common().Args[1] + if pointerValue == nil || lengthValue == nil { + return "unsafe.String has a nil pointer or length SSA operand" + } + pointerType, ok := types.Unalias(a.typeOf(pointerValue.Type())).Underlying().(*types.Pointer) + if !ok || !types.Identical(types.Unalias(a.typeOf(pointerType.Elem())), types.Typ[types.Byte]) { + return "unsafe.String first operand is not *byte" + } + lengthType, ok := types.Unalias(a.typeOf(lengthValue.Type())).Underlying().(*types.Basic) + if !ok || lengthType.Info()&types.IsInteger == 0 || lengthType.Info()&types.IsUntyped != 0 { + return "unsafe.String length is not a concrete integer" + } + resultType, ok := types.Unalias(a.typeOf(call.Type())).Underlying().(*types.Basic) + if !ok || resultType.Kind() != types.String { + return "unsafe.String result is not string-shaped" + } + for name, typ := range map[string]types.Type{ + "pointer": pointerValue.Type(), + "length": lengthValue.Type(), + "result": call.Type(), + } { + if err := validateCoroPhysicalSSAValueType(a.typeOf(typ)); err != nil { + return fmt.Sprintf("unsafe.String %s has unsupported physical type: %v", name, err) + } + } + if !a.allowImplicitNilFault { + return "unsafe.String requires the explicit-status panic ABI" + } + return a.requireOnlyCompilerElidedRuntimeHelpers(call, "AssertRuntimeError") +} + +// compileCoroUnsafeString shares the target-width span arithmetic used by +// unsafe.Slice with element width one, but publishes the distinct Go-required +// unsafe.String panic payloads and forms a two-word string header only after +// all guards succeed. +func (p *context) compileCoroUnsafeString( + b llssa.Builder, + call *ssa.CallCommon, + pointerValue, lengthValue llssa.Expr, +) llssa.Expr { + body := p.coroBody() + if body == nil || b == nil || b.Func != p.fn || call == nil || + !p.coroEmissionExplicitStatus() || + body.abi.version < coroPhysicalABIVersionV1 { + panic("unsafe.String coroutine lowering requires the PhysicalABIV1 explicit-status ABI") + } + results := call.Signature().Results() + if results == nil || results.Len() != 1 || len(call.Args) != 2 || pointerValue.IsNil() || lengthValue.IsNil() { + panic("unsafe.String coroutine lowering lost its exact call shape") + } + resultType := p.patchType(results.At(0).Type()) + resultBasic, ok := types.Unalias(resultType).Underlying().(*types.Basic) + if !ok || resultBasic.Kind() != types.String { + panic("unsafe.String coroutine result is not string-shaped") + } + pointerType, ok := types.Unalias(p.patchType(call.Args[0].Type())).Underlying().(*types.Pointer) + if !ok || !types.Identical(types.Unalias(p.patchType(pointerType.Elem())), types.Typ[types.Byte]) { + panic("unsafe.String coroutine pointer is not *byte") + } + length, preLenFault, nilFault, spanLenFault := b.UnsafeSliceGuardConditions(pointerValue, lengthValue, 1) + p.compileCoroFaultConditionGuard(b, preLenFault, coroFaultUnsafeStringLenV1) + p.compileCoroFaultConditionGuard(b, nilFault, coroFaultUnsafeStringNilV1) + p.compileCoroFaultConditionGuard(b, spanLenFault, coroFaultUnsafeStringLenV1) + return b.Aggregate(p.type_(resultType, llssa.InGo), pointerValue, length) +} diff --git a/cl/coro_worker.go b/cl/coro_worker.go new file mode 100644 index 0000000000..dd3a38a3e0 --- /dev/null +++ b/cl/coro_worker.go @@ -0,0 +1,237 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const ( + coroWorkerParkHookV1 = "__llgo_coro_worker_park_v1" + coroWorkerResumeHookV1 = "__llgo_coro_worker_resume_v1" +) + +const ( + coroWorkerResumeSuccessV1 uint64 = iota + 1 + coroWorkerResumeTaskAbortV1 + coroWorkerResumeShutdownV1 +) + +func coroWorkerParkSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + params := []*types.Var{ + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "handle", pointer), + types.NewParam(token.NoPos, nil, "header", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + types.NewParam(token.NoPos, nil, "function", types.Typ[types.Uintptr]), + types.NewParam(token.NoPos, nil, "argc", types.Typ[types.Uint32]), + } + for index := 0; index < coroWorkerMaxArgsV1; index++ { + params = append(params, types.NewParam(token.NoPos, nil, fmt.Sprintf("a%d", index), types.Typ[types.Uintptr])) + } + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), nil, false) +} + +func coroWorkerResumeSignature() *types.Signature { + pointer := types.Typ[types.UnsafePointer] + wordPointer := types.NewPointer(types.Typ[types.Uintptr]) + params := types.NewTuple( + types.NewParam(token.NoPos, nil, "g", pointer), + types.NewParam(token.NoPos, nil, "state", pointer), + types.NewParam(token.NoPos, nil, "r1", wordPointer), + types.NewParam(token.NoPos, nil, "r2", wordPointer), + types.NewParam(token.NoPos, nil, "errno", wordPointer), + ) + results := types.NewTuple(types.NewParam(token.NoPos, nil, "status", types.Typ[types.Uint32])) + return types.NewSignatureType(nil, nil, nil, params, results, false) +} + +func (p *context) requireCoroWorkerBody(b llssa.Builder) *coroBodyContext { + body := p.coroBody() + if body == nil || b.Func != p.fn { + panic("coroutine worker lowering requires an active planned physical coroutine body") + } + if body.abi.version < coroPhysicalABIVersionV1 || body.completion == nil || + body.finalSuspend == nil || body.unsupportedRunDecision == nil { + panic("coroutine worker lowering requires the complete PhysicalABIV1 scheduler ABI") + } + return body +} + +type coroWorkerWordResultV1 struct { + r1 llssa.Expr + r2 llssa.Expr + errno llssa.Expr +} + +// compileCoroWorkerWordCall is the one physical ForeignWait transaction used +// by both llgo.syscall and exact ordinary C-call thunks. function always names +// a uniform uintptr (...uintptr) thunk whose arity is len(args); typed foreign +// declarations are never called through this ABI directly. +func (p *context) compileCoroWorkerWordCall( + b llssa.Builder, + function llssa.Expr, + args []llssa.Expr, + keepaliveSlots []llssa.Expr, +) coroWorkerWordResultV1 { + body := p.requireCoroWorkerBody(b) + if function.IsNil() || len(args) > coroWorkerMaxArgsV1 { + panic("coroutine worker word call received an invalid function or argument count") + } + word := p.prog.Uintptr() + if !types.Identical(function.RawType(), word.RawType()) { + panic("coroutine worker word call function is not uintptr-shaped") + } + for index, argument := range args { + if argument.IsNil() || !types.Identical(argument.RawType(), word.RawType()) { + panic(fmt.Sprintf("coroutine worker word call argument %d is not uintptr-shaped", index)) + } + } + + state := b.Alloc(p.prog.RuntimeType("CoroWorkerParkV1"), false) + r1 := b.Alloc(p.prog.Uintptr(), false) + r2 := b.Alloc(p.prog.Uintptr(), false) + errno := b.Alloc(p.prog.Uintptr(), false) + zero := p.prog.Zero(p.prog.Uintptr()) + physicalArgs := make([]llssa.Expr, 0, 6+coroWorkerMaxArgsV1) + physicalArgs = append(physicalArgs, + body.task, + body.coro.Handle(), + b.Convert(b.Prog.VoidPtr(), body.header), + b.Convert(b.Prog.VoidPtr(), state), + function, + p.prog.IntVal(uint64(len(args)), p.prog.Uint32()), + ) + for index := 0; index < coroWorkerMaxArgsV1; index++ { + if index < len(args) { + physicalArgs = append(physicalArgs, args[index]) + } else { + physicalArgs = append(physicalArgs, zero) + } + } + + body.emitCoroParkOperation(p, b, coroParkOperation{ + shouldSuspend: b.Prog.BoolVal(true), + park: func(suspend llssa.Builder) { + park := p.pkg.NewFunc(coroWorkerParkHookV1, coroWorkerParkSignature(), llssa.InC) + suspend.Call(park.Expr, physicalArgs...) + }, + resume: func(resume llssa.Builder) llssa.Expr { + resumeHook := p.pkg.NewFunc(coroWorkerResumeHookV1, coroWorkerResumeSignature(), llssa.InC) + return resume.Call( + resumeHook.Expr, + body.task, + resume.Convert(resume.Prog.VoidPtr(), state), + r1, + r2, + errno, + ) + }, + normal: []uint64{coroWorkerResumeSuccessV1}, + abort: coroWorkerResumeTaskAbortV1, + shutdown: coroWorkerResumeShutdownV1, + }) + // The worker queue deliberately contains only copied uintptr words. Keep + // every independently proved typed owner live until the physical completion + // acknowledgement has selected this normal resume path; llvm.fake.use emits + // no machine code but forces CoroSplit to retain the values in the frame. + p.emitCoroKeepaliveSlots(b, keepaliveSlots) + return coroWorkerWordResultV1{r1: b.Load(r1), r2: b.Load(r2), errno: b.Load(errno)} +} + +// compileCoroCallKeepaliveSlots spills the exact typed owners which the +// frame-retention proof binds to one suspending call into ramp-entry slots. +// Compiler-owned resume/cancellation dispatch can enter a continuation through +// an edge on which the source SSA value does not dominate. Reloading the slot +// in that continuation preserves both valid LLVM SSA and the typed owner until +// the physical completion/retirement boundary. +func (p *context) compileCoroCallKeepaliveSlots(b llssa.Builder, call *ssa.Call) []llssa.Expr { + body := p.coroBody() + if body == nil || body.frameRetention == nil || call == nil { + return nil + } + sources := body.frameRetention.exactCallKeepaliveSources(call) + slots := make([]llssa.Expr, len(sources)) + for index, source := range sources { + value := p.compileValue(b, source) + if coroFrameRetentionIntegerLike(source.Type()) { + // uintptr transports retain exact pointer provenance only under the + // selected non-moving conservative/no-GC profile. Re-type the copied + // word as a pointer in the compiler-owned keepalive slot so the frame + // carries an address-shaped root rather than an optimizer-only integer. + value = b.Convert(p.prog.VoidPtr(), value) + } + slots[index] = p.coroFrameAlloc(value.Type) + b.Store(slots[index], value) + } + return slots +} + +func (p *context) emitCoroKeepaliveSlots(b llssa.Builder, slots []llssa.Expr) { + values := make([]llssa.Expr, len(slots)) + for index, slot := range slots { + if slot.IsNil() { + panic("coroutine keepalive contains a nil frame slot") + } + values[index] = b.Load(slot) + } + b.KeepAlive(values...) +} + +func (p *context) coroWorkerOrdinaryCall(common *ssa.CallCommon) *ssa.Call { + if p == nil || p.goFn == nil || common == nil { + return nil + } + for _, block := range p.goFn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if ok && &call.Call == common { + return call + } + } + } + return nil +} + +// compileCoroWorkerSyscall lowers one source-style synchronous llgo.syscall +// family operation into the common ForeignWait recipe. All conventions share +// one park/resume CFG; only the final errno predicate differs. Argument +// evaluation happens before publication, and the fixed pool receives only +// copied uintptr words. +func (p *context) compileCoroWorkerSyscall( + b llssa.Builder, + call *ssa.CallCommon, + args []ssa.Value, + results *types.Tuple, + convention syscallFailureConvention, +) llssa.Expr { + direct := p.coroWorkerOrdinaryCall(call) + compiled := make([]llssa.Expr, len(args)) + for index, argument := range args { + compiled[index] = p.compileValue(b, argument) + } + keepaliveSlots := p.compileCoroCallKeepaliveSlots(b, direct) + result := p.compileCoroWorkerWordCall(b, compiled[0], compiled[1:], keepaliveSlots) + errnoValue := p.filterSyscallErrno(b, result.r1, result.errno, convention) + return b.Aggregate(p.type_(results, llssa.InGo), result.r1, result.r2, errnoValue) +} diff --git a/cl/coro_worker_foreign.go b/cl/coro_worker_foreign.go new file mode 100644 index 0000000000..f7aacb9627 --- /dev/null +++ b/cl/coro_worker_foreign.go @@ -0,0 +1,389 @@ +/* + * 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 ( + "fmt" + "go/token" + "go/types" + "strconv" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +const coroWorkerForeignThunkPrefixV1 = "__llgo_coro_worker_foreign_thunk_v1_" + +type coroWorkerForeignCallShape struct { + target *ssa.Function + signature *types.Signature + argc int + result types.Type +} + +func coroWorkerTypeParamLen(list *types.TypeParamList) int { + if list == nil { + return 0 + } + return list.Len() +} + +func coroWorkerTargetPointerSize(universe *EmissionUniverse) int { + if universe == nil || universe.prog == nil { + return 0 + } + return universe.prog.PointerSize() +} + +func coroWorkerWordType(typ types.Type, pointerSize int) bool { + if typ == nil || pointerSize <= 0 { + return false + } + underlying := types.Unalias(typ).Underlying() + switch underlying := underlying.(type) { + case *types.Pointer: + return true + case *types.Basic: + if underlying.Kind() == types.UnsafePointer { + return true + } + if underlying.Info()&types.IsInteger == 0 || underlying.Info()&types.IsUntyped != 0 { + return false + } + sizes := &types.StdSizes{WordSize: int64(pointerSize), MaxAlign: int64(pointerSize)} + size := sizes.Sizeof(typ) + return size > 0 && size <= int64(pointerSize) + default: + return false + } +} + +// coroWorkerArgumentWordType additionally admits an explicitly C-background +// named callback. LLGo represents such a value as one raw C function pointer, +// not as a managed Go closure/descriptor. This is needed for registration APIs +// that transport—but do not invoke—the callback during the worker call. Plain +// Go function types remain rejected. +func coroWorkerArgumentWordType(universe *EmissionUniverse, typ types.Type, pointerSize int) bool { + if coroWorkerWordType(typ, pointerSize) { + return true + } + if universe == nil || universe.prog == nil || pointerSize <= 0 || + universe.prog.TypeBackground(typ) != llssa.InC { + return false + } + signature, ok := types.Unalias(typ).Underlying().(*types.Signature) + return ok && signature != nil && !signature.Variadic() +} + +// coroWorkerResultWordType deliberately excludes pointer-shaped results. The +// native completion queue transports only untraced uintptr words; until a +// result-provenance capability can prove that a returned pointer is either +// non-Go storage or still owned by an exact retained root, reconstructing a Go +// pointer after the worker acknowledgement would create an unrooted interval. +func coroWorkerResultWordType(typ types.Type, pointerSize int) bool { + if typ == nil || pointerSize <= 0 { + return false + } + basic, ok := types.Unalias(typ).Underlying().(*types.Basic) + if !ok || basic.Info()&types.IsInteger == 0 || basic.Info()&types.IsUntyped != 0 { + return false + } + sizes := &types.StdSizes{WordSize: int64(pointerSize), MaxAlign: int64(pointerSize)} + size := sizes.Sizeof(typ) + return size > 0 && size <= int64(pointerSize) +} + +// validateCoroWorkerForeignAuthorization accepts exactly one of the legacy +// worker certificate and the target-neutral callable declaration contract. +// The latter is deliberately stricter than its general SSA classification: +// this lowering moves the physical call to an arbitrary bounded worker and +// waits for that invocation to return, so it cannot implement affinity, +// managed reentry, retained storage, asynchronous completion, or no-return +// semantics. Plan and frontend certificates are compared as complete values; +// an ID match alone cannot hide stale behavior or physical-ABI fields. +func validateCoroWorkerForeignAuthorization( + plan *coro.SSAPlan, + universe *EmissionUniverse, + target *ssa.Function, +) error { + if plan == nil || universe == nil || target == nil { + return fmt.Errorf("requires an exact coroutine plan, emission universe, and foreign target") + } + + planLegacy, planLegacyCertified := plan.ForeignWorkerCertificate(target) + universeLegacy, universeLegacyCertified, legacyErr := universe.CoroForeignWorkerCertificate(target) + if legacyErr != nil { + return fmt.Errorf("resolve frozen legacy worker-safe certificate: %w", legacyErr) + } + planCallable, planCallableCertified := plan.CallableContractCertificate(target) + universeCallable, universeCallableCertified, callableErr := universe.CoroCallableContractCertificate(target) + if callableErr != nil { + return fmt.Errorf("resolve frozen callable contract certificate: %w", callableErr) + } + + legacyPresent := planLegacyCertified || planLegacy != "" || universeLegacyCertified || + universeLegacy != (CoroForeignWorkerCertificate{}) + callablePresent := planCallableCertified || !planCallable.IsZero() || universeCallableCertified || + !universeCallable.IsZero() + if legacyPresent && callablePresent { + return fmt.Errorf("generic callable contract and legacy worker-safe certificates are mutually exclusive") + } + + if legacyPresent { + if !planLegacyCertified || planLegacy == "" { + return fmt.Errorf("target has no exact legacy worker-safe certificate in the coroutine plan") + } + if !universeLegacyCertified || universeLegacy.ID == "" || universeLegacy.PhysicalSymbol == "" || universeLegacy.ABISignature == "" { + return fmt.Errorf("target has no exact legacy worker-safe certificate in the frozen emission universe") + } + if planLegacy != universeLegacy.ID { + return fmt.Errorf("legacy worker-safe certificate identity differs between the coroutine plan and frozen emission universe") + } + return nil + } + + if !callablePresent { + return fmt.Errorf("target has no exact worker-safe certificate or compatible callable declaration contract") + } + if !planCallableCertified || planCallable.IsZero() { + return fmt.Errorf("target has no exact callable contract certificate in the coroutine plan") + } + if !universeCallableCertified || universeCallable.IsZero() { + return fmt.Errorf("target has no exact callable contract certificate in the frozen emission universe") + } + if planCallable != universeCallable { + return fmt.Errorf("callable contract certificate differs between the coroutine plan and frozen emission universe") + } + if err := universeCallable.Validate(); err != nil { + return fmt.Errorf("invalid callable contract certificate: %w", err) + } + if universeCallable.Scope != coro.CallableContractScopeDeclaration { + return fmt.Errorf("callable contract scope %q does not authorize a worker C declaration", universeCallable.Scope) + } + if universeCallable.CallableABIExplicit { + if _, addressOnly := parseCoroWorkerWordCallableABI(universeCallable.CallableABI); addressOnly { + return fmt.Errorf( + "callable ABI %q is address-only and may be consumed only by the FuncPCABI0-to-llgo.syscall worker path, not an ordinary typed foreign call", + universeCallable.CallableABI, + ) + } + } + contract := universeCallable.Contract + if contract.Progress != coro.ProgressMayBlock { + return fmt.Errorf("callable progress %q does not authorize bounded worker lowering; require %q", contract.Progress, coro.ProgressMayBlock) + } + if contract.Affinity != coro.AffinityAnyThread { + return fmt.Errorf("callable affinity %q does not authorize arbitrary worker-thread execution; require %q", contract.Affinity, coro.AffinityAnyThread) + } + if contract.Reentry != coro.ReentryNone { + return fmt.Errorf("callable reentry %q does not authorize callback-free worker execution; require %q", contract.Reentry, coro.ReentryNone) + } + switch contract.Memory { + case coro.MemoryByValue, coro.MemoryBorrowUntilReturn, coro.MemoryBorrowUntilComplete: + return nil + default: + return fmt.Errorf("callable memory lifetime %q does not authorize bounded worker transport", contract.Memory) + } +} + +// validateCoroWorkerForeignCall recognizes only an ordinary, closed CallForeign +// edge to one exact frontend C declaration. recognized distinguishes a malformed +// foreign edge (which must fail closed) from an unrelated call. +func validateCoroWorkerForeignCall( + plan *coro.SSAPlan, + universe *EmissionUniverse, + call *ssa.Call, + pointerSize int, +) (shape coroWorkerForeignCallShape, recognized bool, err error) { + if plan == nil || universe == nil || call == nil || call.Common() == nil { + return shape, false, nil + } + callPlan, planned := plan.CallPlan(call) + if !planned || callPlan.Kind != coro.CallForeign { + return shape, false, nil + } + recognized = true + common := call.Common() + if call.Parent() == nil { + return shape, true, fmt.Errorf("call has no exact SSA owner") + } + raw := common.StaticCallee() + if raw == nil || common.IsInvoke() || common.Method != nil { + return shape, true, fmt.Errorf("requires one exact static ordinary call") + } + if callPlan.Open || callPlan.MayBeNil || callPlan.Rep != coro.DirectPlain || len(callPlan.Targets) != 1 { + return shape, true, fmt.Errorf( + "requires one closed non-nil direct-plain target, got open=%t may-be-nil=%t representation=%s targets=%d", + callPlan.Open, callPlan.MayBeNil, callPlan.Rep, len(callPlan.Targets), + ) + } + target, frozen := universe.Resolve(raw) + if !frozen || target == nil { + return shape, true, fmt.Errorf("static target is absent from the frozen emission universe") + } + plannedTarget, ok := plan.Function(callPlan.Targets[0]) + if !ok || plannedTarget == nil || plannedTarget != target { + return shape, true, fmt.Errorf("call target %q does not identify the frozen static declaration", callPlan.Targets[0]) + } + targetPlan, ok := plan.FunctionPlan(target) + if !ok || targetPlan.ID != callPlan.Targets[0] { + return shape, true, fmt.Errorf("target has no canonical function plan") + } + background, classified, backgroundErr := universe.FunctionBackground(target) + if backgroundErr != nil { + return shape, true, fmt.Errorf("classify target frontend ABI: %w", backgroundErr) + } + if !classified || background != llssa.InC { + return shape, true, fmt.Errorf("target is not one exact frontend C declaration") + } + if authorizationErr := validateCoroWorkerForeignAuthorization(plan, universe, target); authorizationErr != nil { + return shape, true, authorizationErr + } + if targetPlan.External != coro.ExternalUnknownForeign || targetPlan.Emission != coro.EmitExternal || + targetPlan.Effect != coro.NoSuspend || targetPlan.Exec != coro.BlockForeign|coro.IRQUnsafe { + return shape, true, fmt.Errorf( + "target %q is not an exact blocking foreign declaration (external=%s emission=%s effect=%s exec=%s)", + targetPlan.ID, targetPlan.External, targetPlan.Emission, targetPlan.Effect, targetPlan.Exec, + ) + } + if target.Signature == nil || target.Signature.Recv() != nil || target.Signature.Variadic() || + coroWorkerTypeParamLen(target.Signature.TypeParams()) != 0 || + coroWorkerTypeParamLen(target.Signature.RecvTypeParams()) != 0 || + len(target.FreeVars) != 0 || target.Origin() != nil || len(target.TypeArgs()) != 0 { + return shape, true, fmt.Errorf("target is not a receiver-free, non-variadic, non-generic C declaration") + } + signature, signatureErr := universe.coroPhysicalSourceSignature(target) + if signatureErr != nil { + return shape, true, fmt.Errorf("derive target effective signature: %w", signatureErr) + } + if signature == nil || signature.Recv() != nil || signature.Variadic() { + return shape, true, fmt.Errorf("requires a non-variadic signature with zero to %d arguments", coroWorkerMaxArgsV1) + } + shape.argc = 0 + if signature.Params() != nil { + shape.argc = signature.Params().Len() + } + if shape.argc != len(common.Args) || shape.argc > coroWorkerMaxArgsV1 { + return shape, true, fmt.Errorf("requires a non-variadic signature with zero to %d arguments", coroWorkerMaxArgsV1) + } + owner := universe.ownerOf(call.Parent()) + ownerContext, contextErr := universe.functionABIContext(call.Parent(), owner) + if contextErr != nil { + return shape, true, fmt.Errorf("derive call-site effective signature: %w", contextErr) + } + callSignature, ok := ownerContext.patchType(common.Signature()).(*types.Signature) + if !ok || !types.Identical(coroPhysicalNormalizeSourceSignature(callSignature), signature) { + return shape, true, fmt.Errorf("call-site and target effective C signatures differ") + } + for index, argument := range common.Args { + if argument == nil { + return shape, true, fmt.Errorf("argument %d is nil", index) + } + argumentType := ownerContext.patchType(argument.Type()) + parameterType := signature.Params().At(index).Type() + if !types.Identical(argumentType, parameterType) { + return shape, true, fmt.Errorf("argument %d type does not match the effective C parameter", index) + } + if !coroWorkerArgumentWordType(universe, parameterType, pointerSize) { + return shape, true, fmt.Errorf("argument %d type %s is not losslessly word-packable integer/pointer data", index, parameterType) + } + } + results := signature.Results() + if results != nil && results.Len() > 1 { + return shape, true, fmt.Errorf("requires zero or one result") + } + if results != nil && results.Len() == 1 { + shape.result = results.At(0).Type() + if !coroWorkerResultWordType(shape.result, pointerSize) { + return coroWorkerForeignCallShape{}, true, fmt.Errorf( + "result type %s is not losslessly word-packable integer data", shape.result, + ) + } + } + shape.target = target + shape.signature = signature + return shape, true, nil +} + +func coroWorkerForeignThunkSignature(argc int) *types.Signature { + params := make([]*types.Var, argc) + for index := range params { + params[index] = types.NewParam(token.NoPos, nil, fmt.Sprintf("a%d", index), types.Typ[types.Uintptr]) + } + result := types.NewVar(token.NoPos, nil, "result", types.Typ[types.Uintptr]) + return types.NewSignatureType(nil, nil, nil, types.NewTuple(params...), types.NewTuple(result), false) +} + +func (p *context) coroWorkerForeignThunk(shape coroWorkerForeignCallShape, target llssa.Function) llssa.Function { + if p == nil || shape.target == nil || shape.signature == nil || target == nil { + panic("coroutine foreign worker thunk requires an exact target and signature") + } + key := framedEmissionKey( + "cl-coro-worker-foreign-thunk-v1", + target.Name(), + structuralEmissionABITypeKey(shape.signature), + strconv.Itoa(p.prog.PointerSize()), + ) + name := coroWorkerForeignThunkPrefixV1 + emissionDigest(key) + thunk := p.pkg.NewFuncEx(name, coroWorkerForeignThunkSignature(shape.argc), llssa.InC, false, true) + if thunk.HasBody() { + return thunk + } + b := thunk.MakeBody(1) + args := make([]llssa.Expr, shape.argc) + for index := range args { + args[index] = b.Convert(p.type_(shape.signature.Params().At(index).Type(), llssa.InC), thunk.Param(index)) + } + ret := b.Call(target.Expr, args...) + if shape.result == nil { + b.Return(p.prog.Zero(p.prog.Uintptr())) + } else { + b.Return(b.Convert(p.prog.Uintptr(), ret)) + } + b.EndBuild() + b.Dispose() + return thunk +} + +func (p *context) compileCoroWorkerForeignCall( + b llssa.Builder, call *ssa.Call, shape coroWorkerForeignCallShape, +) llssa.Expr { + if p == nil || !p.hasCoroPhysicalBody() || call == nil || shape.target == nil || shape.signature == nil { + panic("coroutine foreign worker lowering escaped its frozen physical operation recipe") + } + target, _, kind := p.compileFunction(shape.target) + if kind != cFunc || target == nil { + panic("coroutine foreign worker lowering lost its exact C target") + } + thunk := p.coroWorkerForeignThunk(shape, target) + oldInCFunc := p.inCFunc + p.inCFunc = true + compiled := p.compileValues(b, call.Common().Args, fnNormal) + p.inCFunc = oldInCFunc + words := make([]llssa.Expr, len(compiled)) + for index, argument := range compiled { + words[index] = b.Convert(p.prog.Uintptr(), argument) + } + function := b.Convert(p.prog.Uintptr(), thunk.Expr) + keepaliveSlots := p.compileCoroCallKeepaliveSlots(b, call) + result := p.compileCoroWorkerWordCall(b, function, words, keepaliveSlots) + if shape.result == nil { + return llssa.Expr{} + } + return b.Convert(p.type_(shape.result, llssa.InC), result.r1) +} diff --git a/cl/coro_worker_foreign_test.go b/cl/coro_worker_foreign_test.go new file mode 100644 index 0000000000..7ea440d81a --- /dev/null +++ b/cl/coro_worker_foreign_test.go @@ -0,0 +1,597 @@ +//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 ( + "fmt" + "go/ast" + "go/importer" + "go/token" + "go/types" + "regexp" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + "github.com/goplus/llgo/internal/goembed" + llssa "github.com/goplus/llgo/ssa" + "github.com/xgo-dev/llvm" + "golang.org/x/tools/go/ssa" +) + +const coroWorkerForeignTestSource = `package foreignworker + +import "unsafe" + +type FD int32 +type Count uintptr + +//llgo:coro worker +//go:linkname foreign C.foreign_word_probe +func foreign(FD, unsafe.Pointer, Count) FD + +func Root(fd FD, pointer unsafe.Pointer, count Count) FD { + return foreign(fd, pointer, count) +} +` + +const coroWorkerGenericForeignTestSource = `package foreignworker + +import "unsafe" + +type FD int32 +type Count uintptr + +//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete +//go:linkname foreign C.foreign_word_probe +func foreign(FD, unsafe.Pointer, Count) FD + +func Root(fd FD, pointer unsafe.Pointer, count Count) FD { + return foreign(fd, pointer, count) +} +` + +type preparedCoroWorkerForeignFixture struct { + prog llssa.Program + ssaPkg *ssa.Package + files []*ast.File + universe *EmissionUniverse + plan *coro.SSAPlan + root *ssa.Function + call *ssa.Call +} + +func coroWorkerCallableForeignSource(progress, affinity, reentry, memory string) string { + return fmt.Sprintf(`package foreignworker +import _ "unsafe" +//llgo:coro contract foreign.v1 scope=declaration progress=%s affinity=%s reentry=%s memory=%s +//go:linkname foreign C.foreign_callable_probe +func foreign(uintptr) uintptr +func Root(value uintptr) uintptr { return foreign(value) } +`, progress, affinity, reentry, memory) +} + +func prepareCoroWorkerForeignFixture(t *testing.T, source, rootName string) preparedCoroWorkerForeignFixture { + t.Helper() + ssaPkg, _, files := buildGoSSAPkg(t, source) + prog := newLLSSAProg(t) + prog.SetRuntime(func() *types.Package { + runtimePackage, err := importer.For("source", nil).Import(llssa.PkgRuntime) + if err != nil { + t.Fatal("load runtime failed:", err) + } + if runtimePackage.Scope().Lookup("CoroWorkerParkV1") == nil { + name := types.NewTypeName(token.NoPos, runtimePackage, "CoroWorkerParkV1", nil) + types.NewNamed(name, types.NewArray(types.Typ[types.Uintptr], 32), nil) + if previous := runtimePackage.Scope().Insert(name); previous != nil { + t.Fatalf("install test runtime type: duplicate %v", previous) + } + } + return runtimePackage + }) + // Production import records //llgo:type background metadata before the + // emission universe freezes physical signatures. Mirror that ordering so C + // callback word-shape tests exercise the real ABI. + ParsePkgSyntax(prog, ssaPkg.Pkg, files) + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, + nil, + []EmissionPackage{{SSA: ssaPkg, Files: files}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + ssaUniverse, err := coro.NewSSAEmissionUniverse(ssaPkg.Prog, universe.Functions()) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + root := ssaPkg.Func(rootName) + if root == nil { + prog.Dispose() + t.Fatalf("foreign worker fixture lacks root %q", rootName) + } + var foreignCall *ssa.Call + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Common().StaticCallee() == nil { + continue + } + background, classified, backgroundErr := universe.FunctionBackground(call.Common().StaticCallee()) + if backgroundErr == nil && classified && background == llssa.InC { + if foreignCall != nil { + prog.Dispose() + t.Fatalf("foreign worker root %q has multiple C calls", rootName) + } + foreignCall = call + } + } + } + if foreignCall == nil { + prog.Dispose() + t.Fatalf("foreign worker root %q has no exact C call", rootName) + } + functionIDs := universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelWorkerClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + plan, err := coro.AnalyzeSSA(ssaPkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + background, classified, backgroundErr := universe.FunctionBackground(fn) + if backgroundErr != nil { + return coro.SSAFunctionPolicy{}, backgroundErr + } + if classified && background == llssa.InC { + worker, workerCertified, workerErr := universe.CoroForeignWorkerCertificate(fn) + if workerErr != nil { + return coro.SSAFunctionPolicy{}, workerErr + } + callable, callableCertified, callableErr := universe.CoroCallableContractCertificate(fn) + if callableErr != nil { + return coro.SSAFunctionPolicy{}, callableErr + } + if workerCertified && callableCertified { + return coro.SSAFunctionPolicy{}, fmt.Errorf("mutually exclusive legacy worker and generic callable certificates") + } + if callableCertified { + external := coro.ExternalUnknownForeign + exec := coro.BlockForeign | coro.IRQUnsafe | coro.CallableContractExecConstraints(callable.Contract) + switch callable.Contract.Progress { + case coro.ProgressExecutorSafe: + external = coro.ExternalKnown + exec &^= coro.BlockForeign + case coro.ProgressMayBlock, coro.ProgressUnknown, coro.ProgressAsyncCompletion: + case coro.ProgressNoReturn: + exec |= coro.NoReturn + } + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: external, OverrideExternal: true, + Exec: exec, CallableContractCertificate: callable, + }, nil + } + identity := "" + if workerCertified { + identity = worker.ID + } + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: coro.ExternalUnknownForeign, OverrideExternal: true, + Exec: coro.BlockForeign | coro.IRQUnsafe, ForeignWorkerCertificate: identity, + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + return callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init", nil + }, + }) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return preparedCoroWorkerForeignFixture{ + prog: prog, ssaPkg: ssaPkg, files: files, universe: universe, + plan: plan, root: root, call: foreignCall, + } +} + +func TestCoroWorkerClosedForeignCallUsesTypedThunk(t *testing.T) { + llssa.Initialize(llssa.InitAll) + fixture := prepareCoroWorkerForeignFixture(t, coroWorkerGenericForeignTestSource, "Root") + defer fixture.prog.Dispose() + target := fixture.call.Common().StaticCallee() + planCertificate, planCertified := fixture.plan.CallableContractCertificate(target) + universeCertificate, universeCertified, certificateErr := fixture.universe.CoroCallableContractCertificate(target) + if certificateErr != nil || !planCertified || !universeCertified || planCertificate != universeCertificate { + t.Fatalf("generic worker callable certificates = plan:%+v/%t universe:%+v/%t err:%v", planCertificate, planCertified, universeCertificate, universeCertified, certificateErr) + } + if _, legacy := fixture.plan.ForeignWorkerCertificate(target); legacy { + t.Fatal("generic worker lowering unexpectedly retained a legacy worker certificate") + } + rootPlan, planned := fixture.plan.FunctionPlan(fixture.root) + if !planned || rootPlan.Emission != coro.EmitCoroutine || rootPlan.Primary != coro.PrimaryCoroutine || + rootPlan.LocalEffect != coro.NoSuspend || !rootPlan.Effect.Contains(coro.WaitForeign) { + t.Fatalf("Root plan = %+v, present=%t; want one call-edge wait-foreign coroutine", rootPlan, planned) + } + callPlan, planned := fixture.plan.CallPlan(fixture.call) + if !planned || callPlan.Kind != coro.CallForeign || callPlan.Open || callPlan.Rep != coro.DirectPlain || len(callPlan.Targets) != 1 { + t.Fatalf("foreign CallPlan = %+v, present=%t", callPlan, planned) + } + audit, err := newCoroPhysicalPureSSAAudit(fixture.universe, fixture.plan, fixture.root, "") + if err != nil { + t.Fatal(err) + } + if got := strings.Join(rootNames(audit.currentFrameRetentionProof().exactCallKeepaliveRoots(fixture.call)), ","); got != "pointer" { + t.Fatalf("foreign worker keepalive roots = %q, want pointer", got) + } + compilation := &Compilation{ + CoroPlan: fixture.plan, + EmissionUniverse: fixture.universe, + + CoroABI: coro.PhysicalABIV1, + SchedulerABI: coro.SchedulerProgramBootstrapChannelWorkerClosedStaticSpawnABIV0, + PanicABI: coro.PanicExplicitStatusABIV0, + FuncRepABI: coro.FuncRepABIV1, CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities(), + } + pkg, _, err := NewPackageExWithEmbedOptions( + fixture.prog, nil, nil, nil, fixture.ssaPkg, fixture.files, goembed.VarMap{}, + PackageOptions{Compilation: compilation}, + ) + if err != nil { + t.Fatal(err) + } + module := pkg.Module() + defer module.Dispose() + if err := llvm.VerifyModule(module, llvm.ReturnStatusAction); err != nil { + t.Fatalf("verify foreign worker coroutine: %v\n%s", err, module.String()) + } + body := requireCoroPhysicalFunction(t, module, "foreignworker.Root").String() + if strings.Contains(body, "@foreign_word_probe") { + t.Fatalf("coroutine body directly calls the typed foreign symbol:\n%s", body) + } + for _, symbol := range []string{coroWorkerParkHookV1, coroWorkerResumeHookV1} { + if got := strings.Count(body, "@"+symbol); got != 1 { + t.Fatalf("Root %q calls = %d, want one:\n%s", symbol, got, body) + } + } + if !strings.Contains(body, "call void (...) @llvm.fake.use(ptr") { + t.Fatalf("Root does not keep the typed pointer live after worker acknowledgement:\n%s", body) + } + if !regexp.MustCompile(`trunc i64 [^\n]+ to i32`).MatchString(body) { + t.Fatalf("Root does not unpack the signed 32-bit result from its worker word:\n%s", body) + } + var thunk llvm.Value + for function := module.FirstFunction(); !function.IsNil(); function = llvm.NextFunction(function) { + if strings.HasPrefix(function.Name(), coroWorkerForeignThunkPrefixV1) { + if !thunk.IsNil() { + t.Fatalf("module has multiple foreign thunks: %q and %q", thunk.Name(), function.Name()) + } + thunk = function + } + } + if thunk.IsNil() { + t.Fatalf("module has no typed foreign worker thunk:\n%s", module.String()) + } + thunkText := thunk.String() + for _, pattern := range []string{ + `define linkonce i64 @` + regexp.QuoteMeta(thunk.Name()) + `\(i64`, + `call i32 @foreign_word_probe\(i32`, + `inttoptr i64`, + `sext i32 [^\n]+ to i64`, + } { + if !regexp.MustCompile(pattern).MatchString(thunkText) { + t.Errorf("typed thunk lacks %q:\n%s", pattern, thunkText) + } + } + runCoroABITestPipeline(t, fixture.prog, module) + resume := module.NamedFunction("foreignworker.Root$coro.resume") + if resume.IsNil() || !strings.Contains(resume.String(), "call i32 @"+coroWorkerResumeHookV1) || + !strings.Contains(resume.String(), "call void (...) @llvm.fake.use(ptr") { + t.Fatalf("CoroSplit lost foreign worker resume:\n%s", module.String()) + } +} + +func TestCoroWorkerForeignCallShapeRejectsUnsafeABIs(t *testing.T) { + tests := []struct { + name string + declaration string + statement string + want string + }{ + {"float argument", "func foreign(float64) uintptr", "_ = foreign(1)", "argument 0 type float64 is not losslessly word-packable"}, + {"aggregate argument", "func foreign(struct{ X uintptr }) uintptr", "_ = foreign(struct{ X uintptr }{})", "argument 0 type struct"}, + {"float result", "func foreign(uintptr) float64", "_ = foreign(1)", "result type float64 is not losslessly word-packable"}, + {"pointer result", "func foreign(uintptr) *byte", "_ = foreign(1)", "result type *byte is not losslessly word-packable integer data"}, + {"multiple results", "func foreign(uintptr) (uintptr, uintptr)", "_, _ = foreign(1)", "requires zero or one result"}, + {"variadic", "func foreign(...uintptr) uintptr", "_ = foreign(1)", "receiver-free, non-variadic"}, + {"too many arguments", "func foreign(uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr, uintptr) uintptr", "_ = foreign(0, 0, 0, 0, 0, 0, 0, 0, 0, 0)", "zero to 9 arguments"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + source := `package foreignworker +import _ "unsafe" +//llgo:coro worker +//go:linkname foreign C.foreign_reject_probe +` + test.declaration + ` +func Root() { ` + test.statement + ` } +` + fixture := prepareCoroWorkerForeignFixture(t, source, "Root") + defer fixture.prog.Dispose() + _, recognized, err := validateCoroWorkerForeignCall( + fixture.plan, fixture.universe, fixture.call, fixture.prog.PointerSize(), + ) + if !recognized || err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("preflight error = %v; want %q", err, test.want) + } + }) + } +} + +func TestCoroWorkerForeignCallAcceptsExplicitCFunctionPointerArgument(t *testing.T) { + const source = `package foreignworker +import "unsafe" +//llgo:type C +type Callback func(unsafe.Pointer) +//llgo:coro worker +//go:linkname foreign C.foreign_callback_registration_probe +func foreign(Callback, unsafe.Pointer) +func callback(unsafe.Pointer) {} +func Root(pointer unsafe.Pointer) { foreign(callback, pointer) } +` + fixture := prepareCoroWorkerForeignFixture(t, source, "Root") + defer fixture.prog.Dispose() + shape, recognized, err := validateCoroWorkerForeignCall( + fixture.plan, fixture.universe, fixture.call, fixture.prog.PointerSize(), + ) + if !recognized || err != nil || shape.argc != 2 { + t.Fatalf("C callback worker call = shape:%+v recognized:%t err:%v", shape, recognized, err) + } +} + +func TestCoroWorkerGenericCallableContractAcceptsSupportedMemoryLifetimes(t *testing.T) { + for _, memory := range []string{"by-value", "borrow-until-return", "borrow-until-complete"} { + t.Run(memory, func(t *testing.T) { + fixture := prepareCoroWorkerForeignFixture(t, coroWorkerCallableForeignSource( + "may-block", "any-thread", "none", memory, + ), "Root") + defer fixture.prog.Dispose() + shape, recognized, err := validateCoroWorkerForeignCall( + fixture.plan, fixture.universe, fixture.call, fixture.prog.PointerSize(), + ) + if !recognized || err != nil || shape.target == nil || shape.argc != 1 { + t.Fatalf("generic callable worker validation = shape:%+v recognized:%t err:%v", shape, recognized, err) + } + }) + } +} + +func TestCoroWorkerForeignCallRejectsAddressOnlyWordCallableABI(t *testing.T) { + const source = `package foreignworker +import _ "unsafe" +//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete abi=word-call.v1/0 +//go:linkname libc_direct_probe_trampoline C.direct_probe +func libc_direct_probe_trampoline() +func Root() { libc_direct_probe_trampoline() } +` + fixture := prepareCoroWorkerForeignFixture(t, source, "Root") + defer fixture.prog.Dispose() + _, recognized, err := validateCoroWorkerForeignCall( + fixture.plan, fixture.universe, fixture.call, fixture.prog.PointerSize(), + ) + if !recognized || err == nil || !strings.Contains(err.Error(), "address-only") || + !strings.Contains(err.Error(), "FuncPCABI0-to-llgo.syscall") { + t.Fatalf("address-only typed foreign call validation = recognized:%t err:%v", recognized, err) + } +} + +func TestCoroWorkerGenericCallableContractRejectsUnsupportedDimensions(t *testing.T) { + tests := []struct { + name string + progress, affinity, reentry string + memory string + want string + }{ + {"unknown progress", "unknown", "any-thread", "none", "by-value", "callable progress"}, + {"executor-safe progress", "executor-safe", "any-thread", "none", "by-value", "callable progress"}, + {"async completion", "async-completion", "any-thread", "none", "by-value", "callable progress"}, + {"no return", "no-return", "any-thread", "none", "by-value", "callable progress"}, + {"unknown affinity", "may-block", "unknown", "none", "by-value", "callable affinity"}, + {"caller affinity", "may-block", "caller-thread", "none", "by-value", "callable affinity"}, + {"owner affinity", "may-block", "owner-thread", "none", "by-value", "callable affinity"}, + {"host affinity", "may-block", "host-main", "none", "by-value", "callable affinity"}, + {"unknown reentry", "may-block", "any-thread", "unknown", "by-value", "callable reentry"}, + {"managed callback", "may-block", "any-thread", "managed-callback", "by-value", "callable reentry"}, + {"unknown memory", "may-block", "any-thread", "none", "unknown", "callable memory lifetime"}, + {"retained memory", "may-block", "any-thread", "none", "retained", "callable memory lifetime"}, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + fixture := prepareCoroWorkerForeignFixture(t, coroWorkerCallableForeignSource( + test.progress, test.affinity, test.reentry, test.memory, + ), "Root") + defer fixture.prog.Dispose() + target, frozen := fixture.universe.Resolve(fixture.call.Common().StaticCallee()) + if !frozen || target == nil { + t.Fatal("generic callable target is absent from the frozen universe") + } + err := validateCoroWorkerForeignAuthorization(fixture.plan, fixture.universe, target) + if err == nil || !strings.Contains(err.Error(), test.want) { + t.Fatalf("generic callable worker authorization error = %v; want %q", err, test.want) + } + }) + } +} + +func TestCoroWorkerForeignCallRequiresFrozenCertificate(t *testing.T) { + source := strings.Replace(coroWorkerForeignTestSource, "//llgo:coro worker\n", "", 1) + fixture := prepareCoroWorkerForeignFixture(t, source, "Root") + defer fixture.prog.Dispose() + _, recognized, err := validateCoroWorkerForeignCall( + fixture.plan, fixture.universe, fixture.call, fixture.prog.PointerSize(), + ) + if !recognized || err == nil || !strings.Contains(err.Error(), "no exact worker-safe certificate") { + t.Fatalf("uncertified worker call validation = recognized:%t err:%v", recognized, err) + } +} + +func TestCoroWorkerForeignCallRejectsForgedPlanCertificate(t *testing.T) { + fixture := prepareCoroWorkerForeignFixture(t, coroWorkerForeignTestSource, "Root") + defer fixture.prog.Dispose() + ssaUniverse, err := coro.NewSSAEmissionUniverse(fixture.ssaPkg.Prog, fixture.universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := fixture.universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelWorkerClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + forged, err := coro.AnalyzeSSA(fixture.ssaPkg.Prog, coro.Roots{{Function: fixture.root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + background, classified, backgroundErr := fixture.universe.FunctionBackground(fn) + if backgroundErr != nil { + return coro.SSAFunctionPolicy{}, backgroundErr + } + if classified && background == llssa.InC { + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: coro.ExternalUnknownForeign, OverrideExternal: true, + Exec: coro.BlockForeign | coro.IRQUnsafe, ForeignWorkerCertificate: "forged-worker-certificate", + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + return callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init", nil + }, + }) + if err != nil { + t.Fatal(err) + } + _, recognized, err := validateCoroWorkerForeignCall( + forged, fixture.universe, fixture.call, fixture.prog.PointerSize(), + ) + if !recognized || err == nil || !strings.Contains(err.Error(), "identity differs") { + t.Fatalf("forged worker call validation = recognized:%t err:%v", recognized, err) + } +} + +func TestCoroWorkerGenericCallableRejectsPlanUniverseCertificateMismatch(t *testing.T) { + fixture := prepareCoroWorkerForeignFixture(t, coroWorkerGenericForeignTestSource, "Root") + defer fixture.prog.Dispose() + target, frozen := fixture.universe.Resolve(fixture.call.Common().StaticCallee()) + if !frozen || target == nil { + t.Fatal("generic callable target is absent from the frozen universe") + } + frontend, certified, err := fixture.universe.CoroCallableContractCertificate(target) + if err != nil || !certified { + t.Fatalf("frontend callable certificate = %+v, %t, %v", frontend, certified, err) + } + forgedCertificate := frontend + forgedCertificate.CanonicalFunctionIdentity += "#forged-plan" + if err := forgedCertificate.Validate(); err != nil { + t.Fatalf("test forged callable certificate is structurally invalid: %v", err) + } + + ssaUniverse, err := coro.NewSSAEmissionUniverse(fixture.ssaPkg.Prog, fixture.universe.Functions()) + if err != nil { + t.Fatal(err) + } + functionIDs := fixture.universe.FunctionIDConfig() + functionIDs.CoroABI = coro.PhysicalABIV1 + functionIDs.SchedulerABI = coro.SchedulerProgramBootstrapChannelWorkerClosedStaticSpawnABIV0 + functionIDs.ArchiveReady = true + forgedPlan, err := coro.AnalyzeSSA(fixture.ssaPkg.Prog, coro.Roots{{Function: fixture.root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: functionIDs, + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + background, classified, backgroundErr := fixture.universe.FunctionBackground(fn) + if backgroundErr != nil { + return coro.SSAFunctionPolicy{}, backgroundErr + } + if classified && background == llssa.InC { + certificate, present, certificateErr := fixture.universe.CoroCallableContractCertificate(fn) + if certificateErr != nil { + return coro.SSAFunctionPolicy{}, certificateErr + } + if present { + if resolved, ok := fixture.universe.Resolve(fn); ok && resolved == target { + certificate = forgedCertificate + } + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: coro.ExternalUnknownForeign, OverrideExternal: true, + Exec: coro.BlockForeign | coro.IRQUnsafe, CallableContractCertificate: certificate, + }, nil + } + return coro.SSAFunctionPolicy{ + IgnoreBody: true, External: coro.ExternalUnknownForeign, OverrideExternal: true, + Exec: coro.BlockForeign | coro.IRQUnsafe, + }, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, call ssa.CallInstruction) (bool, error) { + callee := call.Common().StaticCallee() + return callee != nil && callee.Pkg != nil && callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init", nil + }, + }) + if err != nil { + t.Fatal(err) + } + err = validateCoroWorkerForeignAuthorization(forgedPlan, fixture.universe, target) + if err == nil || !strings.Contains(err.Error(), "certificate differs") { + t.Fatalf("forged generic callable authorization error = %v; want complete certificate mismatch", err) + } +} + +func TestCoroWorkerForeignWordShapeIsTargetWidthExact(t *testing.T) { + if !coroWorkerWordType(types.Typ[types.Int32], 4) || + !coroWorkerWordType(types.NewPointer(types.Typ[types.Byte]), 4) || + !coroWorkerWordType(types.Typ[types.UnsafePointer], 4) { + t.Fatal("32-bit integer/pointer worker words were rejected") + } + for _, typ := range []types.Type{ + types.Typ[types.Int64], types.Typ[types.Float32], types.NewStruct(nil, nil), types.NewSlice(types.Typ[types.Byte]), + } { + if coroWorkerWordType(typ, 4) { + t.Errorf("32-bit worker accepted non-word type %s", typ) + } + } + for _, typ := range []types.Type{ + types.NewPointer(types.Typ[types.Byte]), types.Typ[types.UnsafePointer], types.Typ[types.Int64], + } { + if coroWorkerResultWordType(typ, 4) { + t.Errorf("32-bit worker accepted unsafe result word type %s", typ) + } + } + for _, typ := range []types.Type{types.Typ[types.Int8], types.Typ[types.Uint32], types.Typ[types.Uintptr]} { + if !coroWorkerResultWordType(typ, 4) { + t.Errorf("32-bit worker rejected integer result word type %s", typ) + } + } +} diff --git a/cl/coro_worker_result_projection.go b/cl/coro_worker_result_projection.go new file mode 100644 index 0000000000..86d991c883 --- /dev/null +++ b/cl/coro_worker_result_projection.go @@ -0,0 +1,202 @@ +/* + * 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 ( + "fmt" + "go/ast" + "strconv" + "strings" + + "golang.org/x/tools/go/ssa" +) + +const coroWorkerResultProjectionWidthV1 = 8 + +// coroWorkerResultProjection is the exact source-owned assertion that one +// internal Go wrapper forwards selected worker result words. It deliberately +// says nothing about pointer-ness: that fact still comes from the exact C +// callable contract carried by one producer-forward incoming edge. +// +// resultToWorker uses zero-based tuple indices internally. -1 means that the +// wrapper result is not projected by the directive. +type coroWorkerResultProjection struct { + functionParameter int + resultToWorker [coroWorkerResultProjectionWidthV1]int8 + canonical string +} + +type coroWorkerResultProjectionCertificate struct { + id string + functionParameter int + resultToWorker [coroWorkerResultProjectionWidthV1]int8 +} + +func parseCoroWorkerResultProjectionDecl(decl *ast.FuncDecl) (coroWorkerResultProjection, bool, error) { + projection := coroWorkerResultProjection{functionParameter: -1} + for index := range projection.resultToWorker { + projection.resultToWorker[index] = -1 + } + if decl == nil || decl.Doc == nil { + return projection, false, nil + } + + var directive []string + var directivePayload string + for _, comment := range decl.Doc.List { + if comment == nil { + continue + } + line := comment.Text + if !strings.HasPrefix(line, "//") { + continue + } + payload := strings.TrimPrefix(line, "//") + fields := strings.Fields(payload) + if len(fields) < 2 || fields[0] != "llgo:coro" || fields[1] != "workerresult" { + continue + } + if directive != nil { + return projection, false, fmt.Errorf("duplicate //llgo:coro workerresult directive") + } + directive = fields + directivePayload = payload + } + if directive == nil { + return projection, false, nil + } + if decl.Body == nil { + return projection, false, fmt.Errorf("//llgo:coro workerresult requires a bodyful Go wrapper") + } + if len(directive) != 5 || directive[2] != "v1" { + return projection, false, fmt.Errorf("//llgo:coro workerresult requires exact syntax: //llgo:coro workerresult v1 fn= map=:[,...]") + } + if !strings.HasPrefix(directive[3], "fn=") || !strings.HasPrefix(directive[4], "map=") { + return projection, false, fmt.Errorf("//llgo:coro workerresult v1 requires canonical fn then map fields") + } + parameterText := strings.TrimPrefix(directive[3], "fn=") + parameter, err := strconv.Atoi(parameterText) + if err != nil || parameter < 0 || strconv.Itoa(parameter) != parameterText { + return projection, false, fmt.Errorf("//llgo:coro workerresult v1 has invalid function parameter %q", parameterText) + } + projection.functionParameter = parameter + + mappingText := strings.TrimPrefix(directive[4], "map=") + if mappingText == "" { + return projection, false, fmt.Errorf("//llgo:coro workerresult v1 requires a non-empty result map") + } + lastWrapper := -1 + canonicalMappings := make([]string, 0, strings.Count(mappingText, ",")+1) + for _, mapping := range strings.Split(mappingText, ",") { + wrapperText, workerText, ok := strings.Cut(mapping, ":") + if !ok || strings.Contains(workerText, ":") { + return projection, false, fmt.Errorf("//llgo:coro workerresult v1 has invalid result mapping %q", mapping) + } + wrapper, wrapperOK := parseCoroWorkerResultWord(wrapperText) + worker, workerOK := parseCoroWorkerResultWord(workerText) + if !wrapperOK || !workerOK { + return projection, false, fmt.Errorf("//llgo:coro workerresult v1 has invalid result mapping %q", mapping) + } + if wrapper <= lastWrapper { + return projection, false, fmt.Errorf("//llgo:coro workerresult v1 result mappings must be unique and ordered by wrapper result") + } + lastWrapper = wrapper + projection.resultToWorker[wrapper] = int8(worker) + canonicalMappings = append(canonicalMappings, coroWorkerResultWord(wrapper)+":"+coroWorkerResultWord(worker)) + } + projection.canonical = "llgo:coro workerresult v1 fn=" + strconv.Itoa(parameter) + " map=" + strings.Join(canonicalMappings, ",") + if directivePayload != projection.canonical { + return projection, false, fmt.Errorf("//llgo:coro workerresult v1 is not in canonical form %q", projection.canonical) + } + return projection, true, nil +} + +func parseCoroWorkerResultWord(text string) (int, bool) { + if len(text) != 2 || text[0] != 'r' || text[1] < '1' || text[1] > '8' { + return 0, false + } + return int(text[1] - '1'), true +} + +func coroWorkerResultWord(index int) string { + return "r" + strconv.Itoa(index+1) +} + +func coroWorkerResultProjectionFor(fn *ssa.Function) (coroWorkerResultProjection, bool, error) { + if fn == nil { + return coroWorkerResultProjection{}, false, nil + } + decl, _ := fn.Syntax().(*ast.FuncDecl) + return parseCoroWorkerResultProjectionDecl(decl) +} + +// freezeCoroWorkerResultProjectionCertificates validates every annotation even +// when its wrapper is not reached by a currently certified worker sink. This +// keeps malformed trusted metadata from silently becoming active after an +// unrelated reachability change. +func (u *EmissionUniverse) freezeCoroWorkerResultProjectionCertificates() error { + if u == nil || !u.CoroWorkerEnabled() { + return nil + } + for _, fn := range u.functions { + if fn == nil || u.canonicalAlias(fn) != fn { + continue + } + projection, present, err := coroWorkerResultProjectionFor(fn) + if err != nil { + return fmt.Errorf("prepare emission universe: worker result projection on %q: %w", fn.Name(), err) + } + if !present { + continue + } + if fn.Parent() != nil || len(fn.FreeVars) != 0 || len(fn.Blocks) == 0 || fn.Signature == nil || + fn.Signature.Recv() != nil || fn.Signature.Variadic() || fn.TypeParams() != nil || len(fn.TypeArgs()) != 0 { + return fmt.Errorf("prepare emission universe: worker result projection %q requires an exact static non-generic Go wrapper", fn.Name()) + } + params, results := fn.Signature.Params(), fn.Signature.Results() + if params == nil || projection.functionParameter >= params.Len() || + projection.functionParameter >= len(fn.Params) || + !coroWorkerUintptrType(params.At(projection.functionParameter).Type()) || + !coroWorkerUintptrType(fn.Params[projection.functionParameter].Type()) { + return fmt.Errorf("prepare emission universe: worker result projection %q function parameter %d is not uintptr-shaped", fn.Name(), projection.functionParameter) + } + for wrapper, worker := range projection.resultToWorker { + if worker < 0 { + continue + } + if results == nil || wrapper >= results.Len() || !coroWorkerUintptrType(results.At(wrapper).Type()) { + return fmt.Errorf("prepare emission universe: worker result projection %q result %s is not a uintptr-shaped wrapper result", fn.Name(), coroWorkerResultWord(wrapper)) + } + } + identity := u.linkIdentities[fn] + if identity == "" { + return fmt.Errorf("prepare emission universe: worker result projection %q has no frozen function identity", fn.Name()) + } + certificate := coroWorkerResultProjectionCertificate{ + functionParameter: projection.functionParameter, + resultToWorker: projection.resultToWorker, + } + certificate.id = framedEmissionKey( + "llgo-coro-worker-result-projection-v1", + identity, + structuralGoLinknameABITypeKey(fn.Signature), + projection.canonical, + ) + u.workerResultProjections[fn] = certificate + } + return nil +} diff --git a/cl/coro_worker_result_provenance_test.go b/cl/coro_worker_result_provenance_test.go new file mode 100644 index 0000000000..9389c10a7e --- /dev/null +++ b/cl/coro_worker_result_provenance_test.go @@ -0,0 +1,362 @@ +//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/ast" + "go/parser" + "go/token" + "strings" + "testing" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +func TestCoroWorkerResultProjectionDirectiveIsCanonical(t *testing.T) { + for _, test := range []struct { + name string + directive string + body string + wantOK bool + }{ + {name: "exact", directive: "//llgo:coro workerresult v1 fn=0 map=r1:r1", body: "{}", wantOK: true}, + {name: "two ordered mappings", directive: "//llgo:coro workerresult v1 fn=0 map=r1:r1,r2:r2", body: "{}", wantOK: true}, + {name: "extra space", directive: "//llgo:coro workerresult v1 fn=0 map=r1:r1", body: "{}"}, + {name: "wrong field order", directive: "//llgo:coro workerresult v1 map=r1:r1 fn=0", body: "{}"}, + {name: "leading zero", directive: "//llgo:coro workerresult v1 fn=00 map=r1:r1", body: "{}"}, + {name: "duplicate result", directive: "//llgo:coro workerresult v1 fn=0 map=r1:r1,r1:r2", body: "{}"}, + {name: "unordered result", directive: "//llgo:coro workerresult v1 fn=0 map=r2:r2,r1:r1", body: "{}"}, + {name: "unknown word", directive: "//llgo:coro workerresult v1 fn=0 map=result1:r1", body: "{}"}, + {name: "bodyless", directive: "//llgo:coro workerresult v1 fn=0 map=r1:r1"}, + } { + t.Run(test.name, func(t *testing.T) { + source := "package p\n" + test.directive + "\nfunc f(fn uintptr) uintptr " + test.body + "\n" + file, err := parser.ParseFile(token.NewFileSet(), "projection.go", source, parser.ParseComments) + if err != nil { + t.Fatal(err) + } + decl, _ := file.Decls[0].(*ast.FuncDecl) + _, ok, parseErr := parseCoroWorkerResultProjectionDecl(decl) + if got := ok && parseErr == nil; got != test.wantOK { + t.Fatalf("projection parse = ok:%t err:%v; want success=%t", ok, parseErr, test.wantOK) + } + }) + } +} + +func TestCoroWorkerWordCallableABIResultMetadataIsExact(t *testing.T) { + for _, test := range []struct { + value string + wantOK bool + wantArgs int + wantMask uint8 + }{ + {value: "word-call.v1/0", wantOK: true}, + {value: "word-call.v1/9", wantOK: true, wantArgs: 9}, + {value: "word-call.v1/3+foreign-pointer-result=r1", wantOK: true, wantArgs: 3, wantMask: 1}, + {value: ""}, + {value: "word-call.v2/3+foreign-pointer-result=r1"}, + {value: "word-call.v1/"}, + {value: "word-call.v1/01"}, + {value: "word-call.v1/+3"}, + {value: "word-call.v1/-0"}, + {value: "word-call.v1/10"}, + {value: "word-call.v1/3+foreign-pointer-result=r2"}, + {value: "word-call.v1/3+foreign-pointer-result=r1+foreign-pointer-result=r1"}, + {value: "word-call.v1/3+foreign-pointer-result=r1x"}, + {value: "word-call.v1/3 +foreign-pointer-result=r1"}, + } { + shape, ok := parseCoroWorkerWordCallableABI(test.value) + if ok != test.wantOK || shape.wordArgs != test.wantArgs || shape.foreignPointerResultMask != test.wantMask { + t.Errorf("parseCoroWorkerWordCallableABI(%q) = %+v, %t; want args=%d mask=%#x ok=%t", + test.value, shape, ok, test.wantArgs, test.wantMask, test.wantOK) + } + } +} + +const coroWorkerResultProvenanceFixture = `package workerresult + +import "unsafe" + +//llgo:link funcPCABI0 llgo.funcPCABI0 +func funcPCABI0(fn any) uintptr + +//llgo:link raw llgo.syscall +func raw(fn, a0 uintptr) (uintptr, uintptr, uintptr) + +//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete abi=word-call.v1/1+foreign-pointer-result=r1 +func libc_pointer_result_v1_trampoline() + +//llgo:coro contract foreign.v1 scope=declaration progress=may-block affinity=any-thread reentry=none memory=borrow-until-complete abi=word-call.v1/1 +func libc_scalar_result_v1_trampoline() + +func DirectR1(a0 uintptr) unsafe.Pointer { + r1, _, _ := raw(funcPCABI0(libc_pointer_result_v1_trampoline), a0) + return unsafe.Pointer(r1) +} + +func DirectR2(a0 uintptr) unsafe.Pointer { + _, r2, _ := raw(funcPCABI0(libc_pointer_result_v1_trampoline), a0) + return unsafe.Pointer(r2) +} + +func DerivedR1(a0 uintptr) unsafe.Pointer { + r1, _, _ := raw(funcPCABI0(libc_pointer_result_v1_trampoline), a0) + return unsafe.Pointer(r1 + a0) +} + +func ScalarR1(a0 uintptr) unsafe.Pointer { + r1, _, _ := raw(funcPCABI0(libc_scalar_result_v1_trampoline), a0) + return unsafe.Pointer(r1) +} + +func privateCarrier(fn, a0 uintptr) uintptr { + r1, _, _ := raw(fn, a0) + return r1 +} + +//llgo:coro workerresult v1 fn=0 map=r1:r1 +func projectedCarrier(fn, a0 uintptr) (uintptr, uintptr, uintptr) { + r1, r2, err := raw(fn, a0) + return r1, r2, err +} + +//llgo:coro workerresult v1 fn=0 map=r1:r1 +func projectedTwoSinks(fn, a0 uintptr) (uintptr, uintptr, uintptr) { + r1, r2, err := raw(fn, a0) + raw(fn, a0) + return r1, r2, err +} + +func ThroughProjectedPointer(a0 uintptr) unsafe.Pointer { + r1, _, _ := projectedCarrier(funcPCABI0(libc_pointer_result_v1_trampoline), a0) + return unsafe.Pointer(r1) +} + +func ThroughProjectedScalar(a0 uintptr) unsafe.Pointer { + r1, _, _ := projectedCarrier(funcPCABI0(libc_scalar_result_v1_trampoline), a0) + return unsafe.Pointer(r1) +} + +func ThroughProjectedDerived(a0 uintptr) unsafe.Pointer { + r1, _, _ := projectedCarrier(funcPCABI0(libc_pointer_result_v1_trampoline), a0) + return unsafe.Pointer(r1 + a0) +} + +func ThroughProjectedTwoSinksPointer(a0 uintptr) unsafe.Pointer { + r1, _, _ := projectedTwoSinks(funcPCABI0(libc_pointer_result_v1_trampoline), a0) + return unsafe.Pointer(r1) +} + +func ThroughPointer(a0 uintptr) uintptr { + return privateCarrier(funcPCABI0(libc_pointer_result_v1_trampoline), a0) +} + +func ThroughScalar(a0 uintptr) uintptr { + return privateCarrier(funcPCABI0(libc_scalar_result_v1_trampoline), a0) +} +` + +func TestCoroWorkerForeignPointerResultProjectsAcrossExactWrapperCall(t *testing.T) { + prog, pkg, universe := prepareCoroWorkerResultProvenanceFixture(t) + defer prog.Dispose() + + for _, test := range []struct { + function string + want bool + }{ + {function: "ThroughProjectedPointer", want: true}, + {function: "ThroughProjectedTwoSinksPointer", want: true}, + {function: "ThroughProjectedScalar"}, + {function: "ThroughProjectedDerived"}, + } { + t.Run(test.function, func(t *testing.T) { + root := pkg.Func(test.function) + plan := analyzeCoroWorkerResultProvenancePlan(t, pkg, universe, root) + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "") + if err != nil { + t.Fatal(err) + } + var conversion *ssa.Convert + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + candidate, ok := instruction.(*ssa.Convert) + if ok && coroFrameRetentionUintptrLike(candidate.X.Type()) && coroFrameRetentionPointerLike(candidate.Type()) { + conversion = candidate + } + } + } + if conversion == nil { + t.Fatal("fixture has no uintptr-to-pointer conversion") + } + if got := audit.provesWorkerForeignPointerResult(conversion.X); got != test.want { + t.Fatalf("projected worker result proof for %T %q = %t; want %t", conversion.X, conversion.X, got, test.want) + } + reason := audit.validateConvert(conversion) + if test.want && reason != "" { + t.Fatalf("exact projected r1 extract rejected: %s", reason) + } + if !test.want && !strings.Contains(reason, "has no traceable exact pointer provenance") { + t.Fatalf("non-exact projected result rejection = %q; want provenance failure", reason) + } + }) + } +} + +func TestCoroWorkerForeignPointerResultCertificateMask(t *testing.T) { + prog, pkg, universe := prepareCoroWorkerResultProvenanceFixture(t) + defer prog.Dispose() + + for _, test := range []struct { + function string + wantTargets int + wantMask uint8 + }{ + {function: "DirectR1", wantTargets: 1, wantMask: 1}, + {function: "DirectR2", wantTargets: 1, wantMask: 1}, + {function: "DerivedR1", wantTargets: 1, wantMask: 1}, + {function: "ScalarR1", wantTargets: 1, wantMask: 0}, + // A private carrier callable by either target may park safely, but it + // cannot promise pointer provenance that only one incoming target owns. + {function: "privateCarrier", wantTargets: 2, wantMask: 0}, + } { + call := exactWorkerSyscallCall(t, universe, pkg.Func(test.function)) + certificate, certified, err := universe.CoroWorkerSyscallCertificate(call) + if err != nil || !certified || certificate.ID == "" || + certificate.StaticTargetCount != test.wantTargets || + certificate.ForeignPointerResultMask != test.wantMask { + t.Errorf("%s certificate = %+v, %t, %v; want targets=%d mask=%#x", + test.function, certificate, certified, err, test.wantTargets, test.wantMask) + } + } +} + +func TestCoroWorkerForeignPointerResultOnlyAuthorizesExactDirectExtract(t *testing.T) { + prog, pkg, universe := prepareCoroWorkerResultProvenanceFixture(t) + defer prog.Dispose() + + for _, test := range []struct { + function string + want bool + }{ + {function: "DirectR1", want: true}, + {function: "DirectR2"}, + {function: "DerivedR1"}, + {function: "ScalarR1"}, + } { + t.Run(test.function, func(t *testing.T) { + root := pkg.Func(test.function) + call := exactWorkerSyscallCall(t, universe, root) + certificate, certified, err := universe.CoroWorkerSyscallCertificate(call) + if err != nil || !certified || certificate.ID == "" { + t.Fatalf("worker certificate = %+v, %t, %v", certificate, certified, err) + } + plan := analyzeCoroWorkerResultProvenancePlan(t, pkg, universe, root) + audit, err := newCoroPhysicalPureSSAAudit(universe, plan, root, "") + if err != nil { + t.Fatal(err) + } + var conversion *ssa.Convert + for _, block := range root.Blocks { + for _, instruction := range block.Instrs { + candidate, ok := instruction.(*ssa.Convert) + if ok && coroFrameRetentionUintptrLike(candidate.X.Type()) && coroFrameRetentionPointerLike(candidate.Type()) { + if conversion != nil { + t.Fatalf("fixture has multiple uintptr-to-pointer conversions") + } + conversion = candidate + } + } + } + if conversion == nil { + t.Fatal("fixture has no uintptr-to-pointer conversion") + } + if got := audit.provesWorkerForeignPointerResult(conversion.X); got != test.want { + t.Fatalf("worker result proof for %T %q = %t; want %t", conversion.X, conversion.X, got, test.want) + } + reason := audit.validateConvert(conversion) + if test.want && reason != "" { + t.Fatalf("exact r1 extract rejected: %s", reason) + } + if !test.want && !strings.Contains(reason, "has no traceable exact pointer provenance") { + t.Fatalf("non-exact result rejection = %q; want provenance failure", reason) + } + }) + } +} + +func prepareCoroWorkerResultProvenanceFixture(t *testing.T) (llssa.Program, *ssa.Package, *EmissionUniverse) { + t.Helper() + pkg, _, files := buildGoSSAPkg(t, coroWorkerResultProvenanceFixture) + prog := newLLSSAProg(t) + universe, err := prepareStacklessEmissionUniverseWithOptions( + prog, nil, []EmissionPackage{{SSA: pkg, Files: files}}, + EmissionUniverseOptions{CoroProfile: CoroProfileStackless, CoroTargetCapabilities: CoroNativeTargetCapabilities()}, + ) + if err != nil { + prog.Dispose() + t.Fatal(err) + } + return prog, pkg, universe +} + +func analyzeCoroWorkerResultProvenancePlan( + t *testing.T, + pkg *ssa.Package, + universe *EmissionUniverse, + root *ssa.Function, +) *coro.SSAPlan { + t.Helper() + ssaUniverse, err := coro.NewSSAEmissionUniverse(pkg.Prog, universe.Functions()) + if err != nil { + t.Fatal(err) + } + plan, err := coro.AnalyzeSSA(pkg.Prog, coro.Roots{{Function: root, Demand: coro.AsyncDemand}}, coro.SSAConfig{ + EmissionUniverse: ssaUniverse, + FunctionIDs: universe.FunctionIDConfig(), + MaxPlainInstructions: -1, + ClassifyFunction: func(fn *ssa.Function) (coro.SSAFunctionPolicy, error) { + if fn == root || fn == pkg.Func("projectedCarrier") || fn == pkg.Func("projectedTwoSinks") { + return coro.SSAFunctionPolicy{Effect: coro.MayPark}, nil + } + return coro.SSAFunctionPolicy{}, nil + }, + ClassifyElidedCall: func(_ *ssa.Function, candidate ssa.CallInstruction) (bool, error) { + if callee := candidate.Common().StaticCallee(); callee != nil && callee.Pkg != nil && + callee.Pkg.Pkg.Path() == "unsafe" && callee.Name() == "init" { + return true, nil + } + semantics, intrinsic, err := universe.CoroIntrinsicCallSiteSemantics(candidate) + return intrinsic && semantics.ElidesManagedCall(), err + }, + ClassifyElidedCallCertificate: func(_ *ssa.Function, candidate ssa.CallInstruction) (string, error) { + certificate, certified, err := universe.CoroWorkerSyscallCertificate(candidate) + if err != nil || !certified { + return "", err + } + return certificate.ID, nil + }, + }) + if err != nil { + t.Fatal(err) + } + return plan +} diff --git a/cl/coro_worker_syscall_capability.go b/cl/coro_worker_syscall_capability.go new file mode 100644 index 0000000000..f20f3c4f4b --- /dev/null +++ b/cl/coro_worker_syscall_capability.go @@ -0,0 +1,883 @@ +/* + * 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 ( + "fmt" + "go/ast" + "go/types" + "sort" + "strconv" + "strings" + + "github.com/goplus/llgo/internal/coro" + llssa "github.com/goplus/llgo/ssa" + "golang.org/x/tools/go/ssa" +) + +// CoroWorkerSyscallCertificate freezes both capabilities required before an +// llgo.syscall function word may cross to a native worker: +// +// - the producer-forward callable shadow remains exact through every private +// carrier edge; +// - every target owns a generic callable contract or legacy workeraddr +// compatibility contract with the exact word-call ABI. +// +// ID binds the exact call occurrence, target physical-symbol set, worker word +// ABI, target layout, and every private parameter owner traversed by the shadow. +// The diagnostic fields are not capabilities; consumers must compare ID. +type CoroWorkerSyscallCertificate struct { + ID string + WorkerABISignature string + PhysicalTargetSetID string + CallableShadowSetID string + StaticTargetCount int + ForeignPointerResultMask uint8 +} + +type coroWorkerAddressTarget struct { + target *ssa.Function + physicalSymbol string + workerArity int + foreignPointerResultMask uint8 + contractCertificateID string + legacyWorkerAddressOnly bool +} + +// coroWorkerSyscallIncomingEdge is one exact, frozen static call into a +// private function-word carrier. Certified says that the producer-forward +// shadow on the edge has the required callable ABI. An +// uncertified edge does not invalidate the conditional universe certificate: +// the final SSA-plan join requires its caller to have EmitNone. This lets one +// standard-library carrier serve a demanded safe wrapper while every unused +// fork/exec/thread-affine wrapper remains fail-closed. +type coroWorkerSyscallIncomingEdge struct { + call *ssa.Call + carrier *ssa.Function + parameter int + certified bool + reason string + targetKeys []string + foreignPointerResultMask uint8 + resultProjectionID string + stableIdentity string +} + +type coroWorkerSyscallIncomingKey struct { + call *ssa.Call + carrier *ssa.Function + parameter int +} + +// coroSelectPatchedWorkerAddressTrampoline makes an alternate-package +// workeraddr declaration participate in ordinary managed-symbol selection. +// Upstream Darwin FuncPCABI0 operands still point at the original SSA +// declaration; selecting the same-name/same-ABI alternate first lets the +// existing exact C-symbol winner logic install the canonical alias when that +// operand is materialized. No unannotated trampoline is selected or inferred. +func coroSelectPatchedWorkerAddressTrampoline(fn *ssa.Function, fromPatch bool) (bool, error) { + if !fromPatch || fn == nil { + return false, nil + } + directive, err := coroForeignCallDirectiveFor(fn) + if err != nil { + return false, err + } + if directive == coroForeignCallWorkerAddress { + return true, nil + } + _, generic, err := coroWorkerCallableDeclarationContractArity(fn) + return generic, err +} + +// aliasPatchedWorkerAddressTrampolines validates patch-owned workeraddr +// declarations and, when an upstream declaration of the same name exists, +// connects that upstream FuncPCABI0 operand to the certified alternate. +// FuncPCABI0 intentionally synthesizes C addresses without materializing +// trampoline SSA declarations, so ordinary reachability-driven patch aliasing +// cannot establish this bridge. A patch may also introduce a new fixed C +// adapter used only by patch code; that form has no upstream alias to install +// but is held to the same frozen symbol, declaration, and arity constraints. +func (u *EmissionUniverse) aliasPatchedWorkerAddressTrampolines() error { + if u == nil || !u.CoroWorkerEnabled() { + return nil + } + packages := make([]*preparedEmissionPackage, 0, len(u.packages)) + for _, prepared := range u.packages { + if prepared != nil && prepared.hasPatch && !prepared.metadataOnly { + packages = append(packages, prepared) + } + } + sort.SliceStable(packages, func(i, j int) bool { + if packages[i].order != packages[j].order { + return packages[i].order < packages[j].order + } + return packages[i].identity < packages[j].identity + }) + for _, prepared := range packages { + names := make([]string, 0, len(prepared.patch.Alt.Members)) + for name := range prepared.patch.Alt.Members { + names = append(names, name) + } + sort.Strings(names) + for _, name := range names { + alternate, ok := prepared.patch.Alt.Members[name].(*ssa.Function) + if !ok || !strings.HasSuffix(name, "_trampoline") { + continue + } + directive, err := coroForeignCallDirectiveFor(alternate) + if err != nil { + return fmt.Errorf("prepare emission universe: patch worker-address target %q: %w", name, err) + } + legacy := directive == coroForeignCallWorkerAddress + _, generic, err := coroWorkerCallableDeclarationContractArity(alternate) + if err != nil { + return fmt.Errorf("prepare emission universe: patch worker callable target %q: %w", name, err) + } + if !legacy && !generic { + continue + } + if !coroWorkerAddressAliasDeclaration(alternate) { + return fmt.Errorf( + "prepare emission universe: patched workeraddr target %q requires an exact bodyless non-method alternate declaration", + name, + ) + } + physical := remapTrampolineCNameForTarget(u.prog.Target(), extractTrampolineCName(name)) + if physical == "" { + return fmt.Errorf("prepare emission universe: patched workeraddr target %q has no physical trampoline symbol", name) + } + ownerKey := emissionFunctionOwnerKey{function: alternate, owner: prepared} + kind, kindOK := u.functionKinds[ownerKey] + finalKey, keyOK := u.finalKeys[ownerKey] + finalKind, finalSymbol, _, keyValid := splitManagedSymbolKey(finalKey) + if !kindOK || kind != cFunc || !keyOK || !keyValid || finalKind != cFunc || finalSymbol != physical { + return fmt.Errorf( + "prepare emission universe: patched workeraddr target %q must explicitly link to physical C symbol %q", + name, physical, + ) + } + if canonical := u.canonicalAlias(alternate); canonical == nil || canonical != alternate { + return fmt.Errorf("prepare emission universe: patched workeraddr target %q is not its exact canonical declaration", name) + } + if _, required := u.required[alternate]; !required { + return fmt.Errorf("prepare emission universe: patched workeraddr target %q is absent from the frozen universe", name) + } + originalMember, exists := prepared.ssa.Members[name] + if !exists { + // Patch-private fixed adapters are already canonical physical + // targets. There is intentionally no upstream SSA identity to + // redirect; calls in the alternate package refer to this exact + // declaration. + continue + } + original, ok := originalMember.(*ssa.Function) + if !ok || !coroWorkerAddressAliasDeclaration(original) { + return fmt.Errorf( + "prepare emission universe: patched workeraddr target %q requires an exact bodyless non-method original declaration when the upstream name exists", + name, + ) + } + if structuralGoLinknameABITypeKey(original.Signature) != structuralGoLinknameABITypeKey(alternate.Signature) { + return fmt.Errorf("prepare emission universe: patched workeraddr target %q changes the upstream trampoline ABI", name) + } + if canonical := u.canonicalAlias(original); canonical == nil || canonical != original { + return fmt.Errorf("prepare emission universe: upstream workeraddr target %q already has a conflicting canonical alias", name) + } + u.aliases[original] = alternate + u.fnOwners[original] = prepared + } + } + return nil +} + +func coroWorkerAddressAliasDeclaration(fn *ssa.Function) bool { + if fn == nil || fn.Parent() != nil || len(fn.FreeVars) != 0 || fn.Signature == nil || + fn.Signature.Recv() != nil || fn.Signature.Variadic() || fn.TypeParams() != nil || + len(fn.TypeArgs()) != 0 || len(fn.Blocks) != 0 { + return false + } + decl, _ := fn.Syntax().(*ast.FuncDecl) + return decl != nil && decl.Body == nil && decl.Recv == nil +} + +// freezeCoroWorkerSyscallCertificates runs after frontend identities and +// aliases are immutable. Unsupported call sites deliberately remain ordinary +// synchronous intrinsics; a physical coroutine cannot elide/lower them. +func (u *EmissionUniverse) freezeCoroWorkerSyscallCertificates() error { + if u == nil || !u.CoroWorkerEnabled() { + return nil + } + shadows, err := AnalyzeCoroCallableShadows(u) + if err != nil { + return fmt.Errorf("prepare emission universe: freeze producer-forward callable shadows: %w", err) + } + for _, fn := range u.functions { + if fn == nil || len(fn.Blocks) == 0 || u.canonicalAlias(fn) != fn { + continue + } + for _, block := range fn.Blocks { + for _, instruction := range block.Instrs { + call, ok := instruction.(*ssa.Call) + if !ok || call.Common() == nil || call.Common().IsInvoke() { + continue + } + callee := call.Common().StaticCallee() + opcode, intrinsic, err := u.coroIntrinsicOpcode(callee) + if err != nil || !intrinsic || !isLLGoSyscallIntrinsic(opcode) { + continue + } + if err := validateCoroWorkerSyscallIntrinsicCallSite(call); err != nil { + continue + } + shadow, observed := shadows.Sink(call) + if !observed || !shadow.Certified { + // No producer-forward shadow means no worker authority. + continue + } + certificate, owners, incoming, err := freezeCoroWorkerSyscallShadowCertificate(u, call, opcode, shadow) + if err != nil { + return fmt.Errorf("prepare emission universe: worker llgo.syscall call %q: %w", call.String(), err) + } + u.workerSyscalls[call] = certificate + u.workerSyscallOwners[call] = owners + u.workerSyscallIncoming[call] = incoming + } + } + } + return nil +} + +func coroWorkerAddressFunctionIdentity(universe *EmissionUniverse, fn *ssa.Function) string { + if fn == nil { + return framedEmissionKey("llgo-coro-worker-address-function-v0", "") + } + pkgPath := "" + provenance := "synthetic" + if fn.Pkg != nil && fn.Pkg.Pkg != nil { + pkgPath = llssa.PathOf(fn.Pkg.Pkg) + provenance = "original" + if universe != nil { + if owner := universe.ownerOf(fn); owner != nil && owner.hasPatch && fn.Pkg == owner.patch.Alt { + provenance = "alternate-patch" + } + } + } + signature := "" + if fn.Signature != nil { + signature = structuralGoLinknameABITypeKey(fn.Signature) + } + return framedEmissionKey( + "llgo-coro-worker-address-function-v0", + pkgPath, + fn.Name(), + signature, + provenance, + ) +} + +func coroWorkerAddressDirectiveArity(fn *ssa.Function) (int, error) { + decl, _ := fn.Syntax().(*ast.FuncDecl) + if decl == nil || decl.Doc == nil { + return 0, fmt.Errorf("//llgo:coro workeraddr target %q has no attached directive", fn.Name()) + } + for _, comment := range decl.Doc.List { + if comment == nil { + continue + } + payload := strings.TrimSpace(strings.TrimPrefix(strings.TrimSpace(comment.Text), "//")) + fields := strings.Fields(payload) + if len(fields) != 3 || fields[0] != "llgo:coro" || fields[1] != "workeraddr" { + continue + } + arity, err := strconv.Atoi(fields[2]) + if err != nil || arity < 0 || arity > coroWorkerMaxArgsV1 { + return 0, fmt.Errorf("//llgo:coro workeraddr target %q has invalid arity %q", fn.Name(), fields[2]) + } + return arity, nil + } + return 0, fmt.Errorf("//llgo:coro workeraddr target %q has no exact arity", fn.Name()) +} + +func coroWorkerCallableTargetSetKey(universe *EmissionUniverse, target coroWorkerAddressTarget) string { + identity := "" + if universe != nil && target.target != nil { + identity = universe.finalIdentity(target.target) + } + return framedEmissionKey( + "llgo-coro-worker-callable-target-set-entry-v1", + identity, + target.physicalSymbol, + strconv.Itoa(target.workerArity), + strconv.FormatUint(uint64(target.foreignPointerResultMask), 10), + target.contractCertificateID, + strconv.FormatBool(target.legacyWorkerAddressOnly), + ) +} + +func coroWorkerCallableShadowTarget(shadow CoroCallableShadow) coroWorkerAddressTarget { + return coroWorkerAddressTarget{ + target: shadow.Target, + physicalSymbol: shadow.PhysicalSymbol, + workerArity: shadow.ABI.WordArgs, + foreignPointerResultMask: shadow.ForeignPointerResultMask, + contractCertificateID: shadow.ContractCertificateID, + legacyWorkerAddressOnly: shadow.LegacyWorkerAddressCompat, + } +} + +func coroWorkerCallableCompatibleShadowTargets( + universe *EmissionUniverse, + candidates []CoroCallableShadow, + abi CoroCallableShadowABI, +) map[string]none { + targets := make(map[string]none) + for _, candidate := range candidates { + if candidate.ABI != abi { + continue + } + targets[coroWorkerCallableTargetSetKey(universe, coroWorkerCallableShadowTarget(candidate))] = none{} + } + return targets +} + +// freezeCoroWorkerSyscallShadowCertificate materializes the final worker +// certificate inventory directly from producer-forward facts. No consumer +// value is walked backwards and no emitted address is inspected. +func freezeCoroWorkerSyscallShadowCertificate( + universe *EmissionUniverse, + call *ssa.Call, + opcode int, + shadow CoroCallableShadowSink, +) (CoroWorkerSyscallCertificate, map[*ssa.Function]none, []coroWorkerSyscallIncomingEdge, error) { + if universe == nil || call == nil || call.Parent() == nil || shadow.Call != call || !shadow.Certified { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("producer-forward callable shadow is absent or uncertified") + } + if shadow.ABI.Family != coroCallableShadowWorkerSyscallFamily || + shadow.ABI.WordArgs != len(call.Common().Args)-1 { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("producer-forward callable shadow ABI differs from worker syscall") + } + parent := universe.canonicalAlias(call.Parent()) + if parent == nil || parent != call.Parent() { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("worker syscall owner is not canonical") + } + linkIdentity := universe.linkIdentities[parent] + if linkIdentity == "" { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("worker syscall owner %q has no frozen link identity", parent.Name()) + } + + targetSet := coroWorkerCallableCompatibleShadowTargets(universe, shadow.Candidates, shadow.ABI) + if len(targetSet) == 0 { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("producer-forward callable shadow has no compatible target") + } + for _, candidate := range shadow.Candidates { + if candidate.ABI != shadow.ABI { + continue + } + if candidate.Producer == nil || candidate.Target == nil || candidate.PhysicalSymbol == "" || + candidate.ContractCertificateID == "" || universe.canonicalAlias(candidate.Target) != candidate.Target { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("producer-forward callable shadow has an incomplete target") + } + exact, reason, err := coroWorkerCallableTarget(universe, candidate.SourceTarget, candidate.Target) + if err != nil { + return CoroWorkerSyscallCertificate{}, nil, nil, err + } + if reason != "" || exact != coroWorkerCallableShadowTarget(candidate) { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("callable shadow target differs from its exact producer contract") + } + } + targetKeys := sortedCoroWorkerStringSet(targetSet) + targetSetID := framedEmissionKey(append([]string{"llgo-coro-worker-callable-target-set-v1"}, targetKeys...)...) + foreignPointerResultMask := uint8(^uint8(0)) + compatibleTargets := 0 + for _, candidate := range shadow.Candidates { + if candidate.ABI != shadow.ABI { + continue + } + foreignPointerResultMask &= candidate.ForeignPointerResultMask + compatibleTargets++ + } + if compatibleTargets == 0 { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("producer-forward callable shadow has no compatible result contract") + } + + owners := make(map[*ssa.Function]none) + edgeSet := make(map[coroWorkerSyscallIncomingKey]none) + incoming := make([]coroWorkerSyscallIncomingEdge, 0, len(shadow.Incoming)) + certifiedIncoming := 0 + for _, edge := range shadow.Incoming { + key := coroWorkerSyscallIncomingKey{call: edge.Call, carrier: edge.Carrier, parameter: edge.Parameter} + if edge.Call == nil || edge.Carrier == nil || edge.Parameter < 0 { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("producer-forward callable shadow has an incomplete incoming edge") + } + if _, duplicate := edgeSet[key]; duplicate { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("producer-forward callable shadow has a duplicate incoming edge") + } + edgeSet[key] = none{} + edgeTargets := coroWorkerCallableCompatibleShadowTargets(universe, edge.Candidates, shadow.ABI) + for target := range edgeTargets { + if _, belongs := targetSet[target]; !belongs { + return CoroWorkerSyscallCertificate{}, nil, nil, fmt.Errorf("incoming edge target is absent from the callable shadow target set") + } + } + frozen := coroWorkerSyscallIncomingEdge{ + call: edge.Call, + carrier: edge.Carrier, + parameter: edge.Parameter, + certified: edge.Certified, + reason: edge.Reason, + targetKeys: sortedCoroWorkerStringSet(edgeTargets), + } + edgeForeignPointerMask := uint8(^uint8(0)) + edgeCompatibleTargets := 0 + for _, candidate := range edge.Candidates { + if candidate.ABI != shadow.ABI { + continue + } + edgeForeignPointerMask &= candidate.ForeignPointerResultMask + edgeCompatibleTargets++ + } + if edgeCompatibleTargets == 0 { + edgeForeignPointerMask = 0 + } + if projection, ok := universe.workerResultProjections[edge.Carrier]; ok && + projection.functionParameter == edge.Parameter { + frozen.resultProjectionID = projection.id + for wrapperResult, workerResult := range projection.resultToWorker { + if workerResult >= 0 && edgeForeignPointerMask&(uint8(1)<= coroWorkerResultProjectionWidthV1 { + return fmt.Errorf("worker result projection requires an exact plan, universe, direct call, and result word") + } + if parent := call.Parent(); parent == nil || universe.canonicalAlias(parent) != parent { + return fmt.Errorf("worker result projection caller is not an exact canonical function") + } + carrier, resolved := universe.Resolve(call.Common().StaticCallee()) + if !resolved || carrier == nil { + return fmt.Errorf("worker result projection call has no exact canonical target") + } + projection, projected := universe.workerResultProjections[carrier] + if !projected || projection.id == "" || projection.resultToWorker[result] < 0 { + return fmt.Errorf("worker result projection target has no frozen mapping for result %s", coroWorkerResultWord(result)) + } + carrierPlan, carrierPlanned := plan.FunctionPlan(carrier) + callPlan, callPlanned := plan.CallPlan(call) + if !carrierPlanned || !callPlanned || callPlan.Kind != coro.CallDirect || callPlan.Open || callPlan.MayBeNil || + len(callPlan.Targets) != 1 || callPlan.Targets[0] != carrierPlan.ID || callPlan.Rep != carrierPlan.FuncRep { + return fmt.Errorf("worker result projection call disagrees with the frozen exact static CallPlan") + } + + matches := 0 + for workerCall, workerSite := range universe.coroProgramIR.callPlans { + if !workerSite.workerCertified { + continue + } + direct, ok := workerCall.(*ssa.Call) + if !ok || direct == nil { + continue + } + for _, edge := range workerSite.workerIncoming { + if edge.call != call || edge.carrier != carrier || + edge.parameter != projection.functionParameter || + edge.resultProjectionID != projection.id { + continue + } + matches++ + if err := validateCoroWorkerSyscallCall(plan, universe, direct); err != nil { + return fmt.Errorf("worker result projection sink is not valid in the frozen plan: %w", err) + } + if edge.foreignPointerResultMask&(uint8(1)<