go/constant: add StringLen function

For fast computation of a string value's length w/o the need to first
"materialize" the actual string.

Use StringLen in the type checker where appropriate.

Fixes #79042.
Fixes #78346.

Change-Id: Id602b060176b771d73fc737e0a37a9707f235a02
Reviewed-on: https://go-review.googlesource.com/c/go/+/772320
Auto-Submit: Robert Griesemer <gri@google.com>
LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Cuong Manh Le <cuong.manhle.vn@gmail.com>
Reviewed-by: Robert Griesemer <gri@google.com>
Reviewed-by: Cherry Mui <cherryyz@google.com>
This commit is contained in:
Robert Griesemer 2026-04-29 14:50:47 -07:00 committed by Robert Griesemer
parent 15fd4ff942
commit 2677fe9bbe
10 changed files with 73 additions and 30 deletions

1
api/next/79042.txt Normal file
View file

@ -0,0 +1 @@
pkg go/constant, func StringLen(Value) int64 #79042

View file

@ -0,0 +1 @@
The new [StringLen] function returns the length of a string [Value]. For an [Unknown] value, the length is 0.

View file

@ -153,7 +153,7 @@ func (check *Checker) builtin(x *operand, call *syntax.CallExpr, id builtinId) (
if isString(t) && id == _Len {
if x.mode() == constant_ {
mode = constant_
val = constant.MakeInt64(int64(len(constant.StringVal(x.val))))
val = constant.MakeInt64(constant.StringLen(x.val))
} else {
mode = value
}

View file

@ -49,16 +49,15 @@ func (check *Checker) overflow(x *operand, opPos syntax.Pos) {
return
}
const maxLen = int(2e9) // cmd/internal/obj.MaxSymSize
// Disable the length check for now, as calling constant.StringVal
// eagerly constructs the string and can lead to significant memory
// usage increase. We may want a StringLen function.
// TODO(go.dev/issue/78346): reenable the check.
if false && x.val.Kind() == constant.String && len(constant.StringVal(x.val)) > maxLen {
check.errorf(atPos(opPos), InvalidConstVal, "constant string too long (%d bytes > %d bytes)",
len(constant.StringVal(x.val)), maxLen)
x.val = constant.MakeUnknown()
return
// String values must not become arbitrarily long (go.dev/issue/78346).
const maxLen = int64(2e9) // cmd/internal/obj.MaxSymSize
if x.val.Kind() == constant.String {
len := constant.StringLen(x.val)
if len > maxLen {
check.errorf(atPos(opPos), InvalidConstVal, "constant string too long (%d bytes > %d bytes)", len, maxLen)
x.val = constant.MakeUnknown()
return
}
}
}

View file

@ -77,7 +77,7 @@ func (check *Checker) indexExpr(x *operand, e *syntax.IndexExpr) (isFuncInst boo
if isString(typ) {
valid = true
if x.mode() == constant_ {
length = int64(len(constant.StringVal(x.val)))
length = constant.StringLen(x.val)
}
// an indexed string always yields a byte value
// (not a constant) even if the string and the
@ -302,7 +302,7 @@ func (check *Checker) sliceExpr(x *operand, e *syntax.SliceExpr) {
}
valid = true
if x.mode() == constant_ {
length = int64(len(constant.StringVal(x.val)))
length = constant.StringLen(x.val)
}
// spec: "For untyped string operands the result
// is a non-constant value of type string."

View file

@ -137,15 +137,13 @@ func (x *stringVal) String() string {
// concatenation. See golang.org/issue/23348.
func (x *stringVal) string() string {
x.mu.Lock()
defer x.mu.Unlock()
if x.l != nil {
x.s = strings.Join(reverse(x.appendReverse(nil)), "")
x.l = nil
x.r = nil
}
s := x.s
x.mu.Unlock()
return s
return x.s
}
// reverse reverses x in place and returns it.
@ -609,6 +607,30 @@ func Val(x Value) any {
}
}
// StringLen returns the length of x if x is a [String].
// If x is [Unknown], the result is 0.
// In all other cases, the function panics.
func StringLen(x Value) int64 {
switch x := x.(type) {
case *stringVal:
return x.len()
case unknownVal:
return 0
default:
panic(fmt.Sprintf("%v not a String", x))
}
}
// len computes and returns the length of x without constructing the entire string.
func (x *stringVal) len() int64 {
x.mu.Lock()
defer x.mu.Unlock()
if x.l != nil {
return x.l.len() + x.r.len()
}
return int64(len(x.s))
}
// Make returns the [Value] for x.
//
// type of x result Kind

View file

@ -437,6 +437,27 @@ func TestString(t *testing.T) {
}
}
func TestStringLen(t *testing.T) {
tests := []struct {
x Value
want int64
}{
{MakeUnknown(), 0},
{val(`""`), 0},
{val(`"foo"`), 3},
{val(`"世界"`), 6},
{BinaryOp(val(`"foo"`), token.ADD, val(`"bar"`)), 6},
{BinaryOp(val(`"世界"`), token.ADD, val(`"!"`)), 7},
{BinaryOp(val(`"a"`), token.ADD, BinaryOp(val(`"b"`), token.ADD, val(`"c"`))), 3},
}
for _, test := range tests {
if got := StringLen(test.x); got != test.want {
t.Errorf("StringLen(%v): got %d; want %d", test.x, got, test.want)
}
}
}
// ----------------------------------------------------------------------------
// Support functions

View file

@ -156,7 +156,7 @@ func (check *Checker) builtin(x *operand, call *ast.CallExpr, id builtinId) (_ b
if isString(t) && id == _Len {
if x.mode() == constant_ {
mode = constant_
val = constant.MakeInt64(int64(len(constant.StringVal(x.val))))
val = constant.MakeInt64(constant.StringLen(x.val))
} else {
mode = value
}

View file

@ -51,16 +51,15 @@ func (check *Checker) overflow(x *operand, opPos token.Pos) {
return
}
const maxLen = int(2e9) // cmd/internal/obj.MaxSymSize
// Disable the length check for now, as calling constant.StringVal
// eagerly constructs the string and can lead to significant memory
// usage increase. We may want a StringLen function.
// TODO(go.dev/issue/78346): reenable the check.
if false && x.val.Kind() == constant.String && len(constant.StringVal(x.val)) > maxLen {
check.errorf(atPos(opPos), InvalidConstVal, "constant string too long (%d bytes > %d bytes)",
len(constant.StringVal(x.val)), maxLen)
x.val = constant.MakeUnknown()
return
// String values must not become arbitrarily long (go.dev/issue/78346).
const maxLen = int64(2e9) // cmd/internal/obj.MaxSymSize
if x.val.Kind() == constant.String {
len := constant.StringLen(x.val)
if len > maxLen {
check.errorf(atPos(opPos), InvalidConstVal, "constant string too long (%d bytes > %d bytes)", len, maxLen)
x.val = constant.MakeUnknown()
return
}
}
}

View file

@ -78,7 +78,7 @@ func (check *Checker) indexExpr(x *operand, e *indexedExpr) (isFuncInst bool) {
if isString(typ) {
valid = true
if x.mode() == constant_ {
length = int64(len(constant.StringVal(x.val)))
length = constant.StringLen(x.val)
}
// an indexed string always yields a byte value
// (not a constant) even if the string and the
@ -307,7 +307,7 @@ func (check *Checker) sliceExpr(x *operand, e *ast.SliceExpr) {
}
valid = true
if x.mode() == constant_ {
length = int64(len(constant.StringVal(x.val)))
length = constant.StringLen(x.val)
}
// spec: "For untyped string operands the result
// is a non-constant value of type string."