Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions assertion/function/assertiontree/backprop.go
Original file line number Diff line number Diff line change
Expand Up @@ -486,6 +486,18 @@ func backpropAcrossRange(rootNode *RootAssertionNode, lhs []ast.Expr, rhs ast.Ex
if typeIsString(rhsType) { // This checks if we are ranging over a string
produceAsIndex(1) // If we are ranging over a string, then the second lhs operand is also non-nil
} else {
// Ranging over a pointer to an array with a non-blank second iteration variable
// implicitly dereferences the pointer to read the elements, so it must be non-nil.
// All other range forms over a pointer to an array are nil-safe: with at most one
// (possibly blank) iteration variable the range expression is not even evaluated,
// since its length is a constant.
if typeshelper.IsDeeplyPtr(rhsType) && !asthelper.IsEmptyExpr(lhs[1]) {
rootNode.AddConsumption(&annotation.ConsumeTrigger{
Annotation: &annotation.PtrLoad{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}},
Expr: rhs,
Guards: guard.NoGuards(),
})
}
produceAsDeepRHS(1) // If we are not ranging over a string, then we cannot assume basic type
}
case 1:
Expand Down
31 changes: 24 additions & 7 deletions assertion/function/assertiontree/root_assertion_node.go
Original file line number Diff line number Diff line change
Expand Up @@ -531,12 +531,21 @@ func (r *RootAssertionNode) AddGuardMatch(expr ast.Expr, behavior GuardMatchBeha

func (r *RootAssertionNode) consumeIndexExpr(expr ast.Expr) {
t := r.Pass().TypesInfo.Types[expr].Type
if typeshelper.IsDeeplySlice(t) {
switch {
case typeshelper.IsDeeplySlice(t):
r.AddConsumption(&annotation.ConsumeTrigger{
Annotation: &annotation.SliceAccess{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}},
Expr: expr,
Guards: guard.NoGuards(),
})
case typeshelper.IsDeeplyPtr(t):
// The only pointer type that can be indexed is a pointer to an array, which implicitly
// dereferences the pointer: p[i] is shorthand for (*p)[i].
r.AddConsumption(&annotation.ConsumeTrigger{
Annotation: &annotation.PtrLoad{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}},
Expr: expr,
Guards: guard.NoGuards(),
})
}
}

Expand Down Expand Up @@ -846,12 +855,20 @@ func (r *RootAssertionNode) AddComputation(expr ast.Expr) {
case *ast.SliceExpr:
// similar to index case

// safe slicing contains b[:0] b[0:0] b[0:] b[:] b[:0:0] b[0:0:0] and length-bounded forms such
// as b[:len(b)], which are safe even when b is nil, so we do not create consumer triggers for
// those slicing.
if !r.isSafeSlicing(expr) {
// For all the other slicing, the slice must be nonnil, so we create a consumer
// trigger.
if typeshelper.IsDeeplyPtr(r.Pass().TypesInfo.TypeOf(expr.X)) {
// Slicing a pointer to an array implicitly dereferences the pointer (p[low:high] is
// shorthand for (*p)[low:high]), so the pointer must be non-nil regardless of the
// indices -- even for forms like p[:0] that are safe on a nil slice.
r.AddConsumption(&annotation.ConsumeTrigger{
Annotation: &annotation.PtrLoad{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}},
Expr: expr.X,
Guards: guard.NoGuards(),
})
} else if !r.isSafeSlicing(expr) {
// safe slicing contains b[:0] b[0:0] b[0:] b[:] b[:0:0] b[0:0:0] and length-bounded
// forms such as b[:len(b)], which are safe even when b is nil, so we do not create
// consumer triggers for those slicing. For all the other slicing, the slice must be
// nonnil, so we create a consumer trigger.
r.AddConsumption(&annotation.ConsumeTrigger{
Annotation: &annotation.SliceAccess{ConsumeTriggerTautology: &annotation.ConsumeTriggerTautology{}},
Expr: expr.X,
Expand Down
107 changes: 107 additions & 0 deletions testdata/src/go.uber.org/slices/inference/arrayptr.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
// Copyright (c) 2026 Uber Technologies, Inc.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package inference

// Implicit uses of a pointer to an array dereference the pointer and panic if it is nil:
// slicing (p[low:high] is shorthand for (*p)[low:high], even for forms like p[:0] that are safe
// on a nil slice), indexing (p[i] is (*p)[i], both reads and writes), and ranging with a
// non-blank second iteration variable (which reads the elements). Forms that never read the
// elements are nil-safe: len(p) and cap(p) are constants, and range loops with at most one
// (possibly blank) iteration variable do not even evaluate the range expression.

var arrayPtrDummy bool

func nilArrayPtr() *[4]int {
if arrayPtrDummy {
return nil
}
return &[4]int{}
}

func testArrayPtrSliceSafeForm() []int {
p := nilArrayPtr()
return p[:0] //want "dereferenced"
}

func testArrayPtrSliceFull() []int {
p := nilArrayPtr()
return p[:] //want "dereferenced"
}

func testArrayPtrSliceBounds() []int {
p := nilArrayPtr()
return p[1:3] //want "dereferenced"
}

func testArrayPtrIndexRead() int {
p := nilArrayPtr()
return p[0] //want "dereferenced"
}

func testArrayPtrIndexWrite() {
p := nilArrayPtr()
p[0] = 1 //want "dereferenced"
}

func testArrayPtrRangeSecondVar() int {
p := nilArrayPtr()
sum := 0
for _, v := range p { //want "dereferenced"
sum += v
}
return sum
}

// Named pointer-to-array types behave just like *[4]int (their core type is what gets sliced).
type namedArrayPtr *[4]int

func nilNamedArrayPtr() namedArrayPtr {
if arrayPtrDummy {
return nil
}
return &[4]int{}
}

func testNamedArrayPtrSlice() []int {
p := nilNamedArrayPtr()
return p[:] //want "dereferenced"
}

func testArrayPtrSafeUses() int {
p := nilArrayPtr()
n := len(p) // len of a pointer to an array is a constant; no dereference happens
for i := range p {
n += i
}
for i, _ := range p { // a blank second variable is equivalent to the one-variable form
n += i
}
for range p {
n++
}
return n
}

func testArrayPtrNilChecked() int {
p := nilArrayPtr()
if p == nil {
return 0
}
for _, v := range p {
_ = v
}
p[0] = 1
return p[0] + len(p[:]) + len(p[1:3])
}