From 1c196dcf6c5e15da34cbbc49d0d37231fce90f75 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 18:06:38 +0800 Subject: [PATCH 1/8] cl, ssa: fix zero-sized global init, fold composite literal alloc stores, and harden static init helpers --- cl/rewrite_internal_test.go | 411 ++++++++++++++++++++++++++++++++++-- cl/static_init.go | 177 +++++++++++++++- ssa/decl.go | 10 + 3 files changed, 576 insertions(+), 22 deletions(-) diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 0eeb380771..7ca063740f 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,391 @@ 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) + } +} + +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) + } +} + +func TestStaticInitNodeAddEdgeCases(t *testing.T) { + c1 := &ssa.Const{Value: constant.MakeInt64(1)} + c2 := &ssa.Const{Value: constant.MakeInt64(2)} + + // 1. Setting value on empty path + root := new(staticInitNode) + if !root.add(nil, c1) { + t.Fatal("expected add to succeed") + } + // 2. Overwriting existing leaf value should fail + if root.add(nil, c2) { + t.Fatal("expected duplicate leaf add to fail") + } + // 3. Adding child path when node already has a leaf value should fail + if root.add([]staticInitPathElem{{index: 0}}, c2) { + t.Fatal("expected adding child to leaf to fail") + } + + // 4. Adding leaf to a node with children should fail + root2 := new(staticInitNode) + if !root2.add([]staticInitPathElem{{index: 0}}, c1) { + t.Fatal("expected child add to succeed") + } + if root2.add(nil, c2) { + t.Fatal("expected leaf add to branch node to fail") + } +} + +func TestStaticInitPathHelperEdgeCases(t *testing.T) { + // staticInitRootGlobal + if g := staticInitRootGlobal(nil); g != nil { + t.Fatalf("expected nil root global, got %v", g) + } + + // staticInitStorePath + if _, ok := staticInitStorePath(nil); ok { + t.Fatal("expected nil addr to fail store path") + } + + // staticInitConstIndex + if _, ok := staticInitConstIndex(nil); ok { + t.Fatal("expected nil index to fail") + } + negConst := &ssa.Const{Value: constant.MakeInt64(-1)} + if _, ok := staticInitConstIndex(negConst); ok { + t.Fatal("expected negative index to fail") + } + strConst := &ssa.Const{Value: constant.MakeString("not an int")} + if _, ok := staticInitConstIndex(strConst); ok { + t.Fatal("expected string index to fail") + } + validConst := &ssa.Const{Value: constant.MakeInt64(5)} + if idx, ok := staticInitConstIndex(validConst); !ok || idx != 5 { + t.Fatalf("expected index 5, got %d, %v", idx, ok) + } + + // staticInitStorePathToAlloc + targetAlloc := new(ssa.Alloc) + otherAlloc := new(ssa.Alloc) + if _, ok := staticInitStorePathToAlloc(nil, targetAlloc); ok { + t.Fatal("expected nil to fail") + } + if _, ok := staticInitStorePathToAlloc(otherAlloc, targetAlloc); ok { + t.Fatal("expected different alloc to fail") + } + if path, ok := staticInitStorePathToAlloc(targetAlloc, targetAlloc); !ok || len(path) != 0 { + t.Fatalf("expected empty path for matching alloc, got %v, %v", path, ok) + } +} + +func TestStaticInitZeroSizedPredicates(t *testing.T) { + // Zero-sized array + arrZero := types.NewArray(types.Typ[types.Int], 0) + if !staticInitZeroSized(arrZero) { + t.Fatal("expected [0]int to be zero sized") + } + + // Non-zero sized array + arrNonZero := types.NewArray(types.Typ[types.Int], 5) + if staticInitZeroSized(arrNonZero) { + t.Fatal("expected [5]int to not be zero sized") + } + + // Zero-sized struct (empty struct) + structEmpty := types.NewStruct(nil, nil) + if !staticInitZeroSized(structEmpty) { + t.Fatal("expected empty struct to be zero sized") + } + + // Array of zero-sized struct + arrEmptyStruct := types.NewArray(structEmpty, 10) + if !staticInitZeroSized(arrEmptyStruct) { + t.Fatal("expected [10]struct{} to be zero sized") + } + + // Non-zero sized struct + field := types.NewField(0, nil, "X", types.Typ[types.Int], false) + structNonEmpty := types.NewStruct([]*types.Var{field}, nil) + if staticInitZeroSized(structNonEmpty) { + t.Fatal("expected struct with int field to not be zero sized") + } +} + +func TestStaticInitChildrenInRange(t *testing.T) { + node := &staticInitNode{ + children: map[int]*staticInitNode{ + 0: {}, + 1: {}, + }, + } + if !staticInitChildrenInRange(node, 2) { + t.Fatal("expected 2 children in range 2 to return true") + } + if staticInitChildrenInRange(node, 1) { + t.Fatal("expected child at index 1 to be out of range 1") + } + negNode := &staticInitNode{ + children: map[int]*staticInitNode{-1: {}}, + } + if staticInitChildrenInRange(negNode, 5) { + t.Fatal("expected negative index child to fail") + } +} + +func TestStaticInitCycleDetection(t *testing.T) { + alloc := new(ssa.Alloc) + visited := map[*ssa.Alloc]bool{ + alloc: true, + } + var stores []staticInitStore + var instrs []ssa.Instruction + if collectAllocStores(alloc, nil, &stores, &instrs, visited) { + t.Fatal("expected visited alloc to be rejected") + } +} + +func TestStaticGlobalSliceWithBoundsRejects(t *testing.T) { + const src = `package staticinit +var backing = [4]int{1, 2, 3, 4} +var SliceSub = backing[1:3] +func Use() int { return SliceSub[0] } +` + ir := compileWithRewrites(t, src, nil) + assertStoreToGlobal(t, ir, "@staticinit.SliceSub") +} + +func TestStaticGlobalScalarNumericTypes(t *testing.T) { + const src = `package staticinit +type FloatStruct struct { + F32 float32 + F64 float64 + U8 uint8 + U64 uint64 + B bool +} +var GFloat = FloatStruct{ + F32: 1.5, + F64: 3.14159, + U8: 255, + U64: 18446744073709551615, + B: true, +} +func Use() float64 { return float64(GFloat.F32) + GFloat.F64 } +` + ir := compileWithRewrites(t, src, nil) + assertNoStoreToGlobal(t, ir, "@staticinit.GFloat") +} + diff --git a/cl/static_init.go b/cl/static_init.go index 7edd7fa942..2cc4c80bbe 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,32 @@ 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 { + 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 + } + 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 +198,151 @@ 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{} } } } +// collectAllocStores recursively traces store instructions made to a local stack alloc, +// recording constant stores into out and tracking intermediate instructions for suppression. +// The visited map guards against cyclic pointer graphs. +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 + } + unopRefs, ok := nonDebugReferrers(ref) + if !ok || len(unopRefs) != 1 { + 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 +} + +// appendStaticInitPath concatenates base and sub paths into a newly allocated slice +// to avoid slice-aliasing hazards when branching across multiple struct fields or array elements. +func appendStaticInitPath(base, sub []staticInitPathElem) []staticInitPathElem { + res := make([]staticInitPathElem, len(base)+len(sub)) + copy(res, base) + copy(res[len(base):], sub) + return res +} + +// handleStoreVal inspects a store value, appending constant stores directly or recursing +// into inner nested local allocs reached through pointer indirection (*ssa.UnOp). +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 { + unopRefs, ok := nonDebugReferrers(unop) + if !ok || len(unopRefs) != 1 || unopRefs[0] != store { + return false + } + return collectAllocStores(innerAlloc, fullPath, out, instrs, visited) + } + } + return false +} + +// staticInitStorePathToAlloc resolves the nested path elements from an address expression +// back to the root target alloc. +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 appendStaticInitPath(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 appendStaticInitPath(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 { @@ -313,7 +470,7 @@ func staticInitStorePath(addr ssa.Value) ([]staticInitPathElem, bool) { if !ok { return nil, false } - return append(path, staticInitPathElem{index: addr.Field}), true + return appendStaticInitPath(path, []staticInitPathElem{{index: addr.Field}}), true case *ssa.IndexAddr: path, ok := staticInitStorePath(addr.X) if !ok { @@ -323,7 +480,7 @@ func staticInitStorePath(addr ssa.Value) ([]staticInitPathElem, bool) { if !ok { return nil, false } - return append(path, staticInitPathElem{index: index}), true + return appendStaticInitPath(path, []staticInitPathElem{{index: index}}), true default: return nil, false } @@ -331,7 +488,7 @@ func staticInitStorePath(addr ssa.Value) ([]staticInitPathElem, bool) { func staticInitConstIndex(v ssa.Value) (int, bool) { c, ok := v.(*ssa.Const) - if !ok { + if !ok || c.Value == nil || c.Value.Kind() != constant.Int { return 0, false } index, exact := constant.Int64Val(c.Value) diff --git a/ssa/decl.go b/ssa/decl.go index 43d1a87afc..389e13e7d0 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -172,10 +172,20 @@ func (p Package) VarOf(name string) Global { // Init initializes the global variable with the given value. func (g Global) Init(v Expr) { + // Zero-sized globals alias the shared moduleZeroName sentinel, which already has + // a null initializer under LinkOnceODRLinkage and must not be mutated. + if g.impl.Name() == moduleZeroName { + return + } g.impl.SetInitializer(v.impl) } func (g Global) InitNil() { + // Zero-sized globals alias the shared moduleZeroName sentinel, which already has + // a null initializer under LinkOnceODRLinkage and must not be mutated. + if g.impl.Name() == moduleZeroName { + return + } g.impl.SetInitializer(llvm.ConstNull(g.impl.GlobalValueType())) } From f40308ed4c425d7626776157f3995fcceb813257 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 18:06:38 +0800 Subject: [PATCH 2/8] benchmark: use PR merge-base for baseline and add LTO workload measurements --- .github/workflows/benchmark.yml | 13 ++++++++++++- benchmark/baseline/README.md | 6 +++--- benchmark/baseline/main.go | 10 ++++++++-- benchmark/baseline/main_test.go | 1 + 4 files changed, 24 insertions(+), 6 deletions(-) diff --git a/.github/workflows/benchmark.yml b/.github/workflows/benchmark.yml index 68821b81fa..72b4573a1e 100644 --- a/.github/workflows/benchmark.yml +++ b/.github/workflows/benchmark.yml @@ -31,13 +31,24 @@ jobs: LLGO_ROOT: ${{ github.workspace }} steps: - uses: actions/checkout@v7 + with: + fetch-depth: 0 + + - name: Determine pull request merge-base + if: github.event_name == 'pull_request' + id: merge-base + run: | + git fetch https://github.com/${{ github.event.pull_request.base.repo.full_name }}.git ${{ github.event.pull_request.base.ref }} + base_sha=$(git merge-base FETCH_HEAD ${{ github.event.pull_request.head.sha }}) + echo "sha=$base_sha" >> "$GITHUB_OUTPUT" + echo "Computed pull request merge-base: $base_sha (head: ${{ github.event.pull_request.head.sha }})" - name: Check out pull request base benchmark source if: github.event_name == 'pull_request' uses: actions/checkout@v7 with: repository: ${{ github.event.pull_request.base.repo.full_name }} - ref: ${{ github.event.pull_request.base.sha }} + ref: ${{ steps.merge-base.outputs.sha }} path: .benchmark/source persist-credentials: false diff --git a/benchmark/baseline/README.md b/benchmark/baseline/README.md index 32073268cf..77c5163aab 100644 --- a/benchmark/baseline/README.md +++ b/benchmark/baseline/README.md @@ -10,9 +10,9 @@ commit, branch, or pull-request series. The program workloads reuse: -- `benchmark/binary_size/cprintf`: only `lib/c.Printf`; -- `benchmark/binary_size/println`: only the built-in `println`; -- `benchmark/binary_size/fmtprintf`: `fmt.Printf`. +- `benchmark/binary_size/cprintf`: only `lib/c.Printf` (default and `-lto=full`); +- `benchmark/binary_size/println`: only the built-in `println` (default and `-lto=full`); +- `benchmark/binary_size/fmtprintf`: `fmt.Printf` (default and `-lto=full`). For each workload, the collector performs an unmeasured warm build, then records median build time, median process time, file size, executable-code bytes, diff --git a/benchmark/baseline/main.go b/benchmark/baseline/main.go index f9e4cbe19b..805f0314f1 100644 --- a/benchmark/baseline/main.go +++ b/benchmark/baseline/main.go @@ -51,12 +51,16 @@ type workload struct { name string source string output string + flags []string } var workloads = []workload{ {name: "cprintf", source: "benchmark/binary_size/cprintf/main.go", output: "Hello, world\n"}, + {name: "cprintf_lto", source: "benchmark/binary_size/cprintf/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, {name: "println", source: "benchmark/binary_size/println/main.go", output: "Hello, world\n"}, + {name: "println_lto", source: "benchmark/binary_size/println/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, {name: "fmtprintf", source: "benchmark/binary_size/fmtprintf/main.go", output: "Hello, world\n"}, + {name: "fmtprintf_lto", source: "benchmark/binary_size/fmtprintf/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, } var expectedGoBenchmarks = []string{ @@ -223,15 +227,17 @@ func collect(ctx context.Context, root, llgo, out string, buildRuns, runRuns int var sizes, timings []metric for _, item := range workloads { binary := filepath.Join(binDir, item.name) + buildArgs := append([]string{"build"}, item.flags...) + buildArgs = append(buildArgs, "-o", binary, filepath.Join(root, item.source)) // Keep first-use toolchain and filesystem caches out of the measured // median so the first revision is not systematically disadvantaged. - if err := run(ctx, env, io.Discard, llgo, "build", "-o", binary, filepath.Join(root, item.source)); err != nil { + if err := run(ctx, env, io.Discard, llgo, buildArgs...); err != nil { return fmt.Errorf("warm build %s: %w", item.name, err) } buildDurations := make([]time.Duration, 0, buildRuns) for range buildRuns { start := time.Now() - if err := run(ctx, env, io.Discard, llgo, "build", "-o", binary, filepath.Join(root, item.source)); err != nil { + if err := run(ctx, env, io.Discard, llgo, buildArgs...); err != nil { return fmt.Errorf("build %s: %w", item.name, err) } buildDurations = append(buildDurations, time.Since(start)) diff --git a/benchmark/baseline/main_test.go b/benchmark/baseline/main_test.go index 2fe56a56e5..88d06d9f13 100644 --- a/benchmark/baseline/main_test.go +++ b/benchmark/baseline/main_test.go @@ -97,6 +97,7 @@ func TestExportBenchmarks(t *testing.T) { "Unit file-bytes better=lower assume=exact", "Unit build-ns better=lower", "BenchmarkProgram/cprintf 1 1 file-bytes 1 text-bytes 1 data-bytes 1 bss-bytes 1 build-ns 1 run-ns", + "BenchmarkProgram/cprintf_lto 1 1 file-bytes 1 text-bytes 1 data-bytes 1 bss-bytes 1 build-ns 1 run-ns", "BenchmarkRuntimeGetG-1 100 12.5 ns/op", } { if !strings.Contains(text, want) { From 89644a70c6f131bbe21dbdd9b0085ddbc8687de5 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 19:38:12 +0800 Subject: [PATCH 3/8] cl, ssa: validate load referrer in collectAllocStores, format files, and add unit tests --- cl/rewrite_internal_test.go | 65 +++++++++++++++++++++++++++++++++++++ cl/static_init.go | 4 +++ ssa/backend_program_test.go | 20 ++++++++++++ 3 files changed, 89 insertions(+) diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 7ca063740f..caf48d17e7 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -1769,3 +1769,68 @@ func Use() float64 { return float64(GFloat.F32) + GFloat.F64 } assertNoStoreToGlobal(t, ir, "@staticinit.GFloat") } +func TestStaticSliceInitOfEdgeCases(t *testing.T) { + // 1. Non-slice store value + intStore := &ssa.Store{ + Addr: new(ssa.Global), + Val: ssa.NewConst(constant.MakeInt64(1), types.Typ[types.Int]), + } + if _, ok := staticSliceInitOf(intStore); ok { + t.Fatal("expected non-slice store to fail staticSliceInitOf") + } + + // 2. Slice with Low / High / Max bounds + c0 := ssa.NewConst(constant.MakeInt64(0), types.Typ[types.Int]) + alloc := &ssa.Alloc{Comment: "arr"} + boundedSlice := &ssa.Slice{ + X: alloc, + Low: c0, + High: c0, + } + boundedStore := &ssa.Store{ + Addr: new(ssa.Global), + Val: boundedSlice, + } + if _, ok := staticSliceInitOf(boundedStore); ok { + t.Fatal("expected bounded slice to fail staticSliceInitOf") + } + + // 3. Slice whose X is not an Alloc + nonAllocSlice := &ssa.Slice{ + X: new(ssa.Global), + } + nonAllocStore := &ssa.Store{ + Addr: new(ssa.Global), + Val: nonAllocSlice, + } + if _, ok := staticSliceInitOf(nonAllocStore); ok { + t.Fatal("expected non-alloc slice source to fail staticSliceInitOf") + } +} + +func TestCollectAllocStoresBranchEdgeCases(t *testing.T) { + alloc := new(ssa.Alloc) + var stores []staticInitStore + var instrs []ssa.Instruction + + // 1. Cycle detection + visited := map[*ssa.Alloc]bool{alloc: true} + if collectAllocStores(alloc, nil, &stores, &instrs, visited) { + t.Fatal("expected visited alloc to fail collectAllocStores") + } + + // 2. handleStoreVal on UnOp with heap alloc + heapAlloc := &ssa.Alloc{Heap: true} + unopHeap := &ssa.UnOp{Op: token.MUL, X: heapAlloc} + storeHeap := &ssa.Store{Addr: alloc, Val: unopHeap} + if handleStoreVal(storeHeap, nil, &stores, &instrs, make(map[*ssa.Alloc]bool)) { + t.Fatal("expected heap alloc unop to fail handleStoreVal") + } + + // 3. handleStoreVal on non-MUL UnOp + unopNotMul := &ssa.UnOp{Op: token.NOT, X: alloc} + storeNotMul := &ssa.Store{Addr: alloc, Val: unopNotMul} + if handleStoreVal(storeNotMul, nil, &stores, &instrs, make(map[*ssa.Alloc]bool)) { + t.Fatal("expected non-MUL unop to fail handleStoreVal") + } +} diff --git a/cl/static_init.go b/cl/static_init.go index 2cc4c80bbe..85a3397e92 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -231,6 +231,10 @@ func collectAllocStores(alloc *ssa.Alloc, basePath []staticInitPathElem, out *[] if !ok || len(unopRefs) != 1 { return false } + elemStore, ok := unopRefs[0].(*ssa.Store) + if !ok || elemStore.Val != ref { + return false + } *instrs = append(*instrs, ref) case *ssa.FieldAddr: subPath, ok := staticInitStorePathToAlloc(ref, alloc) diff --git a/ssa/backend_program_test.go b/ssa/backend_program_test.go index 06df50fdc3..e4550b25a1 100644 --- a/ssa/backend_program_test.go +++ b/ssa/backend_program_test.go @@ -83,3 +83,23 @@ func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { t.Fatal("backend Program did not preserve coordinator configuration") } } + +func TestZeroSizedGlobalInitGuards(t *testing.T) { + prog := NewProgram(nil) + defer prog.Dispose() + pkg := prog.NewPackage("example.com/p", "p") + + // 1. Zero-sized global + zeroElem := types.NewArray(types.Typ[types.Int], 0) + zeroPtr := types.NewPointer(zeroElem) + zeroGlobal := pkg.NewVar("zeroVar", zeroPtr, InGo) + zeroGlobal.Init(pkg.Prog.Zero(pkg.Prog.Type(zeroElem, InGo))) + zeroGlobal.InitNil() + + // 2. Normal global + intElem := types.Typ[types.Int] + intPtr := types.NewPointer(intElem) + normalGlobal := pkg.NewVar("normalVar", intPtr, InGo) + normalGlobal.Init(pkg.Prog.IntVal(42, pkg.Prog.Type(intElem, InGo))) + normalGlobal.InitNil() +} From 321773d664a8dc3caed09b10de05feced6d836c9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 19:39:21 +0800 Subject: [PATCH 4/8] benchmark: use hyphen for LTO workload names to avoid markdown escaping --- benchmark/baseline/main.go | 6 +++--- benchmark/baseline/main_test.go | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/benchmark/baseline/main.go b/benchmark/baseline/main.go index 805f0314f1..94cbf4f01a 100644 --- a/benchmark/baseline/main.go +++ b/benchmark/baseline/main.go @@ -56,11 +56,11 @@ type workload struct { var workloads = []workload{ {name: "cprintf", source: "benchmark/binary_size/cprintf/main.go", output: "Hello, world\n"}, - {name: "cprintf_lto", source: "benchmark/binary_size/cprintf/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, + {name: "cprintf-lto", source: "benchmark/binary_size/cprintf/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, {name: "println", source: "benchmark/binary_size/println/main.go", output: "Hello, world\n"}, - {name: "println_lto", source: "benchmark/binary_size/println/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, + {name: "println-lto", source: "benchmark/binary_size/println/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, {name: "fmtprintf", source: "benchmark/binary_size/fmtprintf/main.go", output: "Hello, world\n"}, - {name: "fmtprintf_lto", source: "benchmark/binary_size/fmtprintf/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, + {name: "fmtprintf-lto", source: "benchmark/binary_size/fmtprintf/main.go", output: "Hello, world\n", flags: []string{"-lto=full"}}, } var expectedGoBenchmarks = []string{ diff --git a/benchmark/baseline/main_test.go b/benchmark/baseline/main_test.go index 88d06d9f13..7f060a0881 100644 --- a/benchmark/baseline/main_test.go +++ b/benchmark/baseline/main_test.go @@ -97,7 +97,7 @@ func TestExportBenchmarks(t *testing.T) { "Unit file-bytes better=lower assume=exact", "Unit build-ns better=lower", "BenchmarkProgram/cprintf 1 1 file-bytes 1 text-bytes 1 data-bytes 1 bss-bytes 1 build-ns 1 run-ns", - "BenchmarkProgram/cprintf_lto 1 1 file-bytes 1 text-bytes 1 data-bytes 1 bss-bytes 1 build-ns 1 run-ns", + "BenchmarkProgram/cprintf-lto 1 1 file-bytes 1 text-bytes 1 data-bytes 1 bss-bytes 1 build-ns 1 run-ns", "BenchmarkRuntimeGetG-1 100 12.5 ns/op", } { if !strings.Contains(text, want) { From 1ae16e029f53a4073d91874b57d1652bf3962022 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 20:05:10 +0800 Subject: [PATCH 5/8] ssa, cl: cover sentinel and alias init paths, and clarify store tracking comment --- cl/static_init.go | 5 +++-- ssa/backend_program_test.go | 15 +++++++++++---- ssa/decl.go | 4 ++-- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/cl/static_init.go b/cl/static_init.go index 85a3397e92..aae878f4fe 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -294,8 +294,9 @@ func appendStaticInitPath(base, sub []staticInitPathElem) []staticInitPathElem { return res } -// handleStoreVal inspects a store value, appending constant stores directly or recursing -// into inner nested local allocs reached through pointer indirection (*ssa.UnOp). +// handleStoreVal inspects a store value, appending constant stores directly to out (while the caller +// tracks the store instruction in instrs for compilation suppression) or recursing into inner nested +// local allocs reached through pointer indirection (*ssa.UnOp). 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{ diff --git a/ssa/backend_program_test.go b/ssa/backend_program_test.go index e4550b25a1..7fbe629b62 100644 --- a/ssa/backend_program_test.go +++ b/ssa/backend_program_test.go @@ -87,19 +87,26 @@ func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { func TestZeroSizedGlobalInitGuards(t *testing.T) { prog := NewProgram(nil) defer prog.Dispose() + rt := types.NewPackage("github.com/xgo-dev/llgo/runtime/internal/runtime", "runtime") + prog.SetRuntime(func() *types.Package { return rt }) pkg := prog.NewPackage("example.com/p", "p") - // 1. Zero-sized global + // 1. Zero-sized global alias zeroElem := types.NewArray(types.Typ[types.Int], 0) zeroPtr := types.NewPointer(zeroElem) - zeroGlobal := pkg.NewVar("zeroVar", zeroPtr, InGo) + zeroGlobal := pkg.NewVar("example.com/p.zeroVar", zeroPtr, InGo) zeroGlobal.Init(pkg.Prog.Zero(pkg.Prog.Type(zeroElem, InGo))) zeroGlobal.InitNil() - // 2. Normal global + // 2. Direct moduleZeroName sentinel global + sentinel := &aGlobal{pkg.moduleZeroSizedAlloc(pkg.Prog.Type(zeroElem, InGo))} + sentinel.Init(pkg.Prog.Zero(pkg.Prog.Type(zeroElem, InGo))) + sentinel.InitNil() + + // 3. Normal global intElem := types.Typ[types.Int] intPtr := types.NewPointer(intElem) - normalGlobal := pkg.NewVar("normalVar", intPtr, InGo) + normalGlobal := pkg.NewVar("example.com/p.normalVar", intPtr, InGo) normalGlobal.Init(pkg.Prog.IntVal(42, pkg.Prog.Type(intElem, InGo))) normalGlobal.InitNil() } diff --git a/ssa/decl.go b/ssa/decl.go index 389e13e7d0..2595673d73 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -174,7 +174,7 @@ func (p Package) VarOf(name string) Global { func (g Global) Init(v Expr) { // Zero-sized globals alias the shared moduleZeroName sentinel, which already has // a null initializer under LinkOnceODRLinkage and must not be mutated. - if g.impl.Name() == moduleZeroName { + if g.impl.Name() == moduleZeroName || !g.impl.IsAGlobalAlias().IsNil() { return } g.impl.SetInitializer(v.impl) @@ -183,7 +183,7 @@ func (g Global) Init(v Expr) { func (g Global) InitNil() { // Zero-sized globals alias the shared moduleZeroName sentinel, which already has // a null initializer under LinkOnceODRLinkage and must not be mutated. - if g.impl.Name() == moduleZeroName { + if g.impl.Name() == moduleZeroName || !g.impl.IsAGlobalAlias().IsNil() { return } g.impl.SetInitializer(llvm.ConstNull(g.impl.GlobalValueType())) From f3de083d0424164b086a5d0ce6d3d4b03397a51f Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 20:30:29 +0800 Subject: [PATCH 6/8] cl, ssa: bind static alloc folding to its destination --- cl/rewrite_internal_test.go | 297 ++++++++++++++++++++++++++++++++++-- cl/static_init.go | 69 +++------ ssa/backend_program_test.go | 27 ---- ssa/decl.go | 10 +- ssa/ssa_test.go | 4 +- 5 files changed, 318 insertions(+), 89 deletions(-) diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index caf48d17e7..97d5263ccf 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -713,6 +713,82 @@ var G int }, initFn.Blocks[0].Instrs...) ctx = &context{prog: ssatest.NewProgram(t, nil)} ctx.collectStaticGlobalInits(nonGlobalStore) + + allocBacked := buildSSAPackage(t, `package foo +var G = 1 +`) + allocGlobal, ok := allocBacked.Members["G"].(*ssa.Global) + if !ok { + t.Fatalf("missing G global: %T", allocBacked.Members["G"]) + } + var allocGlobalStore *ssa.Store + for _, block := range allocBacked.Func("init").Blocks { + for _, instr := range block.Instrs { + if store, ok := instr.(*ssa.Store); ok && store.Addr == allocGlobal { + allocGlobalStore = store + } + } + } + if allocGlobalStore == nil { + t.Fatal("missing store to alloc-backed G") + } + value, ok := allocGlobalStore.Val.(*ssa.Const) + if !ok { + t.Fatalf("G initializer = %T, want *ssa.Const", allocGlobalStore.Val) + } + alloc := new(ssa.Alloc) + allocStore := &ssa.Store{Addr: alloc, Val: value} + resultLoad := &ssa.UnOp{Op: token.MUL, X: alloc} + allocGlobalStore.Val = resultLoad + allocRefs := alloc.Referrers() + loadRefs := resultLoad.Referrers() + if allocRefs == nil || loadRefs == nil { + t.Fatal("expected alloc-backed initializer values to track referrers") + } + *allocRefs = []ssa.Instruction{allocStore, resultLoad} + *loadRefs = []ssa.Instruction{allocGlobalStore} + loweringProg := ssatest.NewProgram(t, nil) + ctx = &context{prog: loweringProg, pkg: loweringProg.NewPackage("foo", "foo")} + ctx.collectStaticGlobalInits(allocBacked) + if _, ok := ctx.staticGlobalInits[allocGlobal]; !ok { + t.Fatal("alloc-backed constant initializer was not folded") + } + for _, instr := range []ssa.Instruction{alloc, allocStore, resultLoad, allocGlobalStore} { + if _, ok := ctx.staticInitInstrs[instr]; !ok { + t.Fatalf("alloc-backed initializer did not suppress %T", instr) + } + } + if _, ok := ctx.staticInitStores[allocStore]; !ok { + t.Fatal("alloc-backed initializer store was not recorded") + } + + invalidPath := buildSSAPackage(t, `package foo +var G = 1 +`) + global, ok := invalidPath.Members["G"].(*ssa.Global) + if !ok { + t.Fatalf("missing G global: %T", invalidPath.Members["G"]) + } + var globalStore *ssa.Store + for _, block := range invalidPath.Func("init").Blocks { + for _, instr := range block.Instrs { + if store, ok := instr.(*ssa.Store); ok && store.Addr == global { + globalStore = store + } + } + } + if globalStore == nil { + t.Fatal("missing store to G") + } + // Keep a recognizable root global but make its index non-constant. A malformed + // or future SSA address shape must make the whole candidate fall back, rather + // than treating the failed path as a store to the root scalar. + globalStore.Addr = &ssa.IndexAddr{X: global, Index: global} + ctx = &context{prog: ssatest.NewProgram(t, nil)} + ctx.collectStaticGlobalInits(invalidPath) + if ctx.staticGlobalInits != nil { + t.Fatalf("unsupported store path produced static initializers: %+v", ctx.staticGlobalInits) + } } func TestStaticInitHelperRejectsNestedUnsupportedPaths(t *testing.T) { @@ -1491,21 +1567,13 @@ func next() int { return 1 } 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) { + if collectAllocStores(targetAlloc, nil, nil, nil, &stores, &instrs, visited) { t.Fatal("expected cycle protection to return false") } @@ -1601,6 +1669,25 @@ func Use() int { } } +func TestStaticGlobalZeroCompositeLiteralInit(t *testing.T) { + const src = `package staticinit + +type Zero struct { + Value int + Array [2]int +} + +var G = Zero{} + +func Use() int { return G.Value + G.Array[1] } +` + ir := compileWithRewrites(t, src, nil) + if !strings.Contains(ir, "@staticinit.G = global %staticinit.Zero zeroinitializer") { + t.Fatalf("missing zero static initializer:\n%s", ir) + } + assertNoStoreToGlobal(t, ir, "@staticinit.G") +} + func TestStaticInitNodeAddEdgeCases(t *testing.T) { c1 := &ssa.Const{Value: constant.MakeInt64(1)} c2 := &ssa.Const{Value: constant.MakeInt64(2)} @@ -1732,7 +1819,7 @@ func TestStaticInitCycleDetection(t *testing.T) { } var stores []staticInitStore var instrs []ssa.Instruction - if collectAllocStores(alloc, nil, &stores, &instrs, visited) { + if collectAllocStores(alloc, nil, nil, nil, &stores, &instrs, visited) { t.Fatal("expected visited alloc to be rejected") } } @@ -1815,7 +1902,7 @@ func TestCollectAllocStoresBranchEdgeCases(t *testing.T) { // 1. Cycle detection visited := map[*ssa.Alloc]bool{alloc: true} - if collectAllocStores(alloc, nil, &stores, &instrs, visited) { + if collectAllocStores(alloc, nil, nil, nil, &stores, &instrs, visited) { t.Fatal("expected visited alloc to fail collectAllocStores") } @@ -1834,3 +1921,191 @@ func TestCollectAllocStoresBranchEdgeCases(t *testing.T) { t.Fatal("expected non-MUL unop to fail handleStoreVal") } } + +func TestCollectAllocStoresNestedPaths(t *testing.T) { + setRefs := func(value ssa.Value, refs ...ssa.Instruction) { + t.Helper() + referrers := value.Referrers() + if referrers == nil { + t.Fatalf("%T does not track referrers", value) + } + *referrers = refs + } + + leafValue := ssa.NewConst(constant.MakeInt64(42), types.Typ[types.Int]) + outerAlloc := new(ssa.Alloc) + innerAlloc := new(ssa.Alloc) + outerField := &ssa.FieldAddr{X: outerAlloc, Field: 1} + innerIndex := &ssa.IndexAddr{ + X: innerAlloc, + Index: ssa.NewConst(constant.MakeInt64(0), types.Typ[types.Int]), + } + leafStore := &ssa.Store{Addr: innerIndex, Val: leafValue} + innerLoad := &ssa.UnOp{Op: token.MUL, X: innerAlloc} + nestedStore := &ssa.Store{Addr: outerField, Val: innerLoad} + resultLoad := &ssa.UnOp{Op: token.MUL, X: outerAlloc} + resultStore := &ssa.Store{Val: resultLoad} + + setRefs(outerAlloc, outerField, resultLoad) + setRefs(outerField, nestedStore) + setRefs(innerAlloc, innerIndex, innerLoad) + setRefs(innerIndex, leafStore) + setRefs(innerLoad, nestedStore) + setRefs(resultLoad, resultStore) + + var stores []staticInitStore + var instrs []ssa.Instruction + if !collectAllocStores( + outerAlloc, resultLoad, resultStore, []staticInitPathElem{{index: 3}}, + &stores, &instrs, make(map[*ssa.Alloc]bool), + ) { + t.Fatal("rejected a fully owned nested aggregate") + } + if len(stores) != 1 || stores[0].store != leafStore || stores[0].value != leafValue { + t.Fatalf("unexpected collected stores: %+v", stores) + } + wantPath := []int{3, 1, 0} + if len(stores[0].path) != len(wantPath) { + t.Fatalf("collected path = %+v, want %v", stores[0].path, wantPath) + } + for i, want := range wantPath { + if stores[0].path[i].index != want { + t.Fatalf("collected path = %+v, want %v", stores[0].path, wantPath) + } + } + for _, want := range []ssa.Instruction{ + outerAlloc, outerField, nestedStore, innerAlloc, innerIndex, leafStore, innerLoad, resultLoad, + } { + found := false + for _, instr := range instrs { + found = found || instr == want + } + if !found { + t.Fatalf("missing suppressed instruction %T", want) + } + } +} + +func TestCollectAllocStoresRejectsUnrelatedLoadConsumer(t *testing.T) { + alloc := new(ssa.Alloc) + resultLoad := &ssa.UnOp{Op: token.MUL, X: alloc} + resultStore := &ssa.Store{Val: resultLoad} + unrelatedStore := &ssa.Store{Val: resultLoad} + allocRefs := alloc.Referrers() + loadRefs := resultLoad.Referrers() + if allocRefs == nil || loadRefs == nil { + t.Fatal("expected alloc and load to track referrers") + } + *allocRefs = []ssa.Instruction{resultLoad} + *loadRefs = []ssa.Instruction{unrelatedStore} + + var stores []staticInitStore + var instrs []ssa.Instruction + if collectAllocStores( + alloc, resultLoad, resultStore, nil, + &stores, &instrs, make(map[*ssa.Alloc]bool), + ) { + t.Fatal("accepted a result load consumed by a store outside the fold") + } +} + +func TestCollectAllocStoresRejectsUnsafeReferrers(t *testing.T) { + setRefs := func(t *testing.T, value ssa.Value, refs ...ssa.Instruction) { + t.Helper() + referrers := value.Referrers() + if referrers == nil { + t.Fatalf("%T does not track referrers", value) + } + *referrers = refs + } + constValue := func() *ssa.Const { + return ssa.NewConst(constant.MakeInt64(1), types.Typ[types.Int]) + } + constIndex := func() *ssa.Const { + return ssa.NewConst(constant.MakeInt64(0), types.Typ[types.Int]) + } + + tests := []struct { + name string + bad func(*testing.T, *ssa.Alloc) ssa.Instruction + }{ + { + name: "extra load", + bad: func(t *testing.T, alloc *ssa.Alloc) ssa.Instruction { + load := &ssa.UnOp{Op: token.MUL, X: alloc} + setRefs(t, load, &ssa.Store{Val: load}) + return load + }, + }, + { + name: "field with multiple consumers", + bad: func(t *testing.T, alloc *ssa.Alloc) ssa.Instruction { + field := &ssa.FieldAddr{X: alloc} + setRefs(t, field, + &ssa.Store{Addr: field, Val: constValue()}, + &ssa.Store{Addr: field, Val: constValue()}, + ) + return field + }, + }, + { + name: "field with dynamic value", + bad: func(t *testing.T, alloc *ssa.Alloc) ssa.Instruction { + field := &ssa.FieldAddr{X: alloc} + setRefs(t, field, &ssa.Store{Addr: field, Val: field}) + return field + }, + }, + { + name: "index with dynamic subscript", + bad: func(t *testing.T, alloc *ssa.Alloc) ssa.Instruction { + return &ssa.IndexAddr{X: alloc, Index: alloc} + }, + }, + { + name: "index with foreign consumer", + bad: func(t *testing.T, alloc *ssa.Alloc) ssa.Instruction { + index := &ssa.IndexAddr{X: alloc, Index: constIndex()} + setRefs(t, index, &ssa.Store{Addr: new(ssa.Alloc), Val: constValue()}) + return index + }, + }, + { + name: "direct store to foreign address", + bad: func(_ *testing.T, _ *ssa.Alloc) ssa.Instruction { + return &ssa.Store{Addr: new(ssa.Alloc), Val: constValue()} + }, + }, + { + name: "direct store with dynamic value", + bad: func(_ *testing.T, alloc *ssa.Alloc) ssa.Instruction { + return &ssa.Store{Addr: alloc, Val: alloc} + }, + }, + { + name: "unsupported referrer", + bad: func(_ *testing.T, _ *ssa.Alloc) ssa.Instruction { + return new(ssa.Call) + }, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + alloc := new(ssa.Alloc) + resultLoad := &ssa.UnOp{Op: token.MUL, X: alloc} + resultStore := &ssa.Store{Val: resultLoad} + setRefs(t, resultLoad, resultStore) + setRefs(t, alloc, test.bad(t, alloc), resultLoad) + + var stores []staticInitStore + var instrs []ssa.Instruction + if collectAllocStores( + alloc, resultLoad, resultStore, nil, + &stores, &instrs, make(map[*ssa.Alloc]bool), + ) { + t.Fatal("accepted an unsafe alloc referrer") + } + }) + } +} diff --git a/cl/static_init.go b/cl/static_init.go index aae878f4fe..6eaacbc2ff 100644 --- a/cl/static_init.go +++ b/cl/static_init.go @@ -125,7 +125,11 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { candidate := candidateOf(global) path, ok := staticInitStorePath(store.Addr) - if ok && len(path) == 0 { + if !ok { + candidate.invalid = true + continue + } + if len(path) == 0 { if slice, ok := staticSliceInitOf(store); ok { if candidate.slice != nil || len(candidate.stores) != 0 { candidate.invalid = true @@ -143,16 +147,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 { + if !collectAllocStores(alloc, unop, store, path, &candidate.stores, &candidate.instrs, make(map[*ssa.Alloc]bool)) { candidate.invalid = true continue } - if !collectAllocStores(alloc, path, &candidate.stores, &candidate.instrs, make(map[*ssa.Alloc]bool)) { - candidate.invalid = true - continue - } - candidate.instrs = append(candidate.instrs, unop, store) + candidate.instrs = append(candidate.instrs, store) } else { candidate.invalid = true continue @@ -209,8 +208,10 @@ func (p *context) collectStaticGlobalInits(pkg *ssa.Package) { // collectAllocStores recursively traces store instructions made to a local stack alloc, // recording constant stores into out and tracking intermediate instructions for suppression. -// The visited map guards against cyclic pointer graphs. -func collectAllocStores(alloc *ssa.Alloc, basePath []staticInitPathElem, out *[]staticInitStore, instrs *[]ssa.Instruction, visited map[*ssa.Alloc]bool) bool { +// resultLoad and resultStore identify the one load that transfers the completed aggregate to +// its destination; rejecting any other load keeps unrelated consumers executable. The visited +// map guards against cyclic pointer graphs. +func collectAllocStores(alloc *ssa.Alloc, resultLoad *ssa.UnOp, resultStore *ssa.Store, basePath []staticInitPathElem, out *[]staticInitStore, instrs *[]ssa.Instruction, visited map[*ssa.Alloc]bool) bool { if visited[alloc] { return false } @@ -221,49 +222,31 @@ func collectAllocStores(alloc *ssa.Alloc, basePath []staticInitPathElem, out *[] if !ok { return false } + seenResultLoad := false for _, ref := range refs { switch ref := ref.(type) { case *ssa.UnOp: - if ref.Op != token.MUL { + if ref != resultLoad || seenResultLoad || ref.Op != token.MUL { return false } unopRefs, ok := nonDebugReferrers(ref) - if !ok || len(unopRefs) != 1 { - return false - } - elemStore, ok := unopRefs[0].(*ssa.Store) - if !ok || elemStore.Val != ref { + if !ok || len(unopRefs) != 1 || resultStore == nil || unopRefs[0] != resultStore || resultStore.Val != ref { return false } + seenResultLoad = true *instrs = append(*instrs, ref) - case *ssa.FieldAddr: - subPath, ok := staticInitStorePathToAlloc(ref, alloc) + case *ssa.FieldAddr, *ssa.IndexAddr: + addr := ref.(ssa.Value) + subPath, ok := staticInitStorePathToAlloc(addr, alloc) if !ok { return false } - fieldRefs, ok := nonDebugReferrers(ref) - if !ok || len(fieldRefs) != 1 { + addrRefs, ok := nonDebugReferrers(addr) + if !ok || len(addrRefs) != 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 { + elemStore, ok := addrRefs[0].(*ssa.Store) + if !ok || elemStore.Addr != addr { return false } if !handleStoreVal(elemStore, appendStaticInitPath(basePath, subPath), out, instrs, visited) { @@ -282,7 +265,7 @@ func collectAllocStores(alloc *ssa.Alloc, basePath []staticInitPathElem, out *[] return false } } - return true + return seenResultLoad } // appendStaticInitPath concatenates base and sub paths into a newly allocated slice @@ -308,11 +291,7 @@ 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) + return collectAllocStores(innerAlloc, unop, store, fullPath, out, instrs, visited) } } return false diff --git a/ssa/backend_program_test.go b/ssa/backend_program_test.go index 7fbe629b62..06df50fdc3 100644 --- a/ssa/backend_program_test.go +++ b/ssa/backend_program_test.go @@ -83,30 +83,3 @@ func TestNewBackendProgramSharesPreparedGoState(t *testing.T) { t.Fatal("backend Program did not preserve coordinator configuration") } } - -func TestZeroSizedGlobalInitGuards(t *testing.T) { - prog := NewProgram(nil) - defer prog.Dispose() - rt := types.NewPackage("github.com/xgo-dev/llgo/runtime/internal/runtime", "runtime") - prog.SetRuntime(func() *types.Package { return rt }) - pkg := prog.NewPackage("example.com/p", "p") - - // 1. Zero-sized global alias - zeroElem := types.NewArray(types.Typ[types.Int], 0) - zeroPtr := types.NewPointer(zeroElem) - zeroGlobal := pkg.NewVar("example.com/p.zeroVar", zeroPtr, InGo) - zeroGlobal.Init(pkg.Prog.Zero(pkg.Prog.Type(zeroElem, InGo))) - zeroGlobal.InitNil() - - // 2. Direct moduleZeroName sentinel global - sentinel := &aGlobal{pkg.moduleZeroSizedAlloc(pkg.Prog.Type(zeroElem, InGo))} - sentinel.Init(pkg.Prog.Zero(pkg.Prog.Type(zeroElem, InGo))) - sentinel.InitNil() - - // 3. Normal global - intElem := types.Typ[types.Int] - intPtr := types.NewPointer(intElem) - normalGlobal := pkg.NewVar("example.com/p.normalVar", intPtr, InGo) - normalGlobal.Init(pkg.Prog.IntVal(42, pkg.Prog.Type(intElem, InGo))) - normalGlobal.InitNil() -} diff --git a/ssa/decl.go b/ssa/decl.go index 2595673d73..e95868693a 100644 --- a/ssa/decl.go +++ b/ssa/decl.go @@ -69,7 +69,7 @@ func (p Package) NewConst(name string, val constant.Value) NamedConst { type aGlobal struct { Expr - //array bool + isZeroSizedAlias bool } // A Global is a named Value holding the address of a package-level @@ -150,7 +150,7 @@ func (p Package) doNewVarEx(name string, t Type, threadLocal bool) Global { // The returned Global intentionally points at the shared // sentinel; the alias above preserves this package variable's // symbol for external references. - ret := &aGlobal{zero} + ret := &aGlobal{Expr: zero, isZeroSizedAlias: true} p.vars[name] = ret return ret } @@ -160,7 +160,7 @@ func (p Package) doNewVarEx(name string, t Type, threadLocal bool) Global { gbl.SetThreadLocal(threadLocal) alignment := p.Prog.td.ABITypeAlignment(typ) gbl.SetAlignment(alignment) - ret := &aGlobal{Expr{gbl, t}} + ret := &aGlobal{Expr: Expr{gbl, t}} p.vars[name] = ret return ret } @@ -174,7 +174,7 @@ func (p Package) VarOf(name string) Global { func (g Global) Init(v Expr) { // Zero-sized globals alias the shared moduleZeroName sentinel, which already has // a null initializer under LinkOnceODRLinkage and must not be mutated. - if g.impl.Name() == moduleZeroName || !g.impl.IsAGlobalAlias().IsNil() { + if g.isZeroSizedAlias { return } g.impl.SetInitializer(v.impl) @@ -183,7 +183,7 @@ func (g Global) Init(v Expr) { func (g Global) InitNil() { // Zero-sized globals alias the shared moduleZeroName sentinel, which already has // a null initializer under LinkOnceODRLinkage and must not be mutated. - if g.impl.Name() == moduleZeroName || !g.impl.IsAGlobalAlias().IsNil() { + if g.isZeroSizedAlias { return } g.impl.SetInitializer(llvm.ConstNull(g.impl.GlobalValueType())) diff --git a/ssa/ssa_test.go b/ssa/ssa_test.go index 13cff55a5f..450bba4f60 100644 --- a/ssa/ssa_test.go +++ b/ssa/ssa_test.go @@ -2559,8 +2559,10 @@ func TestZeroSizedGlobalEmitsAliasSymbol(t *testing.T) { return pkg }) pkg := prog.NewPackage("bar", "foo/bar") - typ := types.NewPointer(types.NewArray(types.Typ[types.Int], 0)) + elem := types.NewArray(types.Typ[types.Int], 0) + typ := types.NewPointer(elem) a := pkg.NewVar("foo/bar.a", typ, InGo) + a.Init(prog.Zero(prog.Type(elem, InGo))) a.InitNil() pkg.NewVar("other/pkg.a", typ, InGo) assertPkg(t, pkg, `; ModuleID = 'foo/bar' From 39e17b710aedaced0c4e72862fb309dbce193ac6 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 21:10:41 +0800 Subject: [PATCH 7/8] cl: cover static alloc fallback paths --- cl/rewrite_internal_test.go | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index 97d5263ccf..cf4b139c06 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -762,6 +762,19 @@ var G = 1 t.Fatal("alloc-backed initializer store was not recorded") } + // A second executable load makes ownership of the temporary alloc + // ambiguous. Verify the package-level collector preserves the dynamic + // initializer instead of only testing the helper's rejection in isolation. + *allocRefs = append(*allocRefs, &ssa.UnOp{Op: token.MUL, X: alloc}) + fallbackCtx := &context{prog: loweringProg, pkg: loweringProg.NewPackage("foo-fallback", "foo-fallback")} + fallbackCtx.collectStaticGlobalInits(allocBacked) + if fallbackCtx.staticGlobalInits != nil { + t.Fatalf("alloc with an extra load produced static initializers: %+v", fallbackCtx.staticGlobalInits) + } + if fallbackCtx.staticInitInstrs != nil || fallbackCtx.staticInitStores != nil { + t.Fatal("rejected alloc-backed initializer suppressed executable instructions") + } + invalidPath := buildSSAPackage(t, `package foo var G = 1 `) @@ -802,6 +815,12 @@ func TestStaticInitHelperRejectsNestedUnsupportedPaths(t *testing.T) { }); ok { t.Fatal("index path with unsupported base should be rejected") } + + target := new(ssa.Alloc) + foreign := new(ssa.Alloc) + if _, ok := staticInitStorePathToAlloc(&ssa.FieldAddr{X: foreign, Field: 0}, target); ok { + t.Fatal("field path rooted at a different alloc should be rejected") + } } func TestStaticInitHelperBuildAdditionalFailures(t *testing.T) { From f9fc847450c4fabf45c29276122981700a865ca9 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 20 Aug 2026 21:43:31 +0800 Subject: [PATCH 8/8] cl: cover foreign alloc index paths --- cl/rewrite_internal_test.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cl/rewrite_internal_test.go b/cl/rewrite_internal_test.go index cf4b139c06..a7d6ac7f0c 100644 --- a/cl/rewrite_internal_test.go +++ b/cl/rewrite_internal_test.go @@ -821,6 +821,12 @@ func TestStaticInitHelperRejectsNestedUnsupportedPaths(t *testing.T) { if _, ok := staticInitStorePathToAlloc(&ssa.FieldAddr{X: foreign, Field: 0}, target); ok { t.Fatal("field path rooted at a different alloc should be rejected") } + if _, ok := staticInitStorePathToAlloc(&ssa.IndexAddr{ + X: foreign, + Index: ssa.NewConst(constant.MakeInt64(0), types.Typ[types.Int]), + }, target); ok { + t.Fatal("index path rooted at a different alloc should be rejected") + } } func TestStaticInitHelperBuildAdditionalFailures(t *testing.T) {