From 4fbb97f9bd15eae4c7fd1b8575ba87b6356c64bc Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 10:26:07 +0800 Subject: [PATCH 1/5] cl: reproduce generic local function capture mismatch --- cl/_testgo/tpycombinator/expect.txt | 1 + cl/_testgo/tpycombinator/in.go | 24 ++++++++++++++++++ cl/funcname_nested_closure_test.go | 38 +++++++++++++++++++++++++++++ 3 files changed, 63 insertions(+) create mode 100644 cl/_testgo/tpycombinator/expect.txt create mode 100644 cl/_testgo/tpycombinator/in.go diff --git a/cl/_testgo/tpycombinator/expect.txt b/cl/_testgo/tpycombinator/expect.txt new file mode 100644 index 0000000000..3fbd4a8698 --- /dev/null +++ b/cl/_testgo/tpycombinator/expect.txt @@ -0,0 +1 @@ +3628800 diff --git a/cl/_testgo/tpycombinator/in.go b/cl/_testgo/tpycombinator/in.go new file mode 100644 index 0000000000..0df40682b0 --- /dev/null +++ b/cl/_testgo/tpycombinator/in.go @@ -0,0 +1,24 @@ +package main + +func Y[Endo ~func(RecFct) RecFct, RecFct ~func(T) R, T, R any](f Endo) RecFct { + type internal[RecFct ~func(T) R, T, R any] func(internal[RecFct, T, R]) RecFct + + g := func(h internal[RecFct, T, R]) RecFct { + return func(t T) R { + return f(h(h))(t) + } + } + return g(g) +} + +func main() { + factorial := Y(func(recur func(int) int) func(int) int { + return func(n int) int { + if n == 0 { + return 1 + } + return n * recur(n-1) + } + }) + println(factorial(10)) +} diff --git a/cl/funcname_nested_closure_test.go b/cl/funcname_nested_closure_test.go index 6f9034ea6c..29ee17073c 100644 --- a/cl/funcname_nested_closure_test.go +++ b/cl/funcname_nested_closure_test.go @@ -136,6 +136,44 @@ func localType[T any]() any { } } +func TestGenericLocalRecursiveFunctionTypePatch(t *testing.T) { + ssapkg := buildSSAPackage(t, `package foo + +func Y[Endo ~func(RecFct) RecFct, RecFct ~func(T) R, T, R any](f Endo) RecFct { + type internal[RecFct ~func(T) R, T, R any] func(internal[RecFct, T, R]) RecFct + g := func(h internal[RecFct, T, R]) RecFct { + return func(t T) R { return f(h(h))(t) } + } + return g(g) +} + +func use() { + f := Y(func(recur func(int) int) func(int) int { return recur }) + _ = f(1) +} +`) + + var checked bool + for fn := range ssautil.AllFunctions(ssapkg.Prog) { + if fn == nil || !strings.HasSuffix(fn.Name(), "]$1") || len(fn.TypeArgs()) == 0 { + continue + } + ctx := &context{goFn: fn} + patched := ctx.patchType(fn.Signature).(*types.Signature) + param, ok := patched.Params().At(0).Type().(*types.Named) + if !ok { + t.Fatalf("patched Y$1 parameter = %T, want named local function type", patched.Params().At(0).Type()) + } + if name := param.Obj().Name(); !strings.Contains(name, ";") { + t.Fatalf("patched Y$1 parameter name = %q, want outer and own type arguments", name) + } + checked = true + } + if !checked { + t.Fatal("instantiated Y$1 closure not found") + } +} + func TestGenericLocalTypePatchHelperBranches(t *testing.T) { ssapkg := buildSSAPackage(t, `package foo From f735cfe034cdc9c7a8f46366133d38b3f0c00c3b Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 10:26:54 +0800 Subject: [PATCH 2/5] cl: patch captured generic local types --- cl/compile.go | 39 +++++++++++++++++++++++++++++---------- test/goroot/xfail.yaml | 4 ---- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 8f99b9a0f5..356de63679 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -415,7 +415,13 @@ func (p *context) compileGlobal(pkg llssa.Package, gbl *ssa.Global) { } } -func makeClosureCtx(pkg *types.Package, vars []*ssa.FreeVar) *types.Var { +func (p *context) makeClosureCtx(fn *ssa.Function, pkg *types.Package, vars []*ssa.FreeVar) *types.Var { + oldGoFn := p.goFn + p.goFn = fn + defer func() { + p.goFn = oldGoFn + }() + n := len(vars) flds := make([]*types.Var, n) for i, v := range vars { @@ -423,7 +429,7 @@ func makeClosureCtx(pkg *types.Package, vars []*ssa.FreeVar) *types.Var { if name == "" { name = "_" } - flds[i] = types.NewField(token.NoPos, pkg, name, v.Type(), false) + flds[i] = types.NewField(token.NoPos, pkg, name, p.patchType(v.Type()), false) } t := types.NewPointer(types.NewStruct(flds, nil)) return types.NewParam(token.NoPos, pkg, "__llgo_ctx", t) @@ -552,7 +558,7 @@ func (p *context) compileFuncDecl(pkg llssa.Package, f *ssa.Function) (llssa.Fun var hasCtx = len(f.FreeVars) > 0 if hasCtx { dbgInstrln("==> NewClosure", name, "type:", sig) - ctx := makeClosureCtx(pkgTypes, f.FreeVars) + ctx := p.makeClosureCtx(f, pkgTypes, f.FreeVars) sig = llssa.FuncAddCtx(ctx, sig) } else { dbgInstrln("==> NewFunc", name, "type:", sig.Recv(), sig, "ftype:", ftype) @@ -2513,19 +2519,16 @@ func (p *context) localTypeOrdinal(obj types.Object) int { } func (p *context) inCurrentFunction(pos token.Pos) bool { - if !pos.IsValid() { - return false - } - syntax := p.currentFunctionSyntax() - return syntax != nil && syntax.Pos() <= pos && pos <= syntax.End() + return p.enclosingFunctionSyntax(pos) != nil } func (p *context) localTypeOrdinalBySyntax(pos token.Pos) int { - if !p.inCurrentFunction(pos) { + syntax := p.enclosingFunctionSyntax(pos) + if syntax == nil { return 0 } n := 0 - ast.Inspect(p.currentFunctionSyntax(), func(node ast.Node) bool { + ast.Inspect(syntax, func(node ast.Node) bool { spec, ok := node.(*ast.TypeSpec) if !ok { return true @@ -2538,6 +2541,22 @@ func (p *context) localTypeOrdinalBySyntax(pos token.Pos) int { return n } +func (p *context) enclosingFunctionSyntax(pos token.Pos) ast.Node { + if !pos.IsValid() { + return nil + } + for fn := p.goFn; fn != nil; fn = fn.Parent() { + syntaxFn := fn + if origin := fn.Origin(); origin != nil { + syntaxFn = origin + } + if syntax := syntaxFn.Syntax(); syntax != nil && syntax.Pos() <= pos && pos <= syntax.End() { + return syntax + } + } + return nil +} + func (p *context) currentFunctionSyntax() ast.Node { if p.goFn == nil { return nil diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index 8183574996..125c11cdae 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -2412,10 +2412,6 @@ xfails: directive: run case: fixedbugs/issue31546.go reason: reflection exposes the initializer value of a blank struct field instead of zero on darwin/arm64 - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue72063.go - reason: generic Y-combinator build exits successfully without producing a binary on darwin/arm64 - platform: darwin/arm64 directive: run case: fixedbugs/issue72844.go From 23331a8f4929622101794c2b5fe78fc79aa55ac7 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 10:51:36 +0800 Subject: [PATCH 3/5] cl: strengthen generic local closure coverage --- cl/_testgo/tpycombinator/expect.txt | 1 + cl/_testgo/tpycombinator/in.go | 9 +++ cl/funcname_nested_closure_test.go | 88 ++++++++++++++++++++++++----- 3 files changed, 83 insertions(+), 15 deletions(-) diff --git a/cl/_testgo/tpycombinator/expect.txt b/cl/_testgo/tpycombinator/expect.txt index 3fbd4a8698..c116b4f11f 100644 --- a/cl/_testgo/tpycombinator/expect.txt +++ b/cl/_testgo/tpycombinator/expect.txt @@ -1 +1,2 @@ 3628800 +xxx diff --git a/cl/_testgo/tpycombinator/in.go b/cl/_testgo/tpycombinator/in.go index 0df40682b0..4414223540 100644 --- a/cl/_testgo/tpycombinator/in.go +++ b/cl/_testgo/tpycombinator/in.go @@ -20,5 +20,14 @@ func main() { return n * recur(n-1) } }) + repeat := Y(func(recur func(string) string) func(string) string { + return func(s string) string { + if len(s) == 3 { + return s + } + return recur(s + "x") + } + }) println(factorial(10)) + println(repeat("")) } diff --git a/cl/funcname_nested_closure_test.go b/cl/funcname_nested_closure_test.go index 29ee17073c..2deebc04ce 100644 --- a/cl/funcname_nested_closure_test.go +++ b/cl/funcname_nested_closure_test.go @@ -136,7 +136,7 @@ func localType[T any]() any { } } -func TestGenericLocalRecursiveFunctionTypePatch(t *testing.T) { +func TestGenericLocalRecursiveClosureContextTypePatch(t *testing.T) { ssapkg := buildSSAPackage(t, `package foo func Y[Endo ~func(RecFct) RecFct, RecFct ~func(T) R, T, R any](f Endo) RecFct { @@ -148,29 +148,87 @@ func Y[Endo ~func(RecFct) RecFct, RecFct ~func(T) R, T, R any](f Endo) RecFct { } func use() { - f := Y(func(recur func(int) int) func(int) int { return recur }) - _ = f(1) + intFn := Y(func(recur func(int) int) func(int) int { return recur }) + stringFn := Y(func(recur func(string) string) func(string) string { return recur }) + _ = intFn(1) + _ = stringFn("") } `) - var checked bool - for fn := range ssautil.AllFunctions(ssapkg.Prog) { - if fn == nil || !strings.HasSuffix(fn.Name(), "]$1") || len(fn.TypeArgs()) == 0 { + y := ssapkg.Func("Y") + if y == nil { + t.Fatal("generic Y function not found") + } + names := make(map[types.BasicKind]string) + for instance := range ssautil.AllFunctions(ssapkg.Prog) { + if instance == nil || instance.Origin() != y { continue } - ctx := &context{goFn: fn} - patched := ctx.patchType(fn.Signature).(*types.Signature) - param, ok := patched.Params().At(0).Type().(*types.Named) + args := instance.TypeArgs() + if len(args) < 3 { + t.Fatalf("Y instance type arguments = %v, want T argument", args) + } + basic, ok := args[2].(*types.Basic) + if !ok || basic.Kind() != types.Int && basic.Kind() != types.String { + t.Fatalf("Y instance T = %v, want int or string", args[2]) + } + + if len(instance.AnonFuncs) == 0 { + t.Fatal("Y instance outer closure not found") + } + outer := instance.AnonFuncs[0] + if len(outer.AnonFuncs) == 0 { + t.Fatal("Y instance inner closure not found") + } + inner := outer.AnonFuncs[0] + + ctx := &context{goFn: outer} + patched := ctx.patchType(outer.Signature).(*types.Signature) + param := findLocalNamed(patched.Params().At(0).Type(), ssapkg.Pkg) + if param == nil { + t.Fatalf("patched Y outer closure parameter = %v, want local named function type", patched.Params().At(0).Type()) + } + + hIndex := -1 + for i, freeVar := range inner.FreeVars { + if freeVar.Name() == "h" { + hIndex = i + break + } + } + if hIndex < 0 { + t.Fatalf("Y inner closure free variables = %v, want h", inner.FreeVars) + } + closureCtx := ctx.makeClosureCtx(inner, ssapkg.Pkg, inner.FreeVars) + ptr, ok := closureCtx.Type().(*types.Pointer) + if !ok { + t.Fatalf("closure context type = %T, want pointer", closureCtx.Type()) + } + fields, ok := ptr.Elem().(*types.Struct) if !ok { - t.Fatalf("patched Y$1 parameter = %T, want named local function type", patched.Params().At(0).Type()) + t.Fatalf("closure context element = %T, want struct", ptr.Elem()) } - if name := param.Obj().Name(); !strings.Contains(name, ";") { - t.Fatalf("patched Y$1 parameter name = %q, want outer and own type arguments", name) + hType := findLocalNamed(fields.Field(hIndex).Type(), ssapkg.Pkg) + if hType == nil { + t.Fatalf("closure context h field = %v, want local named function type", fields.Field(hIndex).Type()) } - checked = true + + name := param.Obj().Name() + if !strings.Contains(name, ";") { + t.Fatalf("patched outer parameter name = %q, want outer and own type arguments", name) + } + if hName := hType.Obj().Name(); hName != name { + t.Fatalf("patched h field name = %q, want outer parameter name %q", hName, name) + } + names[basic.Kind()] = name + } + intName, hasInt := names[types.Int] + stringName, hasString := names[types.String] + if !hasInt || !hasString { + t.Fatalf("patched local type names = %v, want int and string instances", names) } - if !checked { - t.Fatal("instantiated Y$1 closure not found") + if intName == stringName { + t.Fatalf("int and string instances share patched local type name %q", intName) } } From 1be80672ae5629477b7cc06cb48f35a281dad733 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 11:10:25 +0800 Subject: [PATCH 4/5] cl: keep enclosing syntax lookup canonical --- cl/compile.go | 13 ++----------- cl/funcname_nested_closure_test.go | 3 --- 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index 356de63679..9dfc1d6851 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -2545,6 +2545,8 @@ func (p *context) enclosingFunctionSyntax(pos token.Pos) ast.Node { if !pos.IsValid() { return nil } + // Instantiated local types may lose their scope parent while a nested + // closure still refers to a declaration in its enclosing generic function. for fn := p.goFn; fn != nil; fn = fn.Parent() { syntaxFn := fn if origin := fn.Origin(); origin != nil { @@ -2557,17 +2559,6 @@ func (p *context) enclosingFunctionSyntax(pos token.Pos) ast.Node { return nil } -func (p *context) currentFunctionSyntax() ast.Node { - if p.goFn == nil { - return nil - } - fn := p.goFn - if origin := fn.Origin(); origin != nil { - fn = origin - } - return fn.Syntax() -} - func isTypeParamObject(obj types.Object) bool { tn, ok := obj.(*types.TypeName) if !ok { diff --git a/cl/funcname_nested_closure_test.go b/cl/funcname_nested_closure_test.go index 2deebc04ce..e1df6b9a14 100644 --- a/cl/funcname_nested_closure_test.go +++ b/cl/funcname_nested_closure_test.go @@ -402,9 +402,6 @@ func use() { if ctx.inCurrentFunction(token.NoPos) { t.Fatal("invalid position should not be inside current function") } - if (&context{}).currentFunctionSyntax() != nil { - t.Fatal("currentFunctionSyntax without goFn should be nil") - } if (&context{}).localTypeOrdinalBySyntax(token.Pos(1)) != 0 { t.Fatal("localTypeOrdinalBySyntax without current function should be zero") } From 020a2e3e3c1bd5863084b0a8e1a1f3ff825d5cc5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 30 Jul 2026 12:46:45 +0800 Subject: [PATCH 5/5] test: add IR checks for generic Y combinator --- cl/_testgo/tpycombinator/in.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/cl/_testgo/tpycombinator/in.go b/cl/_testgo/tpycombinator/in.go index 4414223540..b8180c0a66 100644 --- a/cl/_testgo/tpycombinator/in.go +++ b/cl/_testgo/tpycombinator/in.go @@ -1,5 +1,9 @@ +// LITTEST package main +// CHECK-DAG: %"{{.*}}tpycombinator.internal[{{.*}},int,int;{{.*}},int,int]" = type { ptr, ptr } +// CHECK-DAG: %"{{.*}}tpycombinator.internal[{{.*}},string,string;{{.*}},string,string]" = type { ptr, ptr } + func Y[Endo ~func(RecFct) RecFct, RecFct ~func(T) R, T, R any](f Endo) RecFct { type internal[RecFct ~func(T) R, T, R any] func(internal[RecFct, T, R]) RecFct