From 335c4060da289bda2456b460aa18e8157ea8cc79 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 12:16:23 +0800 Subject: [PATCH 1/2] ssa, cl: fix zero-sized global init and fold composite literal alloc stores --- cl/rewrite_internal_test.go | 208 +++++++++++++++++++++++++++++++++--- cl/static_init.go | 149 ++++++++++++++++++++++++-- ssa/decl.go | 6 ++ 3 files changed, 344 insertions(+), 19 deletions(-) diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 0eeb380771..f6fa40b78f 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -205,6 +205,12 @@ func Use() string { if strings.Contains(ir, "@staticinit.MethodNames = global %staticinit.Names zeroinitializer") { t.Fatalf("MethodNames still uses a zero initializer:\n%s", ir) } + if !strings.Contains(ir, `[%"github.com/xgo-dev/llgo/runtime/internal/runtime.String" { ptr @0, i64 9 }, %"github.com/xgo-dev/llgo/runtime/internal/runtime.String" { ptr @1, i64 12 }]`) { + t.Fatalf("unexpected MethodNames.Value initializer:\n%s", ir) + } + if !strings.Contains(ir, `%staticinit.Nested { [2 x %"github.com/xgo-dev/llgo/runtime/internal/runtime.String"] [%"github.com/xgo-dev/llgo/runtime/internal/runtime.String" { ptr @2, i64 8 }, %"github.com/xgo-dev/llgo/runtime/internal/runtime.String" { ptr @3, i64 11 }] }`) { + t.Fatalf("unexpected MethodNames.Nested initializer:\n%s", ir) + } for _, want := range []string{`c"KeepValue"`, `c"KeepValueAlt"`, `c"KeepType"`, `c"KeepTypeAlt"`} { if !strings.Contains(ir, want) { t.Fatalf("missing %s in IR:\n%s", want, ir) @@ -320,9 +326,8 @@ var Value = Outer{ } var ( - blankSliceField *ssa.FieldAddr - sawDirectBlank, sawNestedBlank bool - sawBlankArray, sawNonBlankSibling bool + blankSliceField *ssa.FieldAddr + sawDirectBlank, sawNonBlankSibling bool ) initFn := pkg.Func("init") for _, block := range initFn.Blocks { @@ -353,21 +358,15 @@ var Value = Outer{ switch { case len(fields) == 1 && fields[0] == "_": sawDirectBlank = true - case len(fields) > 1 && want && !indexed: - sawNestedBlank = true - case want && indexed: - sawBlankArray = true - case !want: + case !want && !indexed: sawNonBlankSibling = true } } } - if !sawDirectBlank || !sawNestedBlank || !sawBlankArray || !sawNonBlankSibling { + if !sawDirectBlank || !sawNonBlankSibling { t.Fatalf( - "missing SSA classification coverage: direct=%v nested=%v array=%v sibling=%v", + "missing SSA classification coverage: direct=%v sibling=%v", sawDirectBlank, - sawNestedBlank, - sawBlankArray, sawNonBlankSibling, ) } @@ -1382,3 +1381,188 @@ func f() {} t.Fatal("compiled owner should be cached") } } + +func TestCollectAllocStoresFromSSA(t *testing.T) { + const src = `package allocstore + +type Point struct { + X, Y int +} + +type Nested struct { + P Point + Arr [2]int + Tag string +} + +func testConstNested() Nested { + return Nested{ + P: Point{10, 20}, + Arr: [2]int{30, 40}, + Tag: "hello", + } +} + +func testConstPoint() Point { + return Point{100, 200} +} + +func testDynamic() Point { + return Point{next(), 200} +} + +func testCall(p Point) {} + +func testEscape() { + var p = Point{1, 2} + testCall(p) + var n = Nested{P: Point{3, 4}} + pRef := &n.P + pRef.X = 99 +} + +func testArrayInit() [2]int { + var a [2]int + a[0] = 10 + a[1] = 20 + return a +} + +func testDirectStore() int { + var x int + x = 42 + return x +} + +func testArrayDynamic() [2]int { + var a [2]int + a[0] = next() + return a +} + +func next() int { return 1 } +` + fset := token.NewFileSet() + file, err := parser.ParseFile(fset, "allocstore.go", src, 0) + if err != nil { + t.Fatal(err) + } + importer := gpackages.NewImporter(fset) + pkg, _, err := ssautil.BuildPackage( + &types.Config{Importer: importer}, + fset, + types.NewPackage("allocstore", "allocstore"), + []*ast.File{file}, + ssa.SanityCheckFunctions, + ) + if err != nil { + t.Fatal(err) + } + + var foundAllocs []*ssa.Alloc + var foundStores []*ssa.Store + var foundFields []*ssa.FieldAddr + var foundIndices []*ssa.IndexAddr + + for _, member := range pkg.Members { + fn, ok := member.(*ssa.Function) + if !ok { + continue + } + for _, block := range fn.Blocks { + for _, instr := range block.Instrs { + switch instr := instr.(type) { + case *ssa.Alloc: + if !instr.Heap { + foundAllocs = append(foundAllocs, instr) + } + case *ssa.Store: + foundStores = append(foundStores, instr) + case *ssa.FieldAddr: + foundFields = append(foundFields, instr) + case *ssa.IndexAddr: + foundIndices = append(foundIndices, instr) + } + } + } + } + + if len(foundAllocs) == 0 { + t.Fatal("expected to find local allocs in SSA") + } + + // Test collectAllocStores on all found allocs + for _, alloc := range foundAllocs { + var stores []staticInitStore + var instrs []ssa.Instruction + collectAllocStores(alloc, nil, &stores, &instrs, make(map[*ssa.Alloc]bool)) + collectAllocStores(alloc, []staticInitPathElem{{index: 1}}, &stores, &instrs, make(map[*ssa.Alloc]bool)) + } + + targetAlloc := foundAllocs[0] + + // 1. Cycle detection + var stores []staticInitStore + var instrs []ssa.Instruction + visited := map[*ssa.Alloc]bool{targetAlloc: true} + if collectAllocStores(targetAlloc, nil, &stores, &instrs, visited) { + t.Fatal("expected cycle protection to return false") + } + + // 2. appendStaticInitPath + p1 := []staticInitPathElem{{index: 1}, {index: 2}} + p2 := []staticInitPathElem{{index: 3}} + merged := appendStaticInitPath(p1, p2) + if len(merged) != 3 || merged[0].index != 1 || merged[1].index != 2 || merged[2].index != 3 { + t.Fatalf("unexpected appendStaticInitPath result: %+v", merged) + } + + // 3. handleStoreVal branches + if len(foundStores) > 0 { + store := foundStores[0] + // Test Const store + cStore := &ssa.Store{Addr: store.Addr, Val: ssa.NewConst(constant.MakeInt64(1), types.Typ[types.Int])} + stores = nil + if !handleStoreVal(cStore, p1, &stores, &instrs, make(map[*ssa.Alloc]bool)) { + t.Fatal("handleStoreVal failed on const") + } + if len(stores) != 1 || len(stores[0].path) != 2 { + t.Fatalf("unexpected handleStoreVal result: %+v", stores) + } + + // Test non-const non-unop store + badStore := &ssa.Store{Addr: store.Addr, Val: store.Addr} + if handleStoreVal(badStore, p1, &stores, &instrs, make(map[*ssa.Alloc]bool)) { + t.Fatal("expected handleStoreVal to fail on non-const, non-alloc Val") + } + + // Test Heap alloc + heapAlloc := &ssa.Alloc{Heap: true, Comment: "heap"} + heapUnOp := &ssa.UnOp{Op: token.MUL, X: heapAlloc} + heapStore := &ssa.Store{Addr: store.Addr, Val: heapUnOp} + if handleStoreVal(heapStore, p1, &stores, &instrs, make(map[*ssa.Alloc]bool)) { + t.Fatal("expected handleStoreVal to fail on heap alloc") + } + } + + // 4. staticInitStorePathToAlloc edge cases + if _, ok := staticInitStorePathToAlloc(nil, targetAlloc); ok { + t.Fatal("expected nil addr to fail") + } + if path, ok := staticInitStorePathToAlloc(targetAlloc, targetAlloc); !ok || len(path) != 0 { + t.Fatalf("expected exact alloc to return empty path, got %+v, %v", path, ok) + } + if len(foundAllocs) > 1 { + if _, ok := staticInitStorePathToAlloc(foundAllocs[1], targetAlloc); ok { + t.Fatal("expected different alloc to fail") + } + } + if len(foundFields) > 0 { + field := foundFields[0] + _, _ = staticInitStorePathToAlloc(field, targetAlloc) + } + if len(foundIndices) > 0 { + index := foundIndices[0] + _, _ = staticInitStorePathToAlloc(index, targetAlloc) + } +} diff --git a/cl/static_init.go b/cl/static_init.go index 7edd7fa942..516da74580 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -18,6 +18,7 @@ package cl import ( "go/constant" + "go/token" "go/types" "sort" "strings" @@ -43,6 +44,7 @@ type staticInitStore struct { type staticInitCandidate struct { stores []staticInitStore slice *staticSliceInit + instrs []ssa.Instruction invalid bool } @@ -133,16 +135,27 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { continue } } - value, isConst := store.Val.(*ssa.Const) - if !ok || !isConst { + if value, isConst := store.Val.(*ssa.Const); isConst { + candidate.stores = append(candidate.stores, staticInitStore{ + store: store, + path: path, + value: value, + }) + } else if unop, ok := store.Val.(*ssa.UnOp); ok && unop.Op == token.MUL { + if alloc, ok := unop.X.(*ssa.Alloc); ok && !alloc.Heap { + if !collectAllocStores(alloc, path, &candidate.stores, &candidate.instrs, make(map[*ssa.Alloc]bool)) { + candidate.invalid = true + continue + } + candidate.instrs = append(candidate.instrs, unop, store) + } else { + candidate.invalid = true + continue + } + } else { candidate.invalid = true continue } - candidate.stores = append(candidate.stores, staticInitStore{ - store: store, - path: path, - value: value, - }) } } @@ -180,12 +193,134 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { p.staticInitInstrs[instr] = none{} } } + for _, instr := range candidate.instrs { + p.staticInitInstrs[instr] = none{} + } for _, store := range candidate.stores { p.staticInitStores[store.store] = none{} } } } +func collectAllocStores(alloc *ssa.Alloc, basePath []staticInitPathElem, out *[]staticInitStore, instrs *[]ssa.Instruction, visited map[*ssa.Alloc]bool) bool { + if visited[alloc] { + return false + } + visited[alloc] = true + *instrs = append(*instrs, alloc) + + refs, ok := nonDebugReferrers(alloc) + if !ok { + return false + } + for _, ref := range refs { + switch ref := ref.(type) { + case *ssa.UnOp: + if ref.Op != token.MUL { + return false + } + *instrs = append(*instrs, ref) + case *ssa.FieldAddr: + subPath, ok := staticInitStorePathToAlloc(ref, alloc) + if !ok { + return false + } + fieldRefs, ok := nonDebugReferrers(ref) + if !ok || len(fieldRefs) != 1 { + return false + } + elemStore, ok := fieldRefs[0].(*ssa.Store) + if !ok || elemStore.Addr != ref { + return false + } + if !handleStoreVal(elemStore, appendStaticInitPath(basePath, subPath), out, instrs, visited) { + return false + } + *instrs = append(*instrs, ref, elemStore) + case *ssa.IndexAddr: + subPath, ok := staticInitStorePathToAlloc(ref, alloc) + if !ok { + return false + } + indexRefs, ok := nonDebugReferrers(ref) + if !ok || len(indexRefs) != 1 { + return false + } + elemStore, ok := indexRefs[0].(*ssa.Store) + if !ok || elemStore.Addr != ref { + return false + } + if !handleStoreVal(elemStore, appendStaticInitPath(basePath, subPath), out, instrs, visited) { + return false + } + *instrs = append(*instrs, ref, elemStore) + case *ssa.Store: + if ref.Addr != alloc { + return false + } + if !handleStoreVal(ref, appendStaticInitPath(basePath, nil), out, instrs, visited) { + return false + } + *instrs = append(*instrs, ref) + default: + return false + } + } + return true +} + +func appendStaticInitPath(base, sub []staticInitPathElem) []staticInitPathElem { + res := make([]staticInitPathElem, len(base)+len(sub)) + copy(res, base) + copy(res[len(base):], sub) + return res +} + +func handleStoreVal(store *ssa.Store, fullPath []staticInitPathElem, out *[]staticInitStore, instrs *[]ssa.Instruction, visited map[*ssa.Alloc]bool) bool { + if val, ok := store.Val.(*ssa.Const); ok { + *out = append(*out, staticInitStore{ + store: store, + path: fullPath, + value: val, + }) + return true + } + if unop, ok := store.Val.(*ssa.UnOp); ok && unop.Op == token.MUL { + if innerAlloc, ok := unop.X.(*ssa.Alloc); ok && !innerAlloc.Heap { + return collectAllocStores(innerAlloc, fullPath, out, instrs, visited) + } + } + return false +} + +func staticInitStorePathToAlloc(addr ssa.Value, target *ssa.Alloc) ([]staticInitPathElem, bool) { + switch addr := addr.(type) { + case *ssa.Alloc: + if addr == target { + return nil, true + } + return nil, false + case *ssa.FieldAddr: + path, ok := staticInitStorePathToAlloc(addr.X, target) + if !ok { + return nil, false + } + return append(path, staticInitPathElem{index: addr.Field}), true + case *ssa.IndexAddr: + path, ok := staticInitStorePathToAlloc(addr.X, target) + if !ok { + return nil, false + } + index, ok := staticInitConstIndex(addr.Index) + if !ok { + return nil, false + } + return append(path, staticInitPathElem{index: index}), true + default: + return nil, false + } +} + func staticSliceInitOf(store *ssa.Store) (*staticSliceInit, bool) { slice, ok := store.Val.(*ssa.Slice) if !ok || slice.Low != nil || slice.High != nil || slice.Max != nil { diff --git a/ssa/decl.go b/ssa/decl.go index 43d1a87afc..7a02a912b5 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -172,10 +172,16 @@ func (p Package) VarOf(name string) Global { // Init initializes the global variable with the given value. func (g Global) Init(v Expr) { + if g.impl.Name() == moduleZeroName { + return + } g.impl.SetInitializer(v.impl) } func (g Global) InitNil() { + if g.impl.Name() == moduleZeroName { + return + } g.impl.SetInitializer(llvm.ConstNull(g.impl.GlobalValueType())) } From 7624d51784072c6f3773fe7f0b0d10f94fd23963 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 12:57:42 +0800 Subject: [PATCH 2/2] cl: validate UnOp single-consumer during static init folding and add indirection tests --- cl/rewrite_internal_test.go | 34 ++++++++++++++++++++++++++++++++++ cl/static_init.go | 13 +++++++++++++ 2 files changed, 47 insertions(+) diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index f6fa40b78f..8277369b3d 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -1566,3 +1566,37 @@ func next() int { return 1 } _, _ = staticInitStorePathToAlloc(index, targetAlloc) } } + +func TestStaticGlobalPointerIndirectionLiteralInit(t *testing.T) { + const src = `package staticinit + +type Inner struct { + A [2]int + B string +} + +type Outer struct { + I Inner + Val int +} + +var G = Outer{ + I: Inner{ + A: [2]int{10, 20}, + B: "hello", + }, + Val: 99, +} + +func Use() int { + return G.I.A[0] + G.I.A[1] + len(G.I.B) + G.Val +} +` + ir := compileWithRewrites(t, src, nil) + if strings.Contains(ir, "@staticinit.G = global %staticinit.Outer zeroinitializer") { + t.Fatalf("G still uses a zero initializer:\n%s", ir) + } + if !strings.Contains(ir, `c"hello"`) { + t.Fatalf("missing hello in IR:\n%s", ir) + } +} diff --git a/cl/static_init.go b/cl/static_init.go index 516da74580..2d3cc8e1ba 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -143,6 +143,11 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { }) } else if unop, ok := store.Val.(*ssa.UnOp); ok && unop.Op == token.MUL { if alloc, ok := unop.X.(*ssa.Alloc); ok && !alloc.Heap { + unopRefs, ok := nonDebugReferrers(unop) + if !ok || len(unopRefs) != 1 || unopRefs[0] != store { + candidate.invalid = true + continue + } if !collectAllocStores(alloc, path, &candidate.stores, &candidate.instrs, make(map[*ssa.Alloc]bool)) { candidate.invalid = true continue @@ -219,6 +224,10 @@ func collectAllocStores(alloc *ssa.Alloc, basePath []staticInitPathElem, out *[] if ref.Op != token.MUL { return false } + unopRefs, ok := nonDebugReferrers(ref) + if !ok || len(unopRefs) != 1 { + return false + } *instrs = append(*instrs, ref) case *ssa.FieldAddr: subPath, ok := staticInitStorePathToAlloc(ref, alloc) @@ -287,6 +296,10 @@ func handleStoreVal(store *ssa.Store, fullPath []staticInitPathElem, out *[]stat } if unop, ok := store.Val.(*ssa.UnOp); ok && unop.Op == token.MUL { if innerAlloc, ok := unop.X.(*ssa.Alloc); ok && !innerAlloc.Heap { + unopRefs, ok := nonDebugReferrers(unop) + if !ok || len(unopRefs) != 1 || unopRefs[0] != store { + return false + } return collectAllocStores(innerAlloc, fullPath, out, instrs, visited) } }