go/ssa: support pointer-typed composite literals in all contexts
In go1.28 (#12854), a composite literal may omit its type in any
assignment context; if the type is implicit and is a pointer *T,
it denotes &T{...}.
Previously this was possible only for nested literals (handled
by builder.assign), but now such literals may appear in other
contexts (call arguments, etc) all of which reach expr0.
It used to load a T value where a *T was required, causing a crash.
Now, expr0 treats a pointer type as an implicit &-operation.
This actually eliminates a number of special cases.
Recalling the recent change CL 778401, there is now no
difference at all between nested and non-nested literals.
This allows us to rip out the isZero machinery and the
storebuf parameter to compLit. Yay.
Updates golang/go#82039
Change-Id: I104b48855b214bcc4d9c135834e181223d71bbb3
Reviewed-on: https://go-review.googlesource.com/c/tools/+/846686
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Alex Putman <aputman@golang.org>
diff --git a/go/ssa/builder.go b/go/ssa/builder.go
index f204cc7..a20b9ad 100644
--- a/go/ssa/builder.go
+++ b/go/ssa/builder.go
@@ -466,9 +466,7 @@
} else {
v = emitLocal(fn, typ, e.Lbrace, "complit")
}
- var sb storebuf
- b.compLit(fn, v, e, true, &sb)
- sb.emit(fn)
+ b.compLit(fn, v, e)
return &address{addr: v, pos: e.Lbrace, expr: e}
case *ast.ParenExpr:
@@ -562,37 +560,18 @@
}
// assign emits to fn code to initialize the lvalue loc with the value
-// of expression e. If isZero is true, assign assumes that loc holds
-// the zero value for its type.
+// of expression e.
//
-// This is equivalent to loc.store(fn, b.expr(fn, e)), but may generate
-// better code in some cases, e.g., for composite literals in an
-// addressable location.
+// This is equivalent to loc.store(fn, b.expr(fn, e)).
//
// If sb is not nil, assign generates code to evaluate expression e, but
// not to update loc. Instead, the necessary stores are appended to the
// storebuf sb so that they can be executed later. This allows correct
// in-place update of existing variables when the RHS is a composite
// literal that may reference parts of the LHS.
-func (b *builder) assign(fn *Function, loc lvalue, e ast.Expr, isZero bool, sb *storebuf) {
- // Can we initialize it in place?
- if e, ok := ast.Unparen(e).(*ast.CompositeLit); ok {
- // A CompositeLit never evaluates to a pointer,
- // so if the type of the location is a pointer,
- // an &-operation is implied.
- if !is[blank](loc) && isPointerCore(loc.typ()) { // avoid calling blank.typ()
- ptr := b.addr(fn, e, true).address(fn)
- // copy address
- if sb != nil {
- sb.store(loc, ptr)
- } else {
- loc.store(fn, ptr)
- }
- return
- }
- }
-
- // simple case: just copy
+func (b *builder) assign(fn *Function, loc lvalue, e ast.Expr, sb *storebuf) {
+ // A pointer-typed CompositeLit (var _ *MyStruct = {})
+ // implies an &-operation; expr handles this.
rhs := b.expr(fn, e)
if sb != nil {
sb.store(loc, rhs)
@@ -628,6 +607,12 @@
return v
}
+// expr0 compiles the non-constant unparenthesized r-value expression e.
+//
+// A composite literal is not addressable, so it is handled here.
+// If its implicit type is a pointer *T, it denotes &T{...},
+// so it allocates a new T and returns its address. Otherwise
+// it builds the literal in a temporary and loads its value.
func (b *builder) expr0(fn *Function, e ast.Expr, tv types.TypeAndValue) Value {
switch e := e.(type) {
case *ast.BasicLit:
@@ -949,7 +934,18 @@
panic("unexpected container type in IndexExpr: " + xt.String())
}
- case *ast.CompositeLit, *ast.StarExpr:
+ case *ast.CompositeLit:
+ // A composite literal with no explicit type whose
+ // (inferred) type is a pointer *T is equivalent to &T{...}.
+ // This occurs for nested literals (e.g. []*T{{}}) and,
+ // since go1.28, in any assignment context (e.g. f({})).
+ if isPointerCore(fn.typeOf(e)) {
+ return b.addr(fn, e, true).address(fn)
+ }
+ // Value-typed literal: build in a temporary and load.
+ return b.addr(fn, e, false).load(fn)
+
+ case *ast.StarExpr:
// Addressable types (lvalues)
return b.addr(fn, e, false).load(fn)
}
@@ -1167,7 +1163,7 @@
emitLocalVar(fn, identVar(fn, id))
}
lval := b.addr(fn, id, false) // non-escaping
- b.assign(fn, lval, spec.Values[i], true, nil)
+ b.assign(fn, lval, spec.Values[i], nil)
}
case len(spec.Values) == 0:
@@ -1201,7 +1197,7 @@
// Note the similarity with localValueSpec.
func (b *builder) assignStmt(fn *Function, lhss, rhss []ast.Expr, isDef bool) {
// Side effects of all LHSs and RHSs must occur in left-to-right order.
- lvals, isZero := b.assignLHS(fn, lhss, isDef)
+ lvals := b.assignLHS(fn, lhss, isDef)
if len(lhss) == len(rhss) {
// Simple assignment: x = f() (!isDef)
// Parallel assignment: x, y = f(), g() (!isDef)
@@ -1211,7 +1207,7 @@
// so we need a storebuf.
var sb storebuf
for i := range rhss {
- b.assign(fn, lvals[i], rhss[i], isZero[i], &sb)
+ b.assign(fn, lvals[i], rhss[i], &sb)
}
sb.emit(fn)
} else {
@@ -1224,23 +1220,21 @@
}
}
-func (b *builder) assignLHS(fn *Function, lhss []ast.Expr, isDef bool) ([]lvalue, []bool) {
+func (b *builder) assignLHS(fn *Function, lhss []ast.Expr, isDef bool) []lvalue {
lvals := make([]lvalue, len(lhss))
- isZero := make([]bool, len(lhss))
for i, lhs := range lhss {
var lval lvalue = blank{}
if !isBlankIdent(lhs) {
if isDef {
if obj, ok := fn.info.Defs[lhs.(*ast.Ident)].(*types.Var); ok {
emitLocalVar(fn, obj)
- isZero[i] = true
}
}
lval = b.addr(fn, lhs, false) // non-escaping
}
lvals[i] = lval
}
- return lvals, isZero
+ return lvals
}
// assignSelectRecvStmt emits a receive assignment from a single-case select.
@@ -1258,7 +1252,7 @@
}
}
- lvals, _ := b.assignLHS(fn, assign.Lhs, assign.Tok == token.DEFINE)
+ lvals := b.assignLHS(fn, assign.Lhs, assign.Tok == token.DEFINE)
for i, lval := range lvals {
lval.store(fn, values[i])
}
@@ -1284,33 +1278,25 @@
// compLit emits to fn code to initialize a composite literal e at
// address addr with type typ.
//
-// Nested composite literals are recursively initialized in place
-// where possible. If isZero is true, compLit assumes that addr
-// holds the zero value for typ.
+// The variable addr must be a fresh zero-valued Alloc. Since it is
+// not reachable from any element expression, stores to it may be
+// interleaved with evaluation of the elements and need not be
+// deferred using a storebuf. In an assignment such as
//
-// Because the elements of a composite literal may refer to the
-// variables being updated, as in the second line below,
-//
-// x := T{a: 1}
// x = T{a: x.a}
//
-// all the reads must occur before all the writes. Thus all stores to
-// loc are emitted to the storebuf sb for later execution.
+// the literal is built in a temporary and then copied to x
+// (see assignStmt), so all reads of x still precede the write.
//
-// A CompositeLit may have pointer type only in the recursive (nested)
-// case when the type name is implicit. e.g. in []*T{{}}, the inner
-// literal has type *T behaves like &T{}.
+// A CompositeLit may have pointer type only when the type name is
+// implicit, e.g. in []*T{{}}, the inner literal has type *T and
+// behaves like &T{}. (Since go1.28 the type may be omitted in any
+// assignment context, e.g. f({}) where f's parameter is *T.)
// In that case, addr must hold a T, not a *T.
-func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit, isZero bool, sb *storebuf) {
+func (b *builder) compLit(fn *Function, addr Value, e *ast.CompositeLit) {
typ := typeparams.Deref(fn.typeOf(e)) // retain the named/alias/param type, if any
switch t := typeparams.CoreType(typ).(type) {
case *types.Struct:
- if !isZero && len(e.Elts) != t.NumFields() {
- // memclear
- zt := typeparams.MustDeref(addr.Type())
- sb.store(&address{addr, e.Lbrace, nil}, zeroConst(zt))
- isZero = true
- }
var fIndices []int
for i, e := range e.Elts {
var (
@@ -1341,7 +1327,7 @@
faddr.setPos(pos)
faddr.setType(types.NewPointer(fType))
fn.emit(faddr)
- b.assign(fn, &address{addr: faddr, pos: pos, expr: e}, e, isZero, sb)
+ b.assign(fn, &address{addr: faddr, pos: pos, expr: e}, e, nil)
}
case *types.Array, *types.Slice:
@@ -1354,12 +1340,6 @@
case *types.Array:
at = t
array = addr
-
- if !isZero && int64(len(e.Elts)) != at.Len() {
- // memclear
- zt := typeparams.MustDeref(array.Type())
- sb.store(&address{array, e.Lbrace, nil}, zeroConst(zt))
- }
}
var idx *Const
@@ -1382,19 +1362,14 @@
}
iaddr.setType(types.NewPointer(at.Elem()))
fn.emit(iaddr)
- if t != at { // slice
- // backing array is unaliased => storebuf not needed.
- b.assign(fn, &address{addr: iaddr, pos: pos, expr: e}, e, true, nil)
- } else {
- b.assign(fn, &address{addr: iaddr, pos: pos, expr: e}, e, true, sb)
- }
+ b.assign(fn, &address{addr: iaddr, pos: pos, expr: e}, e, nil)
}
if t != at { // slice
s := &Slice{X: array}
s.setPos(e.Lbrace)
s.setType(typ)
- sb.store(&address{addr: addr, pos: e.Lbrace, expr: e}, fn.emit(s))
+ (&address{addr: addr, pos: e.Lbrace, expr: e}).store(fn, fn.emit(s))
}
case *types.Map:
@@ -1405,43 +1380,15 @@
for _, e := range e.Elts {
e := e.(*ast.KeyValueExpr)
- // If a key expression in a map literal is itself a
- // composite literal, the type may be omitted.
- // For example:
- // map[*struct{}]bool{{}: true}
- // An &-operation may be implied:
- // map[*struct{}]bool{&struct{}{}: true}
- wantAddr := false
- if _, ok := ast.Unparen(e.Key).(*ast.CompositeLit); ok {
- wantAddr = isPointerCore(t.Key())
- }
-
- var key Value
- if wantAddr {
- // A CompositeLit never evaluates to a pointer,
- // so if the type of the location is a pointer,
- // an &-operation is implied.
- key = b.addr(fn, e.Key, true).address(fn)
- } else {
- key = b.expr(fn, e.Key)
- }
-
- loc := element{
+ loc := &element{
m: m,
- k: emitConv(fn, key, t.Key()),
+ k: emitConv(fn, b.expr(fn, e.Key), t.Key()),
t: t.Elem(),
pos: e.Colon,
}
-
- // We call assign() only because it takes care
- // of any &-operation required in the recursive
- // case, e.g.,
- // map[int]*struct{}{0: {}} implies &struct{}{}.
- // In-place update is of course impossible,
- // and no storebuf is needed.
- b.assign(fn, &loc, e.Value, true, nil)
+ b.assign(fn, loc, e.Value, nil)
}
- sb.store(&address{addr: addr, pos: e.Lbrace, expr: e}, m)
+ (&address{addr: addr, pos: e.Lbrace, expr: e}).store(fn, m)
default:
panic("unexpected CompositeLit type: " + typ.String())
@@ -3300,7 +3247,7 @@
} else {
lval = blank{}
}
- b.assign(fn, lval, varinit.Rhs, true, nil)
+ b.assign(fn, lval, varinit.Rhs, nil)
} else {
// n:1 initialization: var x, y := f()
tuple := b.exprN(fn, varinit.Rhs)
diff --git a/go/ssa/interp/interp_test.go b/go/ssa/interp/interp_test.go
index dd44b7e..296dbda 100644
--- a/go/ssa/interp/interp_test.go
+++ b/go/ssa/interp/interp_test.go
@@ -112,6 +112,7 @@
var testdataTests = []string{
"boundmeth.go",
"complit.go",
+ "complit_go128.go",
"convert.go",
"coverage.go",
"deepequal.go",
diff --git a/go/ssa/interp/testdata/complit_go128.go b/go/ssa/interp/testdata/complit_go128.go
new file mode 100644
index 0000000..be46dee
--- /dev/null
+++ b/go/ssa/interp/testdata/complit_go128.go
@@ -0,0 +1,179 @@
+// Copyright 2026 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build go1.28
+
+package main
+
+// Test composite literals whose type is inferred from
+// the assignment context, introduced in go1.28.
+// See golang/go#12854 and golang/go#82039.
+
+type T struct{ x, y int }
+
+func val(t T) int { return t.x + t.y }
+func ptr(p *T) int { return p.x + p.y }
+func retVal() T { return {1, 2} }
+func retPtr() *T { return {3, 4} }
+func slice(s []int) int { return len(s) }
+func retTwo() (T, *T) { return {1, 2}, {3, 4} }
+
+func retGeneric[P *T]() P { return {5, 6} }
+func argGeneric[P *T](p P) int { return (*T)(p).x + (*T)(p).y }
+
+type S struct {
+ V T
+ P *T
+}
+
+// Pointers to arrays.
+func ptrArray(p *[3]int) int { return p[0] + p[1] + p[2] }
+func retPtrArray() *[3]int { return {1, 2, 3} }
+
+// Pointers to slices and maps (rarely useful, but legal).
+func ptrSlice(p *[]int) int { return len(*p) }
+func ptrMap(p *map[string]int) int { return (*p)["a"] }
+func retPtrSlice() *[]int { return {1, 2, 3} }
+func retPtrMap() *map[string]int { return {"a": 7} }
+
+func main() {
+ if got := val({1, 2}); got != 3 {
+ panic(got)
+ }
+ if got := ptr({3, 4}); got != 7 {
+ panic(got)
+ }
+ if got := retVal(); got != (T{1, 2}) {
+ panic(got)
+ }
+ if got := retPtr(); *got != (T{3, 4}) {
+ panic(got)
+ }
+ if got := slice({1, 2, 3}); got != 3 {
+ panic(got)
+ }
+
+ // Multi-value return.
+ if v, p := retTwo(); v != (T{1, 2}) || *p != (T{3, 4}) {
+ panic("retTwo")
+ }
+
+ // Type parameter whose core type is *T.
+ if got := retGeneric[*T](); *got != (T{5, 6}) {
+ panic(got)
+ }
+ if got := argGeneric[*T]({7, 8}); got != 15 {
+ panic(got)
+ }
+
+ // Assignments and declarations.
+ var p *T = {5, 6}
+ if p.y != 6 {
+ panic(p)
+ }
+ p = {7, 8}
+ if p.y != 8 {
+ panic(p)
+ }
+
+ // Distinct allocations.
+ if retPtr() == retPtr() {
+ panic("retPtr aliases")
+ }
+
+ // Conversion.
+ q := (*T)({9, 10})
+ if q.x != 9 {
+ panic(q)
+ }
+
+ // Channel send.
+ ch := make(chan *T, 1)
+ ch <- {11, 12}
+ if r := <-ch; r.y != 12 {
+ panic(r)
+ }
+
+ // Map key and value.
+ m := map[T]*T{}
+ m[{1, 1}] = {2, 2}
+ if r := m[T{1, 1}]; r == nil || r.x != 2 {
+ panic(r)
+ }
+
+ // Struct fields.
+ s := S{V: {1, 2}, P: {3, 4}}
+ if s.V.y != 2 || s.P.y != 4 {
+ panic(s)
+ }
+ s2 := S{{5, 6}, {7, 8}}
+ if s2.V.y != 6 || s2.P.y != 8 {
+ panic(s2)
+ }
+
+ // Reassigning a pointer variable from a literal that reads
+ // through it allocates a new variable; the old one is unchanged.
+ old := p
+ p = {p.y, p.x}
+ if p.x != 8 || p.y != 7 || old.x != 7 || old.y != 8 {
+ panic("swap *T")
+ }
+
+ // Pointers to arrays.
+ if got := ptrArray({1, 2, 3}); got != 6 {
+ panic(got)
+ }
+ if got := ptrArray({2: 5}); got != 5 {
+ panic(got)
+ }
+ if got := retPtrArray(); *got != [3]int{1, 2, 3} {
+ panic(got)
+ }
+ if retPtrArray() == retPtrArray() {
+ panic("retPtrArray aliases")
+ }
+ var pa *[3]int = {4, 5, 6}
+ olda := pa
+ pa = {pa[2], pa[1], pa[0]}
+ if *pa != [3]int{6, 5, 4} || *olda != [3]int{4, 5, 6} {
+ panic("swap *[3]int")
+ }
+ nestedArr := []*[2]int{{1, 2}, {3, 4}}
+ if nestedArr[1][1] != 4 {
+ panic(nestedArr[1][1])
+ }
+ cha := make(chan *[2]int, 1)
+ cha <- {7, 8}
+ if r := <-cha; r[1] != 8 {
+ panic(r[1])
+ }
+
+ // Pointers to slices and maps.
+ if got := ptrSlice({1, 2}); got != 2 {
+ panic(got)
+ }
+ if got := ptrMap({"a": 5}); got != 5 {
+ panic(got)
+ }
+ if got := len(*retPtrSlice()); got != 3 {
+ panic(got)
+ }
+ if got := (*retPtrMap())["a"]; got != 7 {
+ panic(got)
+ }
+ var ps *[]int = {4}
+ if (*ps)[0] != 4 {
+ panic((*ps)[0])
+ }
+ nested := []*[]int{{1}, {2, 3}}
+ if got := len(*nested[1]); got != 2 {
+ panic(got)
+ }
+ nm := map[*map[int]int]bool{{1: 2}: true}
+ for k := range nm {
+ if got := (*k)[1]; got != 2 {
+ panic(got)
+ }
+ }
+}