Skip to content

cl: preserve generic local type identity in nested closures - #2229

Open
cpunion wants to merge 7 commits into
xgo-dev:mainfrom
cpunion:codex/xfail-run-audit-20260730
Open

cl: preserve generic local type identity in nested closures#2229
cpunion wants to merge 7 commits into
xgo-dev:mainfrom
cpunion:codex/xfail-run-audit-20260730

Conversation

@cpunion

@cpunion cpunion commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator

Summary

  • patch captured free-variable types under the callee closure's generic context
  • resolve local declarations through the closure Parent/Origin syntax chain
  • preserve distinct runtime and LLVM type identities for each enclosing generic instantiation
  • add runtime and IR regressions for local types used by nested closures

Fixes #2304.

Bug

Go requires a local box declared in boxFuncs[int] to have a different type identity from the corresponding box in boxFuncs[string]. On current main, nested closures omit the enclosing instantiation and both interface conversions use the same runtime type descriptor. Cross-instantiation assertions therefore incorrectly succeed:

main: true true true true
want: true true false false

The runnable tplocalclosureiface regression reproduces this with an int box and a differently laid-out string box. With this PR, their closures use distinct box[int] and box[string] descriptors and produce the expected result.

The same omission also gave the generic Y-combinator's dynamic call and actual closure entry different LLVM nominal types. The IR regression requires both sides of each int and string call edge to use the same instantiated type.

Validation

  • go test ./cl -run 'TestGenericLocalRecursiveClosureContextTypePatch|TestRunAndTestFromTestgo/(tplocalclosureiface|tpycombinator)' -count=1
  • go test ./ssa -run 'TestFromTestgo/(tplocalclosureiface|tpycombinator)' -count=1
  • go run ./cl/_testgo/tplocalclosureiface
  • git diff --check

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review summary

The core fix is well-scoped and correct: makeClosureCtx now becomes a method that temporarily sets p.goFn = fn (save/restore via defer, single exit path — correct) and patches each free-var field type with p.patchType(v.Type()). This makes patchLocalGenericNamed/localTypeOuterArgs resolve captured local generic types against the correct instantiated closure, so a closure-context field and the closure signature land on the same nominal type. The enclosingFunctionSyntax refactor is a sensible generalization, and the unit test plus the tpycombinator runtime golden lock the behavior in.

Verified as correct (no action needed):

  • Golden expect.txt (3628800 / xxx) matches factorial(10) and repeat(""); println goes to stderr, and the cltest harness captures stderr (cl/cltest/cltest.go), so the golden convention holds.
  • No security surface: enclosingFunctionSyntax nil-checks fn.Origin()/Syntax(), the parent walk terminates at nil, and the xfail.yaml removal / comment changes are consistent.

Findings below are maintainability/efficiency only — none block merge.

Maintainability

  • cl/compile.gocurrentFunctionSyntax is now dead production code. After migrating inCurrentFunction and localTypeOrdinalBySyntax to enclosingFunctionSyntax, currentFunctionSyntax has no remaining production caller (grep finds only its definition and the nil-goFn assertion in funcname_nested_closure_test.go:405). It duplicates the p.goFn == nil / fn.Origin() / Syntax() logic and can silently diverge from enclosingFunctionSyntax — e.g. it returns the raw p.goFn syntax even when that syntax does not bracket a given position, whereas the new helper returns nil. Consider deleting it (and the test assertion) so there is one source of truth; enclosingFunctionSyntax's self-case (fn == p.goFn) already covers its former use. (See inline comment.)

  • cl/compile.go:2544 enclosingFunctionSyntax — undocumented behavioral widening. The old inCurrentFunction matched only p.goFn's own syntax bounds; the new helper walks the fn.Parent() chain and returns the first ancestor whose [Pos, End] brackets pos. This is the intended fix (a closure still sees generic locals declared in an enclosing generic function), and it is safe because nested-closure syntax is lexically contained, so innermost matches first and types declared in the current goFn are unaffected. But the parent walk is load-bearing and non-obvious — a one-line comment ("walk parents so a closure still sees generic locals declared in an enclosing generic function") would protect it from a future "simplification."

Efficiency (compile-time only, optional)

  • cl/compile.go:2521-2542 — redundant enclosingFunctionSyntax recomputation. A single localNamedName call can reach enclosingFunctionSyntax(pos) twice for the same pos (via localTypeOrdinallocalTypeOrdinalBySyntax, and via localTypeOuterArgsisGenericLocalTypeinCurrentFunction), each re-walking the parent chain, and localTypeOrdinalBySyntax runs a fresh ast.Inspect over the enclosing function each time. Bounded, narrow-case, compile-time only — but if nested-closure + local-generic mangling shows up hot, memoizing enclosingFunctionSyntax / caching the per-function TypeSpec ordinal would remove the repeated traversals. Not required for this PR.

Comment thread cl/compile.go Outdated
@codecov

codecov Bot commented Jul 30, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

@cpunion

cpunion commented Jul 30, 2026

Copy link
Copy Markdown
Collaborator Author

Fixed the Ubuntu Go workflow failure in 876d13d8f. The new tpycombinator runtime fixture was also discovered by ssa.TestFromTestgo, whose IR-only path requires an IR spec. Instead of skipping that check, the source now has FileCheck assertions requiring distinct int and string local-generic closure-context types, which directly covers this PR’s type-identity invariant.

Validation:

  • go test ./ssa -run "TestFromTestgo/tpycombinator" -count=1 -v
  • go test ./cl -run "TestRunAndTestFromTestgo/tpycombinator" -count=1 -v
  • go test ./ssa -count=1
  • git diff --check

The earlier review-body suggestions are also present in the current head: the dead currentFunctionSyntax helper was removed, and enclosingFunctionSyntax documents why it walks parent functions.

@cpunion
cpunion force-pushed the codex/xfail-run-audit-20260730 branch from 876d13d to 020a2e3 Compare July 30, 2026 13:09
@cpunion cpunion added the go-test-compat Go standard-library and GOROOT test compatibility label Aug 9, 2026
@cpunion
cpunion force-pushed the codex/xfail-run-audit-20260730 branch from 020a2e3 to 35e4a41 Compare August 10, 2026 02:27
@github-actions

github-actions Bot commented Aug 10, 2026

Copy link
Copy Markdown

LLGo baseline benchmarks

8d1e3c0de022 | workflow run | long-term charts

Program measurements

Platform Workload File size vs base Build vs base Run vs base
Linux cprintf 18656 B +0.0% 303.225 ms +0.2% (worse) 1.350 ms -1.1% (better)
Linux fmtprintf 1881664 B +0.0% 3.023 s +4.7% (worse) 3.628 ms +0.7% (worse)
Linux println 68512 B +0.0% 303.669 ms -0.9% (better) 1.754 ms -1.6% (better)
macOS cprintf 84672 B +0.0% 386.488 ms -20.1% (better) 3.623 ms +20.8% (worse)
macOS fmtprintf 1889248 B +0.0% 2.993 s -2.6% (better) 15.640 ms -3.6% (better)
macOS println 121216 B +0.0% 382.859 ms -10.0% (better) 4.396 ms -18.8% (better)
Core language and compiler benchmarks
Platform Benchmark ns/op vs base
Linux BenchmarkLookupPCRandom 12.330 ns/op +0.6% (worse)
Linux BenchmarkMergeCompilerFlags 145.300 ns/op +0.6% (worse)
Linux BenchmarkMergeLinkerFlags 94.980 ns/op +1.0% (worse)
Linux BenchmarkChannelBuffered 36.370 ns/op -0.6% (better)
Linux BenchmarkChannelHandoff 23401 ns/op -0.3% (better)
Linux BenchmarkDefer 50.830 ns/op +2.0% (worse)
Linux BenchmarkDirectCall 1.773 ns/op +0.9% (worse)
Linux BenchmarkGlobalRead 1.774 ns/op +0.9% (worse)
Linux BenchmarkGlobalWrite 2.821 ns/op +0.5% (worse)
Linux BenchmarkGoroutine 30977 ns/op +0.6% (worse)
Linux BenchmarkInterfaceCall 8.503 ns/op +0.6% (worse)
Linux BenchmarkRuntimeGetG 2.463 ns/op +0.0% (worse)
macOS BenchmarkLookupPCRandom 11.590 ns/op -4.2% (better)
macOS BenchmarkMergeCompilerFlags 123.300 ns/op -4.0% (better)
macOS BenchmarkMergeLinkerFlags 76.350 ns/op -4.3% (better)
macOS BenchmarkChannelBuffered 22.570 ns/op -8.1% (better)
macOS BenchmarkChannelHandoff 8085 ns/op -4.8% (better)
macOS BenchmarkDefer 35.070 ns/op +11.2% (worse)
macOS BenchmarkDirectCall 1.153 ns/op +12.6% (worse)
macOS BenchmarkGlobalRead 1.032 ns/op -1.7% (better)
macOS BenchmarkGlobalWrite 1.237 ns/op +8.5% (worse)
macOS BenchmarkGoroutine 36958 ns/op -26.6% (better)
macOS BenchmarkInterfaceCall 5.271 ns/op +9.6% (worse)
macOS BenchmarkRuntimeGetG 2.092 ns/op -5.9% (better)

Compared with c454c0e65b71 measured in the same runner job.

@cpunion
cpunion force-pushed the codex/xfail-run-audit-20260730 branch from 35e4a41 to 42ff689 Compare August 11, 2026 10:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

go-test-compat Go standard-library and GOROOT test compatibility

Projects

None yet

Development

Successfully merging this pull request may close these issues.

cl: generic local types in closures share interface identity

1 participant