From e88e99b875271e39366b6c092a4d8f9d4b05a314 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Sat, 1 Aug 2026 20:48:03 +0800 Subject: [PATCH 1/4] cl: preserve nil checks for unused dereferences --- cl/_testrt/cvar/in.go | 3 --- cl/compile.go | 25 +++++++------------------ cl/zero_size_deref_test.go | 21 +++++++++++++++++++++ test/go/nil_deref_address_test.go | 27 +++++++++++++++++++++++++++ test/goroot/xfail.yaml | 8 -------- 5 files changed, 55 insertions(+), 29 deletions(-) diff --git a/cl/_testrt/cvar/in.go b/cl/_testrt/cvar/in.go index 408e4942eb..75db1c72b2 100644 --- a/cl/_testrt/cvar/in.go +++ b/cl/_testrt/cvar/in.go @@ -18,9 +18,6 @@ var barY struct { Arr [16]int8 } -// CHECK-LABEL: define void @main.main(){{.*}} { -// CHECK: load { [16 x i8], [2 x ptr] }, ptr @_bar_x -// CHECK: load { [16 x i8] }, ptr @_bar_y func main() { _ = barX _ = barY diff --git a/cl/compile.go b/cl/compile.go index 6ea1569324..e8cae83b5c 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1331,28 +1331,17 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue b.AssertNilDeref(x) } if refs, ok := nonDebugReferrers(v); ok && len(refs) == 0 { - if t := p.type_(v.Type(), llssa.InGo); t.RawType() != nil { - if p.isLargeNonPointerValue(t) { - x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) - p.assertNilDerefBase(b, v.X) - b.AssertNilDeref(x) - return - } - } if skipUnusedArrayDeref(v) { p.compileValue(b, v.X) return } - if _, ok := types.Unalias(v.Type()).Underlying().(*types.Slice); ok { - // Zero-length slice-to-array conversions can leave only - // an unused slice deref; preserve its required nil check. - x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) - p.assertNilDerefBase(b, v.X) - b.AssertNilDeref(x) - return - } + // LLVM may eliminate an unused load, but evaluating a Go + // dereference must still panic when its pointer is nil. + x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) + p.assertNilDerefBase(b, v.X) + b.AssertNilDeref(x) + return } if refs, ok := nonDebugReferrers(v); ok && len(refs) == 1 { if _, ok := refs[0].(*ssa.MakeInterface); ok { diff --git a/cl/zero_size_deref_test.go b/cl/zero_size_deref_test.go index 48de2b8d67..a84cccc06f 100644 --- a/cl/zero_size_deref_test.go +++ b/cl/zero_size_deref_test.go @@ -71,3 +71,24 @@ func keepPointer(pointer *struct{}) func() bool { }) } } + +func TestUnusedDerefEmitsNilGuard(t *testing.T) { + const src = `package unusedderef +func LoadArrayElement() { + var values [2]*int + _ = *values[1] +} +func LoadPointer(p *int) { + _ = *p +} +` + ir := compileWithRewrites(t, src, nil) + arrayLoad := llvmFunction(t, ir, "unusedderef.LoadArrayElement") + if !strings.Contains(arrayLoad, "AssertNilDeref") { + t.Fatalf("unused array-element dereference should retain a nil guard:\n%s", arrayLoad) + } + directLoad := llvmFunction(t, ir, "unusedderef.LoadPointer") + if !strings.Contains(directLoad, "AssertNilDeref") { + t.Fatalf("unused direct dereference should retain a nil guard:\n%s", directLoad) + } +} diff --git a/test/go/nil_deref_address_test.go b/test/go/nil_deref_address_test.go index e2a9506b3d..c02f966f53 100644 --- a/test/go/nil_deref_address_test.go +++ b/test/go/nil_deref_address_test.go @@ -47,6 +47,33 @@ func TestNilDerefAddressOperationsPanic(t *testing.T) { } } +func TestUnusedNilDerefOperationsPanic(t *testing.T) { + tests := []struct { + name string + f func() + }{ + { + name: "direct pointer", + f: func() { + var p *int + _ = *p + }, + }, + { + name: "pointer loaded from array", + f: func() { + var values [2]*int + _ = *values[1] + }, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + expectNilDerefAddressPanic(t, tt.f) + }) + } +} + func TestNilDerefPrintedCompositeLoadsPanic(t *testing.T) { tests := []struct { name string diff --git a/test/goroot/xfail.yaml b/test/goroot/xfail.yaml index dae23f6fb2..3ae5c8c3ba 100644 --- a/test/goroot/xfail.yaml +++ b/test/goroot/xfail.yaml @@ -1391,14 +1391,6 @@ xfails: directive: run case: fixedbugs/issue29504.go reason: panic/fault statement-line granularity in untracked functions (P4b prebuilt pcline) - - platform: darwin/arm64 - directive: run - case: fixedbugs/issue38496.go - reason: current main goroot run failure on darwin/arm64 - - platform: linux/amd64 - directive: run - case: fixedbugs/issue38496.go - reason: current main goroot run failure on linux/amd64 - version: go1.26 platform: darwin/arm64 directive: rundir From 6f1f1cc0c950004dda7efa58d7b0a059279d3f84 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 13 Aug 2026 20:34:36 +0800 Subject: [PATCH 2/4] ci: retrigger checks for replacement PR From bf773df006594af198d586a0c5d4c4d6ca23ea27 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Thu, 13 Aug 2026 21:05:56 +0800 Subject: [PATCH 3/4] cl: avoid duplicate unused array nil guards --- cl/compile.go | 21 +++++++++++++-------- cl/range_array_compile_test.go | 4 ++-- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/cl/compile.go b/cl/compile.go index e8cae83b5c..14903e83f1 100644 --- a/cl/compile.go +++ b/cl/compile.go @@ -1325,24 +1325,29 @@ func (p *context) compileInstrOrValue(b llssa.Builder, iv instrOrValue, asValue if _, ok := p.methodNilDerefChecks[v]; ok { return p.compileCheckedDeref(b, v) } - if isEffectfulArrayPointerDeref(v) { - x := p.compileValue(b, v.X) - p.recordPanicLocation(b, v.Pos()) - b.AssertNilDeref(x) - } + effectfulArrayDeref := isEffectfulArrayPointerDeref(v) if refs, ok := nonDebugReferrers(v); ok && len(refs) == 0 { if skipUnusedArrayDeref(v) { - p.compileValue(b, v.X) + x := p.compileValue(b, v.X) + if effectfulArrayDeref { + p.recordPanicLocation(b, v.Pos()) + b.AssertNilDeref(x) + } return } - // LLVM may eliminate an unused load, but evaluating a Go - // dereference must still panic when its pointer is nil. + // Elide the unused load, but keep an explicit nil check so the + // Go dereference still panics instead of relying on a trapping load. x := p.compileValue(b, v.X) p.recordPanicLocation(b, v.Pos()) p.assertNilDerefBase(b, v.X) b.AssertNilDeref(x) return } + if effectfulArrayDeref { + x := p.compileValue(b, v.X) + p.recordPanicLocation(b, v.Pos()) + b.AssertNilDeref(x) + } if refs, ok := nonDebugReferrers(v); ok && len(refs) == 1 { if _, ok := refs[0].(*ssa.MakeInterface); ok { if t := p.type_(v.Type(), llssa.InGo); t.RawType() != nil { diff --git a/cl/range_array_compile_test.go b/cl/range_array_compile_test.go index f004f839da..d957250c36 100644 --- a/cl/range_array_compile_test.go +++ b/cl/range_array_compile_test.go @@ -143,8 +143,8 @@ func rangeArrayReceive(ch <-chan *[3]int) { for _, name := range []string{"rangeArrayCall", "rangeArrayReceive"} { ir := mustNamedFunction(t, m, "foo."+name).String() - if !strings.Contains(ir, "AssertNilDeref") { - t.Fatalf("%s should preserve its required array pointer nil check:\n%s", name, ir) + if got := strings.Count(ir, "AssertNilDeref"); got != 1 { + t.Fatalf("%s nil-check count = %d, want 1:\n%s", name, got, ir) } } } From aaf28093643b7ee7da14c9489ecbc347f9e0e8d3 Mon Sep 17 00:00:00 2001 From: Li Jie Date: Fri, 14 Aug 2026 03:39:15 +0800 Subject: [PATCH 4/4] test(cl): cover builtin-referenced array deref --- cl/range_array_compile_test.go | 35 ++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/cl/range_array_compile_test.go b/cl/range_array_compile_test.go index d957250c36..791ec4a3ea 100644 --- a/cl/range_array_compile_test.go +++ b/cl/range_array_compile_test.go @@ -373,6 +373,41 @@ func copyArray(p *[5]int) [5]int { } } +func TestEffectfulArrayDerefWithBuiltinRefKeepsNilCheck(t *testing.T) { + ssaPkg, _, files := buildGoSSAPkg(t, ` +package foo + +func nextArray() *[3]int { return nil } + +func discard() { _ = *nextArray() } + +func lenSlice(s []int) int { return len(s) } +`) + + load := findUnOp(t, ssaPkg.Func("discard"), token.MUL, true) + refs := load.Referrers() + if refs == nil { + t.Fatal("array deref has no referrer list") + } + oldRefs := *refs + // Current x/tools folds len(*nextArray()) to a static constant and leaves + // the deref unused. Model the builtin ref shape retained by other supported + // SSA versions so the compiler compatibility path remains covered. + builtin := findBuiltinCall(t, ssaPkg.Func("lenSlice"), "len") + *refs = []ssa.Instruction{&ssa.Call{Call: ssa.CallCommon{Value: builtin}}} + defer func() { *refs = oldRefs }() + + prog := newLLSSAProg(t) + pkg, err := NewPackage(prog, ssaPkg, files) + if err != nil { + t.Fatal(err) + } + ir := mustNamedFunction(t, pkg.Module(), "foo.discard").String() + if !strings.Contains(ir, "foo.nextArray") || !strings.Contains(ir, "AssertNilDeref") { + t.Fatalf("builtin-referenced array deref should retain its call and nil check:\n%s", ir) + } +} + func findUnOp(t *testing.T, fn *ssa.Function, op token.Token, wantArray bool) *ssa.UnOp { t.Helper() for _, block := range fn.Blocks {