Skip to content

Implement register-based closure ctx#1599

Merged
xushiwei merged 14 commits intogoplus:mainfrom
cpunion:closure-ctxreg-v2
Feb 5, 2026
Merged

Implement register-based closure ctx#1599
xushiwei merged 14 commits intogoplus:mainfrom
cpunion:closure-ctxreg-v2

Conversation

@cpunion
Copy link
Collaborator

@cpunion cpunion commented Feb 2, 2026

Summary

This PR implements register‑based closure ctx passing while keeping LLGo’s 2‑word closure ABI {fn, env}. On ctx‑register targets we write the ctx register and call the real symbol; on other targets we conditionally pass ctx as an implicit first parameter. Compared to PR #1568, the closure layout stays 2‑word (no inline env/hasCtx), so LLVM IR/snapshots change accordingly.

ABI / Representation

  • Closures are represented as a 2‑word value:
    type closure struct {
      fn  *func
      env unsafe.Pointer // nil if no ctx
    }
  • env points to a context object containing captured fields, or is nil for plain funcs.
  • No hasCtx; env == nil is the sole indicator.
  • C function pointers are first‑class: fn stores the real symbol, no wrapper stubs (__llgo_stub.* removed).
  • Interface method values: fn points at the real method entry, env is the receiver/data pointer.

Calling Convention

  • With ctx register:
    write_ctx(env)
    fn(args...)
    
    No branching. For interface method values, we still pass env as the first argument (receiver) and also write ctx for uniform semantics.
  • Without ctx register: call sites branch on env == nil:
    if env != nil { fn(ctx, args...) } else { fn(args...) }
    
    Closure bodies take an explicit ctx param on these targets.

getClosurePtr

  • Returns the env pointer.
  • On ctx‑register targets it reads the ctx register.
  • On no‑reg targets it uses the explicit ctx parameter.

Context Register Mapping

GOARCH Register Notes
amd64 mm0 disable x87 via -mno-80387
386 mm0 disable x87 via -mfpmath=sse -msse2 -mno-80387
arm64 x26 reserved via clang target‑feature
riscv64 x27 reserved via clang target‑feature
riscv32 x27 reserved via clang target‑feature
wasm - conditional ctx param
arm - conditional ctx param

Native builds reserve the ctx reg via clang target‑feature +reserve-<reg> (arm64/riscv64/riscv32).
x86/x86_64 use compiler flags to disable x87 so MM0 is not clobbered by long double operations.

Example IR (closure + C func)

Example Go code:

func cfunc(i int64)

func main() {
  var fn func(i int64)
  fn = cfunc
  fn(0)

  var i int64 = 0
  fn = func(v int64) { i = v }
  fn(0)
}

With ctx register (arm64/riscv64/riscv32/amd64/386)

Caller (main) writes ctx register and calls the real symbol (example uses arm64 x26; x86 uses mm0 with movq/movd and a memory clobber):

; fn = cfunc
store { ptr, ptr } { ptr @cfunc, ptr null }, ptr %fn_slot
; fn(0)
%fv = load { ptr, ptr }, ptr %fn_slot
%fnptr = extractvalue { ptr, ptr } %fv, 0
%env = extractvalue { ptr, ptr } %fv, 1
call void asm sideeffect "mov x26, $0", "r,~{x26}"(ptr %env)
call void %fnptr(i64 0)

; fn = closure
%envobj = call ptr @AllocU(i64 8)
store ptr %i, ptr %envobj
%fv2 = insertvalue { ptr, ptr } { ptr @main$1, ptr undef }, ptr %envobj, 1
store { ptr, ptr } %fv2, ptr %fn_slot
; fn(0)
%fv3 = load { ptr, ptr }, ptr %fn_slot
%fnptr2 = extractvalue { ptr, ptr } %fv3, 0
%env2 = extractvalue { ptr, ptr } %fv3, 1
call void asm sideeffect "mov x26, $0", "r,~{x26}"(ptr %env2)
call void %fnptr2(i64 0)

Closure body (main$1) reads ctx register at entry:

define void @main$1(i64 %v) {
entry:
  %env = call ptr asm sideeffect "mov $0, x26", "=r"()
  %i_ptr = load ptr, ptr %env
  store i64 %v, ptr %i_ptr
  ret void
}

C function remains a normal symbol:

declare void @cfunc(i64)

Without ctx register (wasm/arm)

Caller (main) branches on env == nil:

%env = extractvalue { ptr, ptr } %fv, 1
%has = icmp ne ptr %env, null
br i1 %has, label %with, label %plain

with:
  %fnptr = extractvalue { ptr, ptr } %fv, 0
  %fp1 = bitcast ptr %fnptr to ptr (ptr, i64)*
  call void %fp1(ptr %env, i64 0)
  br label %done

plain:
  %fnptr2 = extractvalue { ptr, ptr } %fv, 0
  %fp2 = bitcast ptr %fnptr2 to ptr (i64)*
  call void %fp2(i64 0)
  br label %done

Closure body (main$1) takes an explicit ctx parameter:

define void @main$1(ptr %env, i64 %v) {
entry:
  %i_ptr = load ptr, ptr %env
  store i64 %v, ptr %i_ptr
  ret void
}

Discussion: Alternative Layout (difference only)

Alternative inline‑env layout (as in PR #1568):

type funcval struct {
  fn  *func
  hasCtx uintptr
  env ...
}
type closure = *funcval

Differences vs {fn, env} (no value judgment):

  • Closure value is a pointer to a variable‑length object instead of a fixed 2‑word value.
  • hasCtx is explicit; call sites branch on it instead of env == nil.
  • Env data is inline after the header instead of in a separate object.
  • IR/snapshots differ primarily in closure layout and env access paths.

Covered Scenarios

Plain funcs, captured closures, method values/expressions, interface method values, varargs, go/defer, C callbacks.

@gemini-code-assist
Copy link

Note

The number of changes in this pull request is too large for Gemini Code Assist to generate a summary.

@xgopilot
Copy link
Contributor

xgopilot bot commented Feb 2, 2026

Code Review Summary

This is a well-designed implementation of register-based closure context passing. The 2-word ABI {fn, env} is clean, eliminates the need for wrapper stubs, and provides good performance on supported platforms (amd64, arm64, 386, riscv64).

Strengths

  • Clean architecture: Single source of truth for closure representation
  • Comprehensive testing: Extensive unit tests in ssa/ssa_test.go covering multiple architectures
  • Good documentation: Clear explanations in ssa/target.go and the PR description
  • Proper inline asm constraints: Correct use of sideeffects, clobbers, and register constraints

Key Areas for Improvement

  1. Dead code: sawReservedRegWarning in cl/cltest/cltest.go is never set to true

  2. Code duplication: filterLinkerWarnings() and filterExpectOutput() are nearly identical - consider extracting to a shared utility

  3. Duplicate definitions: Context register info defined in 3 places (internal/ctxreg, ssa/target.go, ssa/ctxreg.go) - consider consolidating

  4. Missing nil check: Runtime functions in alg_ctxreg.go cast closure pointer directly without validation - a defensive nil check could improve debuggability

  5. PR description accuracy: IR examples show LLVM intrinsics but implementation uses inline assembly

See inline comments for specific feedback.

@codecov
Copy link

codecov bot commented Feb 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.51%. Comparing base (505a3fb) to head (bb3a9cc).
⚠️ Report is 39 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1599      +/-   ##
==========================================
- Coverage   91.00%   90.51%   -0.49%     
==========================================
  Files          45       48       +3     
  Lines       11927    12120     +193     
==========================================
+ Hits        10854    10971     +117     
- Misses        898      958      +60     
- Partials      175      191      +16     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@cpunion cpunion changed the title Implement register-based closure ctx (2-word ABI) Implement register-based closure ctx Feb 3, 2026
luoliwoshang added a commit to luoliwoshang/llgo that referenced this pull request Feb 3, 2026
This test is for issue goplus#1559, not goplus#1599.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
xgopilot bot pushed a commit to luoliwoshang/llgo that referenced this pull request Feb 5, 2026
This test is for issue goplus#1559, not goplus#1599.

Generated with [codeagent](https://github.com/qbox/codeagent)
Co-authored-by: luoliwoshang <51194195+luoliwoshang@users.noreply.github.com>
@xushiwei xushiwei merged commit ba842c4 into goplus:main Feb 5, 2026
44 of 45 checks passed
@cpunion cpunion deleted the closure-ctxreg-v2 branch February 12, 2026 00:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants