[dev.simd] all: merge master (f8ccda2) into dev.simd

Merge List:

+ 2025-06-25 f8ccda2e05 runtime: make explicit nil check in (*spanInlineMarkBits).init
+ 2025-06-25 f069a82998 runtime: note custom GOMAXPROCS even if value doesn't change
+ 2025-06-24 e515ef8bc2 context: fix typo in context_test.go
+ 2025-06-24 47b941f445 cmd/link: add one more linkname to the blocklist
+ 2025-06-24 34cf5f6205 go/types: add test for interface method field type
+ 2025-06-24 6e618cd42a encoding/json: use zstd compressed testdata
+ 2025-06-24 fcb9850859 net/http: reduce allocs in CrossOriginProtection.Check
+ 2025-06-24 11f11f2a00 encoding/json/v2: support ISO 8601 durations
+ 2025-06-24 62deaf4fb8 doc: fix links to runtime Environment Variables
+ 2025-06-24 2e9bb62bfe encoding/json/v2: reject unquoted dash as a JSON field name
+ 2025-06-23 ed7815726d encoding/json/v2: report error on time.Duration without explicit format
+ 2025-06-23 f866958246 cmd/dist: test encoding/json/... with GOEXPERIMENT=jsonv2
+ 2025-06-23 f77a0aa6b6 internal/trace: improve gc-stress test
+ 2025-06-23 4506796a6e encoding/json/jsontext: consistently use JSON terminology
+ 2025-06-23 456a90aa16 runtime: add missing unlock in sysReserveAlignedSbrk
+ 2025-06-23 1cf6386b5e Revert "go/types, types2: don't register interface methods in Info.Types map"
+ 2025-06-20 49cdf0c42e testing, testing/synctest: handle T.Helper in synctest bubbles
+ 2025-06-20 3bf1eecbd3 runtime: fix struct comment
+ 2025-06-20 8ed23a2936 crypto/cipher: fix link to crypto/aes
+ 2025-06-20 ef60769b46 go/doc: add a golden test that reproduces #62640
+ 2025-06-18 8552bcf7c2 cmd/go/internal/fips140: ignore GOEXPERIMENT on error
+ 2025-06-18 4c7567290c runtime: set mspan limit field early and eagerly
+ 2025-06-18 c6ac736288 runtime: prevent mutual deadlock between GC stopTheWorld and suspendG
+ 2025-06-17 53af292aed encoding/json/jsontext: fix spelling error
+ 2025-06-16 d058254689 cmd/dist: always include variant in package names
+ 2025-06-16 3254c2bb83 internal/reflectlite: fix comment about meaning of flag field
+ 2025-06-16 816199e421 runtime: don't let readTrace spin on trace.shutdown
+ 2025-06-16 ea00461b17 internal/trace: make Value follow reflect conventions
+ 2025-06-13 96a6e147b2 runtime: comment that some linknames are used by runtime/trace
+ 2025-06-13 644905891f runtime: remove unused unique.runtime_blockUntilEmptyFinalizerQueue
+ 2025-06-13 683810a368 cmd/link: block new standard library linknames
+ 2025-06-12 9149876112 all: replace a few user-visible mentions of golang.org and godoc.org
+ 2025-06-12 934d5f2cf7 internal/trace: end test programs with SIGQUIT
+ 2025-06-12 5a08865de3 net: remove some BUG entries
+ 2025-06-11 d166a0b03e encoding/json/jsontext, encoding/json/v2: document experimental nature
+ 2025-06-11 d4c6effaa7 cmd/compile: add up-to-date test for generated files
+ 2025-06-10 7fa2c736b3 os: disallow Root.Remove(".") on Plan 9, js, and Windows
+ 2025-06-10 281cfcfc1b runtime: handle system goroutines later in goroutine profiling
+ 2025-06-10 4f86f22671 testing/synctest, runtime: avoid panic when using linker-alloc WG from bubble

Change-Id: I8bbbf40ce053a80395b08977e21b1f34c67de117
This commit is contained in:
Cherry Mui 2025-06-25 15:00:04 -04:00
commit f4a7c124cc
80 changed files with 1159 additions and 371 deletions

View file

@ -187,7 +187,7 @@ Go 1.25 switched to SHA-256 to fill in missing SubjectKeyId in
crypto/x509.CreateCertificate. The setting `x509sha256skid=0` reverts to SHA-1. crypto/x509.CreateCertificate. The setting `x509sha256skid=0` reverts to SHA-1.
Go 1.25 corrected the semantics of contention reports for runtime-internal locks, Go 1.25 corrected the semantics of contention reports for runtime-internal locks,
and so removed the [`runtimecontentionstacks` setting](/pkg/runtime#hdr-Environment_Variable). and so removed the [`runtimecontentionstacks` setting](/pkg/runtime#hdr-Environment_Variables).
### Go 1.24 ### Go 1.24
@ -369,7 +369,7 @@ In particular, a common default Linux kernel configuration can result in
significant memory overheads, and Go 1.22 no longer works around this default. significant memory overheads, and Go 1.22 no longer works around this default.
To work around this issue without adjusting kernel settings, transparent huge To work around this issue without adjusting kernel settings, transparent huge
pages can be disabled for Go memory with the pages can be disabled for Go memory with the
[`disablethp` setting](/pkg/runtime#hdr-Environment_Variable). [`disablethp` setting](/pkg/runtime#hdr-Environment_Variables).
This behavior was backported to Go 1.21.1, but the setting is only available This behavior was backported to Go 1.21.1, but the setting is only available
starting with Go 1.21.6. starting with Go 1.21.6.
This setting may be removed in a future release, and users impacted by this issue This setting may be removed in a future release, and users impacted by this issue
@ -381,7 +381,7 @@ Go 1.22 added contention on runtime-internal locks to the [`mutex`
profile](/pkg/runtime/pprof#Profile). Contention on these locks is always profile](/pkg/runtime/pprof#Profile). Contention on these locks is always
reported at `runtime._LostContendedRuntimeLock`. Complete stack traces of reported at `runtime._LostContendedRuntimeLock`. Complete stack traces of
runtime locks can be enabled with the [`runtimecontentionstacks` runtime locks can be enabled with the [`runtimecontentionstacks`
setting](/pkg/runtime#hdr-Environment_Variable). These stack traces have setting](/pkg/runtime#hdr-Environment_Variables). These stack traces have
non-standard semantics, see setting documentation for details. non-standard semantics, see setting documentation for details.
Go 1.22 added a new [`crypto/x509.Certificate`](/pkg/crypto/x509/#Certificate) Go 1.22 added a new [`crypto/x509.Certificate`](/pkg/crypto/x509/#Certificate)

View file

@ -14,7 +14,7 @@ case "$GOWASIRUNTIME" in
exec wazero run -mount /:/ -env-inherit -cachedir "${TMPDIR:-/tmp}"/wazero ${GOWASIRUNTIMEARGS:-} "$1" "${@:2}" exec wazero run -mount /:/ -env-inherit -cachedir "${TMPDIR:-/tmp}"/wazero ${GOWASIRUNTIMEARGS:-} "$1" "${@:2}"
;; ;;
"wasmtime" | "") "wasmtime" | "")
exec wasmtime run --dir=/ --env PWD="$PWD" --env PATH="$PATH" -W max-wasm-stack=1048576 ${GOWASIRUNTIMEARGS:-} "$1" "${@:2}" exec wasmtime run --dir=/ --env PWD="$PWD" --env PATH="$PATH" -W max-wasm-stack=8388608 ${GOWASIRUNTIMEARGS:-} "$1" "${@:2}"
;; ;;
*) *)
echo "Unknown Go WASI runtime specified: $GOWASIRUNTIME" echo "Unknown Go WASI runtime specified: $GOWASIRUNTIME"

View file

@ -358,6 +358,11 @@ func TestTypesInfo(t *testing.T) {
// go.dev/issue/47895 // go.dev/issue/47895
{`package p; import "unsafe"; type S struct { f int }; var s S; var _ = unsafe.Offsetof(s.f)`, `s.f`, `int`}, {`package p; import "unsafe"; type S struct { f int }; var s S; var _ = unsafe.Offsetof(s.f)`, `s.f`, `int`},
// go.dev/issue/74303. Note that interface field types are synthetic, so
// even though `func()` doesn't appear in the source, it appears in the
// syntax tree.
{`package p; type T interface { M(int) }`, `func(int)`, `func(int)`},
// go.dev/issue/50093 // go.dev/issue/50093
{`package u0a; func _[_ interface{int}]() {}`, `int`, `int`}, {`package u0a; func _[_ interface{int}]() {}`, `int`, `int`},
{`package u1a; func _[_ interface{~int}]() {}`, `~int`, `~int`}, {`package u1a; func _[_ interface{~int}]() {}`, `~int`, `~int`},

View file

@ -137,19 +137,17 @@ func (check *Checker) interfaceType(ityp *Interface, iface *syntax.InterfaceType
name := f.Name.Value name := f.Name.Value
if name == "_" { if name == "_" {
check.error(f.Name, BlankIfaceMethod, "methods must have a unique non-blank name") check.error(f.Name, BlankIfaceMethod, "methods must have a unique non-blank name")
continue // ignore method continue // ignore
} }
// Type-check method declaration. typ := check.typ(f.Type)
// Note: Don't call check.typ(f.Type) as that would record sig, _ := typ.(*Signature)
// the method incorrectly as a type expression in Info.Types. if sig == nil {
ftyp, _ := f.Type.(*syntax.FuncType) if isValid(typ) {
if ftyp == nil { check.errorf(f.Type, InvalidSyntaxTree, "%s is not a method signature", typ)
check.errorf(f.Type, InvalidSyntaxTree, "%s is not a method signature", f.Type) }
continue // ignore method continue // ignore
} }
sig := new(Signature)
check.funcType(sig, nil, nil, ftyp)
// use named receiver type if available (for better error messages) // use named receiver type if available (for better error messages)
var recvTyp Type = ityp var recvTyp Type = ityp

23
src/cmd/dist/test.go vendored
View file

@ -336,6 +336,10 @@ type goTest struct {
// omitVariant indicates that variant is used solely for the dist test name and // omitVariant indicates that variant is used solely for the dist test name and
// that the set of test names run by each variant (including empty) of a package // that the set of test names run by each variant (including empty) of a package
// is non-overlapping. // is non-overlapping.
//
// TODO(mknyszek): Consider removing omitVariant as it is no longer set to true
// by any test. It's too valuable to have timing information in ResultDB that
// corresponds directly with dist names for tests.
omitVariant bool omitVariant bool
// We have both pkg and pkgs as a convenience. Both may be set, in which // We have both pkg and pkgs as a convenience. Both may be set, in which
@ -596,7 +600,10 @@ func (t *tester) registerRaceBenchTest(pkg string) {
ranGoBench = true ranGoBench = true
return (&goTest{ return (&goTest{
variant: "racebench", variant: "racebench",
omitVariant: true, // The only execution of benchmarks in dist; benchmark names are guaranteed not to overlap with test names. // Include the variant even though there's no overlap in test names.
// This makes the test targets distinct, allowing our build system to record
// elapsed time for each one, which is useful for load-balancing test shards.
omitVariant: false,
timeout: 1200 * time.Second, // longer timeout for race with benchmarks timeout: 1200 * time.Second, // longer timeout for race with benchmarks
race: true, race: true,
bench: true, bench: true,
@ -736,6 +743,15 @@ func (t *tester) registerTests() {
} }
} }
// Test GOEXPERIMENT=jsonv2.
if !strings.Contains(goexperiment, "jsonv2") {
t.registerTest("GOEXPERIMENT=jsonv2 go test encoding/json/...", &goTest{
variant: "jsonv2",
env: []string{"GOEXPERIMENT=jsonv2"},
pkg: "encoding/json/...",
})
}
// Test ios/amd64 for the iOS simulator. // Test ios/amd64 for the iOS simulator.
if goos == "darwin" && goarch == "amd64" && t.cgoEnabled { if goos == "darwin" && goarch == "amd64" && t.cgoEnabled {
t.registerTest("GOOS=ios on darwin/amd64", t.registerTest("GOOS=ios on darwin/amd64",
@ -986,7 +1002,10 @@ func (t *tester) registerTests() {
t.registerTest("../test", t.registerTest("../test",
&goTest{ &goTest{
variant: id, variant: id,
omitVariant: true, // Shards of the same Go package; tests are guaranteed not to overlap. // Include the variant even though there's no overlap in test names.
// This makes the test target more clearly distinct in our build
// results and is important for load-balancing test shards.
omitVariant: false,
pkg: "cmd/internal/testdir", pkg: "cmd/internal/testdir",
testFlags: []string{fmt.Sprintf("-shard=%d", shard), fmt.Sprintf("-shards=%d", nShards)}, testFlags: []string{fmt.Sprintf("-shard=%d", shard), fmt.Sprintf("-shards=%d", nShards)},
runOnHost: true, runOnHost: true,

View file

@ -69,7 +69,7 @@ const bugFooter = `### What did you do?
<!-- <!--
If possible, provide a recipe for reproducing the error. If possible, provide a recipe for reproducing the error.
A complete runnable program is good. A complete runnable program is good.
A link on play.golang.org is best. A link on go.dev/play is best.
--> -->

View file

@ -28,7 +28,7 @@ var initDefaultCacheOnce = sync.OnceValue(initDefaultCache)
const cacheREADME = `This directory holds cached build artifacts from the Go build system. const cacheREADME = `This directory holds cached build artifacts from the Go build system.
Run "go clean -cache" if the directory is getting too large. Run "go clean -cache" if the directory is getting too large.
Run "go clean -fuzzcache" to delete the fuzz cache. Run "go clean -fuzzcache" to delete the fuzz cache.
See golang.org to learn more about Go. See go.dev to learn more about Go.
` `
// initDefaultCache does the work of finding the default cache // initDefaultCache does the work of finding the default cache

View file

@ -114,7 +114,11 @@ func Init() {
fsys.Bind(Dir(), filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140")) fsys.Bind(Dir(), filepath.Join(cfg.GOROOT, "src/crypto/internal/fips140"))
} }
if cfg.Experiment.BoringCrypto && Enabled() { // ExperimentErr != nil if GOEXPERIMENT failed to parse. Typically
// cmd/go main will exit in this case, but it is allowed during
// toolchain selection, as the GOEXPERIMENT may be valid for the
// selected toolchain version.
if cfg.ExperimentErr == nil && cfg.Experiment.BoringCrypto && Enabled() {
base.Fatalf("go: cannot use GOFIPS140 with GOEXPERIMENT=boringcrypto") base.Fatalf("go: cannot use GOFIPS140 with GOEXPERIMENT=boringcrypto")
} }
} }

View file

@ -2368,7 +2368,6 @@ var blockedLinknames = map[string][]string{
"crypto/internal/sysrand.fatal": {"crypto/internal/sysrand"}, "crypto/internal/sysrand.fatal": {"crypto/internal/sysrand"},
"crypto/rand.fatal": {"crypto/rand"}, "crypto/rand.fatal": {"crypto/rand"},
"internal/runtime/maps.errNilAssign": {"internal/runtime/maps"}, "internal/runtime/maps.errNilAssign": {"internal/runtime/maps"},
"internal/runtime/maps.typeString": {"internal/runtime/maps"},
"internal/runtime/maps.fatal": {"internal/runtime/maps"}, "internal/runtime/maps.fatal": {"internal/runtime/maps"},
"internal/runtime/maps.newarray": {"internal/runtime/maps"}, "internal/runtime/maps.newarray": {"internal/runtime/maps"},
"internal/runtime/maps.newobject": {"internal/runtime/maps"}, "internal/runtime/maps.newobject": {"internal/runtime/maps"},
@ -2399,6 +2398,23 @@ var blockedLinknames = map[string][]string{
"runtime.mapdelete_fast32": {"runtime"}, "runtime.mapdelete_fast32": {"runtime"},
"runtime.mapdelete_fast64": {"runtime"}, "runtime.mapdelete_fast64": {"runtime"},
"runtime.mapdelete_faststr": {"runtime"}, "runtime.mapdelete_faststr": {"runtime"},
// New internal linknames in Go 1.25
// Pushed from runtime
"internal/cpu.riscvHWProbe": {"internal/cpu"},
"internal/runtime/cgroup.throw": {"internal/runtime/cgroup"},
"internal/runtime/maps.typeString": {"internal/runtime/maps"},
"internal/synctest.IsInBubble": {"internal/synctest"},
"internal/synctest.associate": {"internal/synctest"},
"internal/synctest.disassociate": {"internal/synctest"},
"internal/synctest.isAssociated": {"internal/synctest"},
"runtime/trace.runtime_readTrace": {"runtime/trace"},
"runtime/trace.runtime_traceClockUnitsPerSecond": {"runtime/trace"},
"sync_test.runtime_blockUntilEmptyCleanupQueue": {"sync_test"},
"time.runtimeIsBubbled": {"time"},
"unique.runtime_blockUntilEmptyCleanupQueue": {"unique"},
// Others
"net.newWindowsFile": {"net"}, // pushed from os
"testing/synctest.testingSynctestTest": {"testing/synctest"}, // pushed from testing
} }
// check if a linkname reference to symbol s from pkg is allowed // check if a linkname reference to symbol s from pkg is allowed

View file

@ -283,11 +283,11 @@ func (g *globalMetricGenerator) GlobalMetric(ctx *traceContext, ev *trace.Event)
m := ev.Metric() m := ev.Metric()
switch m.Name { switch m.Name {
case "/memory/classes/heap/objects:bytes": case "/memory/classes/heap/objects:bytes":
ctx.HeapAlloc(ctx.elapsed(ev.Time()), m.Value.ToUint64()) ctx.HeapAlloc(ctx.elapsed(ev.Time()), m.Value.Uint64())
case "/gc/heap/goal:bytes": case "/gc/heap/goal:bytes":
ctx.HeapGoal(ctx.elapsed(ev.Time()), m.Value.ToUint64()) ctx.HeapGoal(ctx.elapsed(ev.Time()), m.Value.Uint64())
case "/sched/gomaxprocs:threads": case "/sched/gomaxprocs:threads":
ctx.Gomaxprocs(m.Value.ToUint64()) ctx.Gomaxprocs(m.Value.Uint64())
} }
} }

View file

@ -5,7 +5,7 @@
package context package context
// Tests in package context cannot depend directly on package testing due to an import cycle. // Tests in package context cannot depend directly on package testing due to an import cycle.
// If your test does requires access to unexported members of the context package, // If your test requires access to unexported members of the context package,
// add your test below as `func XTestFoo(t testingT)` and add a `TestFoo` to x_test.go // add your test below as `func XTestFoo(t testingT)` and add a `TestFoo` to x_test.go
// that calls it. Otherwise, write a regular test in a test.go file in package context_test. // that calls it. Otherwise, write a regular test in a test.go file in package context_test.

View file

@ -82,7 +82,7 @@ func newGCM(cipher Block, nonceSize, tagSize int) (AEAD, error) {
// NewGCMWithRandomNonce returns the given cipher wrapped in Galois Counter // NewGCMWithRandomNonce returns the given cipher wrapped in Galois Counter
// Mode, with randomly-generated nonces. The cipher must have been created by // Mode, with randomly-generated nonces. The cipher must have been created by
// [aes.NewCipher]. // [crypto/aes.NewCipher].
// //
// It generates a random 96-bit nonce, which is prepended to the ciphertext by Seal, // It generates a random 96-bit nonce, which is prepended to the ciphertext by Seal,
// and is extracted from the ciphertext by Open. The NonceSize of the AEAD is zero, // and is extracted from the ciphertext by Open. The NonceSize of the AEAD is zero,

View file

@ -14,9 +14,9 @@ package json
import ( import (
"bytes" "bytes"
"compress/gzip"
"fmt" "fmt"
"internal/testenv" "internal/testenv"
"internal/zstd"
"io" "io"
"os" "os"
"reflect" "reflect"
@ -46,15 +46,12 @@ var codeJSON []byte
var codeStruct codeResponse var codeStruct codeResponse
func codeInit() { func codeInit() {
f, err := os.Open("testdata/code.json.gz") f, err := os.Open("internal/jsontest/testdata/golang_source.json.zst")
if err != nil { if err != nil {
panic(err) panic(err)
} }
defer f.Close() defer f.Close()
gz, err := gzip.NewReader(f) gz := zstd.NewReader(f)
if err != nil {
panic(err)
}
data, err := io.ReadAll(gz) data, err := io.ReadAll(gz)
if err != nil { if err != nil {
panic(err) panic(err)

View file

@ -1189,6 +1189,27 @@ var unmarshalTests = []struct {
out: []int{1, 2, 0, 4, 5}, out: []int{1, 2, 0, 4, 5},
err: &UnmarshalTypeError{Value: "bool", Type: reflect.TypeFor[int](), Offset: 9}, err: &UnmarshalTypeError{Value: "bool", Type: reflect.TypeFor[int](), Offset: 9},
}, },
{
CaseName: Name("DashComma"),
in: `{"-":"hello"}`,
ptr: new(struct {
F string `json:"-,"`
}),
out: struct {
F string `json:"-,"`
}{"hello"},
},
{
CaseName: Name("DashCommaOmitEmpty"),
in: `{"-":"hello"}`,
ptr: new(struct {
F string `json:"-,omitempty"`
}),
out: struct {
F string `json:"-,omitempty"`
}{"hello"},
},
} }
func TestMarshal(t *testing.T) { func TestMarshal(t *testing.T) {

View file

@ -10,6 +10,11 @@
// primitive data types such as booleans, strings, and numbers, // primitive data types such as booleans, strings, and numbers,
// in addition to structured data types such as objects and arrays. // in addition to structured data types such as objects and arrays.
// //
// This package (encoding/json/jsontext) is experimental,
// and not subject to the Go 1 compatibility promise.
// It only exists when building with the GOEXPERIMENT=jsonv2 environment variable set.
// Most users should use [encoding/json].
//
// The [Encoder] and [Decoder] types are used to encode or decode // The [Encoder] and [Decoder] types are used to encode or decode
// a stream of JSON tokens or values. // a stream of JSON tokens or values.
// //
@ -20,8 +25,8 @@
// - a JSON literal (i.e., null, true, or false) // - a JSON literal (i.e., null, true, or false)
// - a JSON string (e.g., "hello, world!") // - a JSON string (e.g., "hello, world!")
// - a JSON number (e.g., 123.456) // - a JSON number (e.g., 123.456)
// - a start or end delimiter for a JSON object (i.e., '{' or '}') // - a begin or end delimiter for a JSON object (i.e., '{' or '}')
// - a start or end delimiter for a JSON array (i.e., '[' or ']') // - a begin or end delimiter for a JSON array (i.e., '[' or ']')
// //
// A JSON token is represented by the [Token] type in Go. Technically, // A JSON token is represented by the [Token] type in Go. Technically,
// there are two additional structural characters (i.e., ':' and ','), // there are two additional structural characters (i.e., ':' and ','),

View file

@ -713,7 +713,7 @@ func (e *encoderState) reformatValue(dst []byte, src Value, depth int) ([]byte,
// appends it to the end of src, reformatting whitespace and strings as needed. // appends it to the end of src, reformatting whitespace and strings as needed.
// It returns the extended dst buffer and the number of consumed input bytes. // It returns the extended dst buffer and the number of consumed input bytes.
func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, int, error) { func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte, int, error) {
// Append object start. // Append object begin.
if len(src) == 0 || src[0] != '{' { if len(src) == 0 || src[0] != '{' {
panic("BUG: reformatObject must be called with a buffer that starts with '{'") panic("BUG: reformatObject must be called with a buffer that starts with '{'")
} else if depth == maxNestingDepth+1 { } else if depth == maxNestingDepth+1 {
@ -824,7 +824,7 @@ func (e *encoderState) reformatObject(dst []byte, src Value, depth int) ([]byte,
// appends it to the end of dst, reformatting whitespace and strings as needed. // appends it to the end of dst, reformatting whitespace and strings as needed.
// It returns the extended dst buffer and the number of consumed input bytes. // It returns the extended dst buffer and the number of consumed input bytes.
func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, int, error) { func (e *encoderState) reformatArray(dst []byte, src Value, depth int) ([]byte, int, error) {
// Append array start. // Append array begin.
if len(src) == 0 || src[0] != '[' { if len(src) == 0 || src[0] != '[' {
panic("BUG: reformatArray must be called with a buffer that starts with '['") panic("BUG: reformatArray must be called with a buffer that starts with '['")
} else if depth == maxNestingDepth+1 { } else if depth == maxNestingDepth+1 {

View file

@ -297,7 +297,7 @@ func (m *stateMachine) appendNumber() error {
return m.appendLiteral() return m.appendLiteral()
} }
// pushObject appends a JSON start object token as next in the sequence. // pushObject appends a JSON begin object token as next in the sequence.
// If an error is returned, the state is not mutated. // If an error is returned, the state is not mutated.
func (m *stateMachine) pushObject() error { func (m *stateMachine) pushObject() error {
switch { switch {
@ -332,7 +332,7 @@ func (m *stateMachine) popObject() error {
} }
} }
// pushArray appends a JSON start array token as next in the sequence. // pushArray appends a JSON begin array token as next in the sequence.
// If an error is returned, the state is not mutated. // If an error is returned, the state is not mutated.
func (m *stateMachine) pushArray() error { func (m *stateMachine) pushArray() error {
switch { switch {

View file

@ -33,8 +33,8 @@ var errInvalidToken = errors.New("invalid jsontext.Token")
// - a JSON literal (i.e., null, true, or false) // - a JSON literal (i.e., null, true, or false)
// - a JSON string (e.g., "hello, world!") // - a JSON string (e.g., "hello, world!")
// - a JSON number (e.g., 123.456) // - a JSON number (e.g., 123.456)
// - a start or end delimiter for a JSON object (i.e., { or } ) // - a begin or end delimiter for a JSON object (i.e., { or } )
// - a start or end delimiter for a JSON array (i.e., [ or ] ) // - a begin or end delimiter for a JSON array (i.e., [ or ] )
// //
// A Token cannot represent entire array or object values, while a [Value] can. // A Token cannot represent entire array or object values, while a [Value] can.
// There is no Token to represent commas and colons since // There is no Token to represent commas and colons since
@ -481,9 +481,9 @@ func (t Token) Kind() Kind {
// - 't': true // - 't': true
// - '"': string // - '"': string
// - '0': number // - '0': number
// - '{': object start // - '{': object begin
// - '}': object end // - '}': object end
// - '[': array start // - '[': array begin
// - ']': array end // - ']': array end
// //
// An invalid kind is usually represented using 0, // An invalid kind is usually represented using 0,

Binary file not shown.

View file

@ -147,17 +147,23 @@ var export = jsontext.Internal.Export(&internal.AllowInternalUse)
// If the format matches one of the format constants declared // If the format matches one of the format constants declared
// in the time package (e.g., RFC1123), then that format is used. // in the time package (e.g., RFC1123), then that format is used.
// If the format is "unix", "unixmilli", "unixmicro", or "unixnano", // If the format is "unix", "unixmilli", "unixmicro", or "unixnano",
// then the timestamp is encoded as a JSON number of the number of seconds // then the timestamp is encoded as a possibly fractional JSON number
// (or milliseconds, microseconds, or nanoseconds) since the Unix epoch, // of the number of seconds (or milliseconds, microseconds, or nanoseconds)
// which is January 1st, 1970 at 00:00:00 UTC. // since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC.
// To avoid a fractional component, round the timestamp to the relevant unit.
// Otherwise, the format is used as-is with [time.Time.Format] if non-empty. // Otherwise, the format is used as-is with [time.Time.Format] if non-empty.
// //
// - A Go [time.Duration] is encoded as a JSON string containing the duration // - A Go [time.Duration] currently has no default representation and
// formatted according to [time.Duration.String]. // requires an explicit format to be specified.
// If the format is "sec", "milli", "micro", or "nano", // If the format is "sec", "milli", "micro", or "nano",
// then the duration is encoded as a JSON number of the number of seconds // then the duration is encoded as a possibly fractional JSON number
// (or milliseconds, microseconds, or nanoseconds) in the duration. // of the number of seconds (or milliseconds, microseconds, or nanoseconds).
// If the format is "units", it uses [time.Duration.String]. // To avoid a fractional component, round the duration to the relevant unit.
// If the format is "units", it is encoded as a JSON string formatted using
// [time.Duration.String] (e.g., "1h30m" for 1 hour 30 minutes).
// If the format is "iso8601", it is encoded as a JSON string using the
// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes)
// using only accurate units of hours, minutes, and seconds.
// //
// - All other Go types (e.g., complex numbers, channels, and functions) // - All other Go types (e.g., complex numbers, channels, and functions)
// have no default representation and result in a [SemanticError]. // have no default representation and result in a [SemanticError].
@ -375,17 +381,21 @@ func marshalEncode(out *jsontext.Encoder, in any, mo *jsonopts.Struct) (err erro
// If the format matches one of the format constants declared in // If the format matches one of the format constants declared in
// the time package (e.g., RFC1123), then that format is used for parsing. // the time package (e.g., RFC1123), then that format is used for parsing.
// If the format is "unix", "unixmilli", "unixmicro", or "unixnano", // If the format is "unix", "unixmilli", "unixmicro", or "unixnano",
// then the timestamp is decoded from a JSON number of the number of seconds // then the timestamp is decoded from an optionally fractional JSON number
// (or milliseconds, microseconds, or nanoseconds) since the Unix epoch, // of the number of seconds (or milliseconds, microseconds, or nanoseconds)
// which is January 1st, 1970 at 00:00:00 UTC. // since the Unix epoch, which is January 1st, 1970 at 00:00:00 UTC.
// Otherwise, the format is used as-is with [time.Time.Parse] if non-empty. // Otherwise, the format is used as-is with [time.Time.Parse] if non-empty.
// //
// - A Go [time.Duration] is decoded from a JSON string by // - A Go [time.Duration] currently has no default representation and
// passing the decoded string to [time.ParseDuration]. // requires an explicit format to be specified.
// If the format is "sec", "milli", "micro", or "nano", // If the format is "sec", "milli", "micro", or "nano",
// then the duration is decoded from a JSON number of the number of seconds // then the duration is decoded from an optionally fractional JSON number
// (or milliseconds, microseconds, or nanoseconds) in the duration. // of the number of seconds (or milliseconds, microseconds, or nanoseconds).
// If the format is "units", it uses [time.ParseDuration]. // If the format is "units", it is decoded from a JSON string parsed using
// [time.ParseDuration] (e.g., "1h30m" for 1 hour 30 minutes).
// If the format is "iso8601", it is decoded from a JSON string using the
// ISO 8601 standard for durations (e.g., "PT1H30M" for 1 hour 30 minutes)
// accepting only accurate units of hours, minutes, or seconds.
// //
// - All other Go types (e.g., complex numbers, channels, and functions) // - All other Go types (e.g., complex numbers, channels, and functions)
// have no default representation and result in a [SemanticError]. // have no default representation and result in a [SemanticError].

View file

@ -365,7 +365,7 @@ type (
Interface any `json:",omitzero,format:invalid"` Interface any `json:",omitzero,format:invalid"`
} }
structDurationFormat struct { structDurationFormat struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:units"` D2 time.Duration `json:",format:units"`
D3 time.Duration `json:",format:sec"` D3 time.Duration `json:",format:sec"`
D4 time.Duration `json:",string,format:sec"` D4 time.Duration `json:",string,format:sec"`
@ -375,6 +375,7 @@ type (
D8 time.Duration `json:",string,format:micro"` D8 time.Duration `json:",string,format:micro"`
D9 time.Duration `json:",format:nano"` D9 time.Duration `json:",format:nano"`
D10 time.Duration `json:",string,format:nano"` D10 time.Duration `json:",string,format:nano"`
D11 time.Duration `json:",format:iso8601"`
} }
structTimeFormat struct { structTimeFormat struct {
T1 time.Time T1 time.Time
@ -4312,14 +4313,14 @@ func TestMarshal(t *testing.T) {
}, { }, {
name: jsontest.Name("Duration/Zero"), name: jsontest.Name("Duration/Zero"),
in: struct { in: struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{0, 0}, }{0, 0},
want: `{"D1":"0s","D2":0}`, want: `{"D1":"0s","D2":0}`,
}, { }, {
name: jsontest.Name("Duration/Positive"), name: jsontest.Name("Duration/Positive"),
in: struct { in: struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{ }{
123456789123456789, 123456789123456789,
@ -4329,7 +4330,7 @@ func TestMarshal(t *testing.T) {
}, { }, {
name: jsontest.Name("Duration/Negative"), name: jsontest.Name("Duration/Negative"),
in: struct { in: struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{ }{
-123456789123456789, -123456789123456789,
@ -4356,11 +4357,12 @@ func TestMarshal(t *testing.T) {
want: `{"D"`, want: `{"D"`,
wantErr: EM(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType(0, T[time.Duration]()), wantErr: EM(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType(0, T[time.Duration]()),
}, { }, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/IgnoreInvalidFormat"), name: jsontest.Name("Duration/IgnoreInvalidFormat"),
opts: []Options{invalidFormatOption}, opts: []Options{invalidFormatOption},
in: time.Duration(0), in: time.Duration(0),
want: `"0s"`, want: `"0s"`,
}, { }, { */
name: jsontest.Name("Duration/Format"), name: jsontest.Name("Duration/Format"),
opts: []Options{jsontext.Multiline(true)}, opts: []Options{jsontext.Multiline(true)},
in: structDurationFormat{ in: structDurationFormat{
@ -4374,6 +4376,7 @@ func TestMarshal(t *testing.T) {
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
}, },
want: `{ want: `{
"D1": "12h34m56.078090012s", "D1": "12h34m56.078090012s",
@ -4385,21 +4388,24 @@ func TestMarshal(t *testing.T) {
"D7": 45296078090.012, "D7": 45296078090.012,
"D8": "45296078090.012", "D8": "45296078090.012",
"D9": 45296078090012, "D9": 45296078090012,
"D10": "45296078090012" "D10": "45296078090012",
"D11": "PT12H34M56.078090012S"
}`, }`,
}, { }, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/Format/Legacy"), name: jsontest.Name("Duration/Format/Legacy"),
opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1},
in: structDurationFormat{ in: structDurationFormat{
D1: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, D1: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
D2: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, D2: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
}, },
want: `{"D1":45296078090012,"D2":"12h34m56.078090012s","D3":0,"D4":"0","D5":0,"D6":"0","D7":0,"D8":"0","D9":0,"D10":"0"}`, want: `{"D1":45296078090012,"D2":"12h34m56.078090012s","D3":0,"D4":"0","D5":0,"D6":"0","D7":0,"D8":"0","D9":0,"D10":"0","D11":"PT0S"}`,
}, { }, { */
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/MapKey"), name: jsontest.Name("Duration/MapKey"),
in: map[time.Duration]string{time.Second: ""}, in: map[time.Duration]string{time.Second: ""},
want: `{"1s":""}`, want: `{"1s":""}`,
}, { }, { */
name: jsontest.Name("Duration/MapKey/Legacy"), name: jsontest.Name("Duration/MapKey/Legacy"),
opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1},
in: map[time.Duration]string{time.Second: ""}, in: map[time.Duration]string{time.Second: ""},
@ -8713,33 +8719,33 @@ func TestUnmarshal(t *testing.T) {
name: jsontest.Name("Duration/Null"), name: jsontest.Name("Duration/Null"),
inBuf: `{"D1":null,"D2":null}`, inBuf: `{"D1":null,"D2":null}`,
inVal: addr(struct { inVal: addr(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{1, 1}), }{1, 1}),
want: addr(struct { want: addr(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{0, 0}), }{0, 0}),
}, { }, {
name: jsontest.Name("Duration/Zero"), name: jsontest.Name("Duration/Zero"),
inBuf: `{"D1":"0s","D2":0}`, inBuf: `{"D1":"0s","D2":0}`,
inVal: addr(struct { inVal: addr(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{1, 1}), }{1, 1}),
want: addr(struct { want: addr(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{0, 0}), }{0, 0}),
}, { }, {
name: jsontest.Name("Duration/Positive"), name: jsontest.Name("Duration/Positive"),
inBuf: `{"D1":"34293h33m9.123456789s","D2":123456789123456789}`, inBuf: `{"D1":"34293h33m9.123456789s","D2":123456789123456789}`,
inVal: new(struct { inVal: new(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}), }),
want: addr(struct { want: addr(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{ }{
123456789123456789, 123456789123456789,
@ -8749,11 +8755,11 @@ func TestUnmarshal(t *testing.T) {
name: jsontest.Name("Duration/Negative"), name: jsontest.Name("Duration/Negative"),
inBuf: `{"D1":"-34293h33m9.123456789s","D2":-123456789123456789}`, inBuf: `{"D1":"-34293h33m9.123456789s","D2":-123456789123456789}`,
inVal: new(struct { inVal: new(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}), }),
want: addr(struct { want: addr(struct {
D1 time.Duration D1 time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
D2 time.Duration `json:",format:nano"` D2 time.Duration `json:",format:nano"`
}{ }{
-123456789123456789, -123456789123456789,
@ -8801,20 +8807,20 @@ func TestUnmarshal(t *testing.T) {
name: jsontest.Name("Duration/String/Mismatch"), name: jsontest.Name("Duration/String/Mismatch"),
inBuf: `{"D":-123456789123456789}`, inBuf: `{"D":-123456789123456789}`,
inVal: addr(struct { inVal: addr(struct {
D time.Duration D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}), }{1}),
want: addr(struct { want: addr(struct {
D time.Duration D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}), }{1}),
wantErr: EU(nil).withPos(`{"D":`, "/D").withType('0', timeDurationType), wantErr: EU(nil).withPos(`{"D":`, "/D").withType('0', timeDurationType),
}, { }, {
name: jsontest.Name("Duration/String/Invalid"), name: jsontest.Name("Duration/String/Invalid"),
inBuf: `{"D":"5minkutes"}`, inBuf: `{"D":"5minkutes"}`,
inVal: addr(struct { inVal: addr(struct {
D time.Duration D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}), }{1}),
want: addr(struct { want: addr(struct {
D time.Duration D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}), }{1}),
wantErr: EU(func() error { wantErr: EU(func() error {
_, err := time.ParseDuration("5minkutes") _, err := time.ParseDuration("5minkutes")
@ -8824,12 +8830,41 @@ func TestUnmarshal(t *testing.T) {
name: jsontest.Name("Duration/Syntax/Invalid"), name: jsontest.Name("Duration/Syntax/Invalid"),
inBuf: `{"D":x}`, inBuf: `{"D":x}`,
inVal: addr(struct { inVal: addr(struct {
D time.Duration D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}), }{1}),
want: addr(struct { want: addr(struct {
D time.Duration D time.Duration `json:",format:units"` // TODO(https://go.dev/issue/71631): Remove the format flag.
}{1}), }{1}),
wantErr: newInvalidCharacterError("x", "at start of value", len64(`{"D":`), "/D"), wantErr: newInvalidCharacterError("x", "at start of value", len64(`{"D":`), "/D"),
}, {
name: jsontest.Name("Duration/Format"),
inBuf: `{
"D1": "12h34m56.078090012s",
"D2": "12h34m56.078090012s",
"D3": 45296.078090012,
"D4": "45296.078090012",
"D5": 45296078.090012,
"D6": "45296078.090012",
"D7": 45296078090.012,
"D8": "45296078090.012",
"D9": 45296078090012,
"D10": "45296078090012",
"D11": "PT12H34M56.078090012S"
}`,
inVal: new(structDurationFormat),
want: addr(structDurationFormat{
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
}),
}, { }, {
name: jsontest.Name("Duration/Format/Invalid"), name: jsontest.Name("Duration/Format/Invalid"),
inBuf: `{"D":"0s"}`, inBuf: `{"D":"0s"}`,
@ -8841,6 +8876,7 @@ func TestUnmarshal(t *testing.T) {
}{1}), }{1}),
wantErr: EU(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType(0, timeDurationType), wantErr: EU(errInvalidFormatFlag).withPos(`{"D":`, "/D").withType(0, timeDurationType),
}, { }, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/Format/Legacy"), name: jsontest.Name("Duration/Format/Legacy"),
inBuf: `{"D1":45296078090012,"D2":"12h34m56.078090012s"}`, inBuf: `{"D1":45296078090012,"D2":"12h34m56.078090012s"}`,
opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1},
@ -8849,24 +8885,26 @@ func TestUnmarshal(t *testing.T) {
D1: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, D1: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
D2: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond, D2: 12*time.Hour + 34*time.Minute + 56*time.Second + 78*time.Millisecond + 90*time.Microsecond + 12*time.Nanosecond,
}), }),
}, { }, { */
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/MapKey"), name: jsontest.Name("Duration/MapKey"),
inBuf: `{"1s":""}`, inBuf: `{"1s":""}`,
inVal: new(map[time.Duration]string), inVal: new(map[time.Duration]string),
want: addr(map[time.Duration]string{time.Second: ""}), want: addr(map[time.Duration]string{time.Second: ""}),
}, { }, { */
name: jsontest.Name("Duration/MapKey/Legacy"), name: jsontest.Name("Duration/MapKey/Legacy"),
opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1}, opts: []Options{jsonflags.FormatTimeWithLegacySemantics | 1},
inBuf: `{"1000000000":""}`, inBuf: `{"1000000000":""}`,
inVal: new(map[time.Duration]string), inVal: new(map[time.Duration]string),
want: addr(map[time.Duration]string{time.Second: ""}), want: addr(map[time.Duration]string{time.Second: ""}),
}, { }, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: jsontest.Name("Duration/IgnoreInvalidFormat"), name: jsontest.Name("Duration/IgnoreInvalidFormat"),
opts: []Options{invalidFormatOption}, opts: []Options{invalidFormatOption},
inBuf: `"1s"`, inBuf: `"1s"`,
inVal: addr(time.Duration(0)), inVal: addr(time.Duration(0)),
want: addr(time.Second), want: addr(time.Second),
}, { }, { */
name: jsontest.Name("Time/Zero"), name: jsontest.Name("Time/Zero"),
inBuf: `{"T1":"0001-01-01T00:00:00Z","T2":"01 Jan 01 00:00 UTC","T3":"0001-01-01","T4":"0001-01-01T00:00:00Z","T5":"0001-01-01T00:00:00Z"}`, inBuf: `{"T1":"0001-01-01T00:00:00Z","T2":"01 Jan 01 00:00 UTC","T3":"0001-01-01","T4":"0001-01-01T00:00:00Z","T5":"0001-01-01T00:00:00Z"}`,
inVal: new(struct { inVal: new(struct {

View file

@ -52,6 +52,9 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler {
} }
} else if mo.Flags.Get(jsonflags.FormatTimeWithLegacySemantics) { } else if mo.Flags.Get(jsonflags.FormatTimeWithLegacySemantics) {
return marshalNano(enc, va, mo) return marshalNano(enc, va, mo)
} else {
// TODO(https://go.dev/issue/71631): Decide on default duration representation.
return newMarshalErrorBefore(enc, t, errors.New("no default representation (see https://go.dev/issue/71631); specify an explicit format"))
} }
// TODO(https://go.dev/issue/62121): Use reflect.Value.AssertTo. // TODO(https://go.dev/issue/62121): Use reflect.Value.AssertTo.
@ -75,6 +78,9 @@ func makeTimeArshaler(fncs *arshaler, t reflect.Type) *arshaler {
} }
} else if uo.Flags.Get(jsonflags.FormatTimeWithLegacySemantics) { } else if uo.Flags.Get(jsonflags.FormatTimeWithLegacySemantics) {
return unmarshalNano(dec, va, uo) return unmarshalNano(dec, va, uo)
} else {
// TODO(https://go.dev/issue/71631): Decide on default duration representation.
return newUnmarshalErrorBeforeWithSkipping(dec, uo, t, errors.New("no default representation (see https://go.dev/issue/71631); specify an explicit format"))
} }
stringify := !u.isNumeric() || xd.Tokens.Last.NeedObjectName() || uo.Flags.Get(jsonflags.StringifyNumbers) stringify := !u.isNumeric() || xd.Tokens.Last.NeedObjectName() || uo.Flags.Get(jsonflags.StringifyNumbers)
@ -200,6 +206,7 @@ type durationArshaler struct {
// - 0 uses time.Duration.String // - 0 uses time.Duration.String
// - 1e0, 1e3, 1e6, or 1e9 use a decimal encoding of the duration as // - 1e0, 1e3, 1e6, or 1e9 use a decimal encoding of the duration as
// nanoseconds, microseconds, milliseconds, or seconds. // nanoseconds, microseconds, milliseconds, or seconds.
// - 8601 uses ISO 8601
base uint64 base uint64
} }
@ -215,6 +222,8 @@ func (a *durationArshaler) initFormat(format string) (ok bool) {
a.base = 1e3 a.base = 1e3
case "nano": case "nano":
a.base = 1e0 a.base = 1e0
case "iso8601":
a.base = 8601
default: default:
return false return false
} }
@ -222,13 +231,15 @@ func (a *durationArshaler) initFormat(format string) (ok bool) {
} }
func (a *durationArshaler) isNumeric() bool { func (a *durationArshaler) isNumeric() bool {
return a.base != 0 && a.base != 60 return a.base != 0 && a.base != 8601
} }
func (a *durationArshaler) appendMarshal(b []byte) ([]byte, error) { func (a *durationArshaler) appendMarshal(b []byte) ([]byte, error) {
switch a.base { switch a.base {
case 0: case 0:
return append(b, a.td.String()...), nil return append(b, a.td.String()...), nil
case 8601:
return appendDurationISO8601(b, a.td), nil
default: default:
return appendDurationBase10(b, a.td, a.base), nil return appendDurationBase10(b, a.td, a.base), nil
} }
@ -238,6 +249,8 @@ func (a *durationArshaler) unmarshal(b []byte) (err error) {
switch a.base { switch a.base {
case 0: case 0:
a.td, err = time.ParseDuration(string(b)) a.td, err = time.ParseDuration(string(b))
case 8601:
a.td, err = parseDurationISO8601(b)
default: default:
a.td, err = parseDurationBase10(b, a.base) a.td, err = parseDurationBase10(b, a.base)
} }
@ -412,7 +425,7 @@ func appendDurationBase10(b []byte, d time.Duration, pow10 uint64) []byte {
// parseDurationBase10 parses d from a decimal fractional number, // parseDurationBase10 parses d from a decimal fractional number,
// where pow10 is a power-of-10 used to scale up the number. // where pow10 is a power-of-10 used to scale up the number.
func parseDurationBase10(b []byte, pow10 uint64) (time.Duration, error) { func parseDurationBase10(b []byte, pow10 uint64) (time.Duration, error) {
suffix, neg := consumeSign(b) // consume sign suffix, neg := consumeSign(b, false) // consume sign
wholeBytes, fracBytes := bytesCutByte(suffix, '.', true) // consume whole and frac fields wholeBytes, fracBytes := bytesCutByte(suffix, '.', true) // consume whole and frac fields
whole, okWhole := jsonwire.ParseUint(wholeBytes) // parse whole field; may overflow whole, okWhole := jsonwire.ParseUint(wholeBytes) // parse whole field; may overflow
frac, okFrac := parseFracBase10(fracBytes, pow10) // parse frac field frac, okFrac := parseFracBase10(fracBytes, pow10) // parse frac field
@ -428,6 +441,166 @@ func parseDurationBase10(b []byte, pow10 uint64) (time.Duration, error) {
} }
} }
// appendDurationISO8601 appends an ISO 8601 duration with a restricted grammar,
// where leading and trailing zeroes and zero-value designators are omitted.
// It only uses hour, minute, and second designators since ISO 8601 defines
// those as being "accurate", while year, month, week, and day are "nominal".
func appendDurationISO8601(b []byte, d time.Duration) []byte {
if d == 0 {
return append(b, "PT0S"...)
}
b, n := mayAppendDurationSign(b, d)
b = append(b, "PT"...)
n, nsec := bits.Div64(0, n, 1e9) // compute nsec field
n, sec := bits.Div64(0, n, 60) // compute sec field
hour, min := bits.Div64(0, n, 60) // compute hour and min fields
if hour > 0 {
b = append(strconv.AppendUint(b, hour, 10), 'H')
}
if min > 0 {
b = append(strconv.AppendUint(b, min, 10), 'M')
}
if sec > 0 || nsec > 0 {
b = append(appendFracBase10(strconv.AppendUint(b, sec, 10), nsec, 1e9), 'S')
}
return b
}
// daysPerYear is the exact average number of days in a year according to
// the Gregorian calender, which has an extra day each year that is
// a multiple of 4, unless it is evenly divisible by 100 but not by 400.
// This does not take into account leap seconds, which are not deterministic.
const daysPerYear = 365.2425
var errInaccurateDateUnits = errors.New("inaccurate year, month, week, or day units")
// parseDurationISO8601 parses a duration according to ISO 8601-1:2019,
// section 5.5.2.2 and 5.5.2.3 with the following restrictions or extensions:
//
// - A leading minus sign is permitted for negative duration according
// to ISO 8601-2:2019, section 4.4.1.9. We do not permit negative values
// for each "time scale component", which is permitted by section 4.4.1.1,
// but rarely supported by parsers.
//
// - A leading plus sign is permitted (and ignored).
// This is not required by ISO 8601, but not forbidden either.
// There is some precedent for this as it is supported by the principle of
// duration arithmetic as specified in ISO 8601-2-2019, section 14.1.
// Of note, the JavaScript grammar for ISO 8601 permits a leading plus sign.
//
// - A fractional value is only permitted for accurate units
// (i.e., hour, minute, and seconds) in the last time component,
// which is permissible by ISO 8601-1:2019, section 5.5.2.3.
//
// - Both periods ('.') and commas (',') are supported as the separator
// between the integer part and fraction part of a number,
// as specified in ISO 8601-1:2019, section 3.2.6.
// While ISO 8601 recommends comma as the default separator,
// most formatters uses a period.
//
// - Leading zeros are ignored. This is not required by ISO 8601,
// but also not forbidden by the standard. Many parsers support this.
//
// - Lowercase designators are supported. This is not required by ISO 8601,
// but also not forbidden by the standard. Many parsers support this.
//
// If the nominal units of year, month, week, or day are present,
// this produces a best-effort value and also reports [errInaccurateDateUnits].
//
// The accepted grammar is identical to JavaScript's Duration:
//
// https://tc39.es/proposal-temporal/#prod-Duration
//
// We follow JavaScript's grammar as JSON itself is derived from JavaScript.
// The Temporal.Duration.toJSON method is guaranteed to produce an output
// that can be parsed by this function so long as arithmetic in JavaScript
// do not use a largestUnit value higher than "hours" (which is the default).
// Even if it does, this will do a best-effort parsing with inaccurate units,
// but report [errInaccurateDateUnits].
func parseDurationISO8601(b []byte) (time.Duration, error) {
var invalid, overflow, inaccurate, sawFrac bool
var sumNanos, n, co uint64
// cutBytes is like [bytes.Cut], but uses either c0 or c1 as the separator.
cutBytes := func(b []byte, c0, c1 byte) (prefix, suffix []byte, ok bool) {
for i, c := range b {
if c == c0 || c == c1 {
return b[:i], b[i+1:], true
}
}
return b, nil, false
}
// mayParseUnit attempts to parse another date or time number
// identified by the desHi and desLo unit characters.
// If the part is absent for current unit, it returns b as is.
mayParseUnit := func(b []byte, desHi, desLo byte, unit time.Duration) []byte {
number, suffix, ok := cutBytes(b, desHi, desLo)
if !ok || sawFrac {
return b // designator is not present or already saw fraction, which can only be in the last component
}
// Parse the number.
// A fraction allowed for the accurate units in the last part.
whole, frac, ok := cutBytes(number, '.', ',')
if ok {
sawFrac = true
invalid = invalid || len(frac) == len("") || unit > time.Hour
if unit == time.Second {
n, ok = parsePaddedBase10(frac, uint64(time.Second))
invalid = invalid || !ok
} else {
f, err := strconv.ParseFloat("0."+string(frac), 64)
invalid = invalid || err != nil || len(bytes.Trim(frac[len("."):], "0123456789")) > 0
n = uint64(math.Round(f * float64(unit))) // never overflows since f is within [0..1]
}
sumNanos, co = bits.Add64(sumNanos, n, 0) // overflow if co > 0
overflow = overflow || co > 0
}
for len(whole) > 1 && whole[0] == '0' {
whole = whole[len("0"):] // trim leading zeros
}
n, ok := jsonwire.ParseUint(whole) // overflow if !ok && MaxUint64
hi, lo := bits.Mul64(n, uint64(unit)) // overflow if hi > 0
sumNanos, co = bits.Add64(sumNanos, lo, 0) // overflow if co > 0
invalid = invalid || (!ok && n != math.MaxUint64)
overflow = overflow || (!ok && n == math.MaxUint64) || hi > 0 || co > 0
inaccurate = inaccurate || unit > time.Hour
return suffix
}
suffix, neg := consumeSign(b, true)
prefix, suffix, okP := cutBytes(suffix, 'P', 'p')
durDate, durTime, okT := cutBytes(suffix, 'T', 't')
invalid = invalid || len(prefix) > 0 || !okP || (okT && len(durTime) == 0) || len(durDate)+len(durTime) == 0
if len(durDate) > 0 { // nominal portion of the duration
durDate = mayParseUnit(durDate, 'Y', 'y', time.Duration(daysPerYear*24*60*60*1e9))
durDate = mayParseUnit(durDate, 'M', 'm', time.Duration(daysPerYear/12*24*60*60*1e9))
durDate = mayParseUnit(durDate, 'W', 'w', time.Duration(7*24*60*60*1e9))
durDate = mayParseUnit(durDate, 'D', 'd', time.Duration(24*60*60*1e9))
invalid = invalid || len(durDate) > 0 // unknown elements
}
if len(durTime) > 0 { // accurate portion of the duration
durTime = mayParseUnit(durTime, 'H', 'h', time.Duration(60*60*1e9))
durTime = mayParseUnit(durTime, 'M', 'm', time.Duration(60*1e9))
durTime = mayParseUnit(durTime, 'S', 's', time.Duration(1e9))
invalid = invalid || len(durTime) > 0 // unknown elements
}
d := mayApplyDurationSign(sumNanos, neg)
overflow = overflow || (neg != (d < 0) && d != 0) // overflows signed duration
switch {
case invalid:
return 0, fmt.Errorf("invalid ISO 8601 duration %q: %w", b, strconv.ErrSyntax)
case overflow:
return 0, fmt.Errorf("invalid ISO 8601 duration %q: %w", b, strconv.ErrRange)
case inaccurate:
return d, fmt.Errorf("invalid ISO 8601 duration %q: %w", b, errInaccurateDateUnits)
default:
return d, nil
}
}
// mayAppendDurationSign appends a negative sign if n is negative. // mayAppendDurationSign appends a negative sign if n is negative.
func mayAppendDurationSign(b []byte, d time.Duration) ([]byte, uint64) { func mayAppendDurationSign(b []byte, d time.Duration) ([]byte, uint64) {
if d < 0 { if d < 0 {
@ -471,7 +644,7 @@ func appendTimeUnix(b []byte, t time.Time, pow10 uint64) []byte {
// parseTimeUnix parses t formatted as a decimal fractional number, // parseTimeUnix parses t formatted as a decimal fractional number,
// where pow10 is a power-of-10 used to scale down the number. // where pow10 is a power-of-10 used to scale down the number.
func parseTimeUnix(b []byte, pow10 uint64) (time.Time, error) { func parseTimeUnix(b []byte, pow10 uint64) (time.Time, error) {
suffix, neg := consumeSign(b) // consume sign suffix, neg := consumeSign(b, false) // consume sign
wholeBytes, fracBytes := bytesCutByte(suffix, '.', true) // consume whole and frac fields wholeBytes, fracBytes := bytesCutByte(suffix, '.', true) // consume whole and frac fields
whole, okWhole := jsonwire.ParseUint(wholeBytes) // parse whole field; may overflow whole, okWhole := jsonwire.ParseUint(wholeBytes) // parse whole field; may overflow
frac, okFrac := parseFracBase10(fracBytes, 1e9/pow10) // parse frac field frac, okFrac := parseFracBase10(fracBytes, 1e9/pow10) // parse frac field
@ -570,10 +743,14 @@ func parsePaddedBase10(b []byte, max10 uint64) (n uint64, ok bool) {
return n, true return n, true
} }
// consumeSign consumes an optional leading negative sign. // consumeSign consumes an optional leading negative or positive sign.
func consumeSign(b []byte) ([]byte, bool) { func consumeSign(b []byte, allowPlus bool) ([]byte, bool) {
if len(b) > 0 && b[0] == '-' { if len(b) > 0 {
if b[0] == '-' {
return b[len("-"):], true return b[len("-"):], true
} else if b[0] == '+' && allowPlus {
return b[len("+"):], false
}
} }
return b, false return b, false
} }

View file

@ -7,8 +7,10 @@
package json package json
import ( import (
"errors"
"fmt" "fmt"
"math" "math"
"strconv"
"testing" "testing"
"time" "time"
@ -28,63 +30,67 @@ var formatDurationTestdata = []struct {
base10Milli string base10Milli string
base10Micro string base10Micro string
base10Nano string base10Nano string
iso8601 string
}{ }{
{math.MaxInt64, "9223372036.854775807", "9223372036854.775807", "9223372036854775.807", "9223372036854775807"}, {math.MaxInt64, "9223372036.854775807", "9223372036854.775807", "9223372036854775.807", "9223372036854775807", "PT2562047H47M16.854775807S"},
{1e12 + 1e12, "2000", "2000000", "2000000000", "2000000000000"}, {123*time.Hour + 4*time.Minute + 56*time.Second, "443096", "443096000", "443096000000", "443096000000000", "PT123H4M56S"},
{1e12 + 1e11, "1100", "1100000", "1100000000", "1100000000000"}, {time.Hour, "3600", "3600000", "3600000000", "3600000000000", "PT1H"},
{1e12 + 1e10, "1010", "1010000", "1010000000", "1010000000000"}, {time.Minute, "60", "60000", "60000000", "60000000000", "PT1M"},
{1e12 + 1e9, "1001", "1001000", "1001000000", "1001000000000"}, {1e12 + 1e12, "2000", "2000000", "2000000000", "2000000000000", "PT33M20S"},
{1e12 + 1e8, "1000.1", "1000100", "1000100000", "1000100000000"}, {1e12 + 1e11, "1100", "1100000", "1100000000", "1100000000000", "PT18M20S"},
{1e12 + 1e7, "1000.01", "1000010", "1000010000", "1000010000000"}, {1e12 + 1e10, "1010", "1010000", "1010000000", "1010000000000", "PT16M50S"},
{1e12 + 1e6, "1000.001", "1000001", "1000001000", "1000001000000"}, {1e12 + 1e9, "1001", "1001000", "1001000000", "1001000000000", "PT16M41S"},
{1e12 + 1e5, "1000.0001", "1000000.1", "1000000100", "1000000100000"}, {1e12 + 1e8, "1000.1", "1000100", "1000100000", "1000100000000", "PT16M40.1S"},
{1e12 + 1e4, "1000.00001", "1000000.01", "1000000010", "1000000010000"}, {1e12 + 1e7, "1000.01", "1000010", "1000010000", "1000010000000", "PT16M40.01S"},
{1e12 + 1e3, "1000.000001", "1000000.001", "1000000001", "1000000001000"}, {1e12 + 1e6, "1000.001", "1000001", "1000001000", "1000001000000", "PT16M40.001S"},
{1e12 + 1e2, "1000.0000001", "1000000.0001", "1000000000.1", "1000000000100"}, {1e12 + 1e5, "1000.0001", "1000000.1", "1000000100", "1000000100000", "PT16M40.0001S"},
{1e12 + 1e1, "1000.00000001", "1000000.00001", "1000000000.01", "1000000000010"}, {1e12 + 1e4, "1000.00001", "1000000.01", "1000000010", "1000000010000", "PT16M40.00001S"},
{1e12 + 1e0, "1000.000000001", "1000000.000001", "1000000000.001", "1000000000001"}, {1e12 + 1e3, "1000.000001", "1000000.001", "1000000001", "1000000001000", "PT16M40.000001S"},
{+(1e9 + 1), "1.000000001", "1000.000001", "1000000.001", "1000000001"}, {1e12 + 1e2, "1000.0000001", "1000000.0001", "1000000000.1", "1000000000100", "PT16M40.0000001S"},
{+(1e9), "1", "1000", "1000000", "1000000000"}, {1e12 + 1e1, "1000.00000001", "1000000.00001", "1000000000.01", "1000000000010", "PT16M40.00000001S"},
{+(1e9 - 1), "0.999999999", "999.999999", "999999.999", "999999999"}, {1e12 + 1e0, "1000.000000001", "1000000.000001", "1000000000.001", "1000000000001", "PT16M40.000000001S"},
{+100000000, "0.1", "100", "100000", "100000000"}, {+(1e9 + 1), "1.000000001", "1000.000001", "1000000.001", "1000000001", "PT1.000000001S"},
{+120000000, "0.12", "120", "120000", "120000000"}, {+(1e9), "1", "1000", "1000000", "1000000000", "PT1S"},
{+123000000, "0.123", "123", "123000", "123000000"}, {+(1e9 - 1), "0.999999999", "999.999999", "999999.999", "999999999", "PT0.999999999S"},
{+123400000, "0.1234", "123.4", "123400", "123400000"}, {+100000000, "0.1", "100", "100000", "100000000", "PT0.1S"},
{+123450000, "0.12345", "123.45", "123450", "123450000"}, {+120000000, "0.12", "120", "120000", "120000000", "PT0.12S"},
{+123456000, "0.123456", "123.456", "123456", "123456000"}, {+123000000, "0.123", "123", "123000", "123000000", "PT0.123S"},
{+123456700, "0.1234567", "123.4567", "123456.7", "123456700"}, {+123400000, "0.1234", "123.4", "123400", "123400000", "PT0.1234S"},
{+123456780, "0.12345678", "123.45678", "123456.78", "123456780"}, {+123450000, "0.12345", "123.45", "123450", "123450000", "PT0.12345S"},
{+123456789, "0.123456789", "123.456789", "123456.789", "123456789"}, {+123456000, "0.123456", "123.456", "123456", "123456000", "PT0.123456S"},
{+12345678, "0.012345678", "12.345678", "12345.678", "12345678"}, {+123456700, "0.1234567", "123.4567", "123456.7", "123456700", "PT0.1234567S"},
{+1234567, "0.001234567", "1.234567", "1234.567", "1234567"}, {+123456780, "0.12345678", "123.45678", "123456.78", "123456780", "PT0.12345678S"},
{+123456, "0.000123456", "0.123456", "123.456", "123456"}, {+123456789, "0.123456789", "123.456789", "123456.789", "123456789", "PT0.123456789S"},
{+12345, "0.000012345", "0.012345", "12.345", "12345"}, {+12345678, "0.012345678", "12.345678", "12345.678", "12345678", "PT0.012345678S"},
{+1234, "0.000001234", "0.001234", "1.234", "1234"}, {+1234567, "0.001234567", "1.234567", "1234.567", "1234567", "PT0.001234567S"},
{+123, "0.000000123", "0.000123", "0.123", "123"}, {+123456, "0.000123456", "0.123456", "123.456", "123456", "PT0.000123456S"},
{+12, "0.000000012", "0.000012", "0.012", "12"}, {+12345, "0.000012345", "0.012345", "12.345", "12345", "PT0.000012345S"},
{+1, "0.000000001", "0.000001", "0.001", "1"}, {+1234, "0.000001234", "0.001234", "1.234", "1234", "PT0.000001234S"},
{0, "0", "0", "0", "0"}, {+123, "0.000000123", "0.000123", "0.123", "123", "PT0.000000123S"},
{-1, "-0.000000001", "-0.000001", "-0.001", "-1"}, {+12, "0.000000012", "0.000012", "0.012", "12", "PT0.000000012S"},
{-12, "-0.000000012", "-0.000012", "-0.012", "-12"}, {+1, "0.000000001", "0.000001", "0.001", "1", "PT0.000000001S"},
{-123, "-0.000000123", "-0.000123", "-0.123", "-123"}, {0, "0", "0", "0", "0", "PT0S"},
{-1234, "-0.000001234", "-0.001234", "-1.234", "-1234"}, {-1, "-0.000000001", "-0.000001", "-0.001", "-1", "-PT0.000000001S"},
{-12345, "-0.000012345", "-0.012345", "-12.345", "-12345"}, {-12, "-0.000000012", "-0.000012", "-0.012", "-12", "-PT0.000000012S"},
{-123456, "-0.000123456", "-0.123456", "-123.456", "-123456"}, {-123, "-0.000000123", "-0.000123", "-0.123", "-123", "-PT0.000000123S"},
{-1234567, "-0.001234567", "-1.234567", "-1234.567", "-1234567"}, {-1234, "-0.000001234", "-0.001234", "-1.234", "-1234", "-PT0.000001234S"},
{-12345678, "-0.012345678", "-12.345678", "-12345.678", "-12345678"}, {-12345, "-0.000012345", "-0.012345", "-12.345", "-12345", "-PT0.000012345S"},
{-123456789, "-0.123456789", "-123.456789", "-123456.789", "-123456789"}, {-123456, "-0.000123456", "-0.123456", "-123.456", "-123456", "-PT0.000123456S"},
{-123456780, "-0.12345678", "-123.45678", "-123456.78", "-123456780"}, {-1234567, "-0.001234567", "-1.234567", "-1234.567", "-1234567", "-PT0.001234567S"},
{-123456700, "-0.1234567", "-123.4567", "-123456.7", "-123456700"}, {-12345678, "-0.012345678", "-12.345678", "-12345.678", "-12345678", "-PT0.012345678S"},
{-123456000, "-0.123456", "-123.456", "-123456", "-123456000"}, {-123456789, "-0.123456789", "-123.456789", "-123456.789", "-123456789", "-PT0.123456789S"},
{-123450000, "-0.12345", "-123.45", "-123450", "-123450000"}, {-123456780, "-0.12345678", "-123.45678", "-123456.78", "-123456780", "-PT0.12345678S"},
{-123400000, "-0.1234", "-123.4", "-123400", "-123400000"}, {-123456700, "-0.1234567", "-123.4567", "-123456.7", "-123456700", "-PT0.1234567S"},
{-123000000, "-0.123", "-123", "-123000", "-123000000"}, {-123456000, "-0.123456", "-123.456", "-123456", "-123456000", "-PT0.123456S"},
{-120000000, "-0.12", "-120", "-120000", "-120000000"}, {-123450000, "-0.12345", "-123.45", "-123450", "-123450000", "-PT0.12345S"},
{-100000000, "-0.1", "-100", "-100000", "-100000000"}, {-123400000, "-0.1234", "-123.4", "-123400", "-123400000", "-PT0.1234S"},
{-(1e9 - 1), "-0.999999999", "-999.999999", "-999999.999", "-999999999"}, {-123000000, "-0.123", "-123", "-123000", "-123000000", "-PT0.123S"},
{-(1e9), "-1", "-1000", "-1000000", "-1000000000"}, {-120000000, "-0.12", "-120", "-120000", "-120000000", "-PT0.12S"},
{-(1e9 + 1), "-1.000000001", "-1000.000001", "-1000000.001", "-1000000001"}, {-100000000, "-0.1", "-100", "-100000", "-100000000", "-PT0.1S"},
{math.MinInt64, "-9223372036.854775808", "-9223372036854.775808", "-9223372036854775.808", "-9223372036854775808"}, {-(1e9 - 1), "-0.999999999", "-999.999999", "-999999.999", "-999999999", "-PT0.999999999S"},
{-(1e9), "-1", "-1000", "-1000000", "-1000000000", "-PT1S"},
{-(1e9 + 1), "-1.000000001", "-1000.000001", "-1000000.001", "-1000000001", "-PT1.000000001S"},
{math.MinInt64, "-9223372036.854775808", "-9223372036854.775808", "-9223372036854775.808", "-9223372036854775808", "-PT2562047H47M16.854775808S"},
} }
func TestFormatDuration(t *testing.T) { func TestFormatDuration(t *testing.T) {
@ -107,6 +113,7 @@ func TestFormatDuration(t *testing.T) {
check(tt.td, tt.base10Milli, 1e6) check(tt.td, tt.base10Milli, 1e6)
check(tt.td, tt.base10Micro, 1e3) check(tt.td, tt.base10Micro, 1e3)
check(tt.td, tt.base10Nano, 1e0) check(tt.td, tt.base10Nano, 1e0)
check(tt.td, tt.iso8601, 8601)
} }
} }
@ -114,31 +121,108 @@ var parseDurationTestdata = []struct {
in string in string
base uint64 base uint64
want time.Duration want time.Duration
wantErr bool wantErr error
}{ }{
{"0", 1e0, 0, false}, {"0", 1e0, 0, nil},
{"0.", 1e0, 0, true}, {"0.", 1e0, 0, strconv.ErrSyntax},
{"0.0", 1e0, 0, false}, {"0.0", 1e0, 0, nil},
{"0.00", 1e0, 0, false}, {"0.00", 1e0, 0, nil},
{"00.0", 1e0, 0, true}, {"00.0", 1e0, 0, strconv.ErrSyntax},
{"+0", 1e0, 0, true}, {"+0", 1e0, 0, strconv.ErrSyntax},
{"1e0", 1e0, 0, true}, {"1e0", 1e0, 0, strconv.ErrSyntax},
{"1.000000000x", 1e9, 0, true}, {"1.000000000x", 1e9, 0, strconv.ErrSyntax},
{"1.000000x", 1e6, 0, true}, {"1.000000x", 1e6, 0, strconv.ErrSyntax},
{"1.000x", 1e3, 0, true}, {"1.000x", 1e3, 0, strconv.ErrSyntax},
{"1.x", 1e0, 0, true}, {"1.x", 1e0, 0, strconv.ErrSyntax},
{"1.0000000009", 1e9, +time.Second, false}, {"1.0000000009", 1e9, +time.Second, nil},
{"1.0000009", 1e6, +time.Millisecond, false}, {"1.0000009", 1e6, +time.Millisecond, nil},
{"1.0009", 1e3, +time.Microsecond, false}, {"1.0009", 1e3, +time.Microsecond, nil},
{"1.9", 1e0, +time.Nanosecond, false}, {"1.9", 1e0, +time.Nanosecond, nil},
{"-9223372036854775809", 1e0, 0, true}, {"-9223372036854775809", 1e0, 0, strconv.ErrRange},
{"9223372036854775.808", 1e3, 0, true}, {"9223372036854775.808", 1e3, 0, strconv.ErrRange},
{"-9223372036854.775809", 1e6, 0, true}, {"-9223372036854.775809", 1e6, 0, strconv.ErrRange},
{"9223372036.854775808", 1e9, 0, true}, {"9223372036.854775808", 1e9, 0, strconv.ErrRange},
{"-1.9", 1e0, -time.Nanosecond, false}, {"-1.9", 1e0, -time.Nanosecond, nil},
{"-1.0009", 1e3, -time.Microsecond, false}, {"-1.0009", 1e3, -time.Microsecond, nil},
{"-1.0000009", 1e6, -time.Millisecond, false}, {"-1.0000009", 1e6, -time.Millisecond, nil},
{"-1.0000000009", 1e9, -time.Second, false}, {"-1.0000000009", 1e9, -time.Second, nil},
{"", 8601, 0, strconv.ErrSyntax},
{"P", 8601, 0, strconv.ErrSyntax},
{"PT", 8601, 0, strconv.ErrSyntax},
{"PT0", 8601, 0, strconv.ErrSyntax},
{"DT0S", 8601, 0, strconv.ErrSyntax},
{"PT0S", 8601, 0, nil},
{" PT0S", 8601, 0, strconv.ErrSyntax},
{"PT0S ", 8601, 0, strconv.ErrSyntax},
{"+PT0S", 8601, 0, nil},
{"PT0.M", 8601, 0, strconv.ErrSyntax},
{"PT0.S", 8601, 0, strconv.ErrSyntax},
{"PT0.0S", 8601, 0, nil},
{"PT0.0_0H", 8601, 0, strconv.ErrSyntax},
{"PT0.0_0M", 8601, 0, strconv.ErrSyntax},
{"PT0.0_0S", 8601, 0, strconv.ErrSyntax},
{"PT.0S", 8601, 0, strconv.ErrSyntax},
{"PT00.0S", 8601, 0, nil},
{"PT0S", 8601, 0, nil},
{"PT1,5S", 8601, time.Second + 500*time.Millisecond, nil},
{"PT1H", 8601, time.Hour, nil},
{"PT1H0S", 8601, time.Hour, nil},
{"PT0S", 8601, 0, nil},
{"PT00S", 8601, 0, nil},
{"PT000S", 8601, 0, nil},
{"PTS", 8601, 0, strconv.ErrSyntax},
{"PT1M", 8601, time.Minute, nil},
{"PT01M", 8601, time.Minute, nil},
{"PT001M", 8601, time.Minute, nil},
{"PT1H59S", 8601, time.Hour + 59*time.Second, nil},
{"PT123H4M56.789S", 8601, 123*time.Hour + 4*time.Minute + 56*time.Second + 789*time.Millisecond, nil},
{"-PT123H4M56.789S", 8601, -123*time.Hour - 4*time.Minute - 56*time.Second - 789*time.Millisecond, nil},
{"PT0H0S", 8601, 0, nil},
{"PT0H", 8601, 0, nil},
{"PT0M", 8601, 0, nil},
{"-PT0S", 8601, 0, nil},
{"PT1M0S", 8601, time.Minute, nil},
{"PT0H1M0S", 8601, time.Minute, nil},
{"PT01H02M03S", 8601, 1*time.Hour + 2*time.Minute + 3*time.Second, nil},
{"PT0,123S", 8601, 123 * time.Millisecond, nil},
{"PT1.S", 8601, 0, strconv.ErrSyntax},
{"PT1.000S", 8601, time.Second, nil},
{"PT0.025H", 8601, time.Minute + 30*time.Second, nil},
{"PT0.025H0M", 8601, 0, strconv.ErrSyntax},
{"PT1.5M", 8601, time.Minute + 30*time.Second, nil},
{"PT1.5M0S", 8601, 0, strconv.ErrSyntax},
{"PT60M", 8601, time.Hour, nil},
{"PT3600S", 8601, time.Hour, nil},
{"PT1H2M3.0S", 8601, 1*time.Hour + 2*time.Minute + 3*time.Second, nil},
{"pt1h2m3,0s", 8601, 1*time.Hour + 2*time.Minute + 3*time.Second, nil},
{"PT-1H-2M-3S", 8601, 0, strconv.ErrSyntax},
{"P1Y", 8601, time.Duration(daysPerYear * 24 * 60 * 60 * 1e9), errInaccurateDateUnits},
{"P1.0Y", 8601, 0, strconv.ErrSyntax},
{"P1M", 8601, time.Duration(daysPerYear / 12 * 24 * 60 * 60 * 1e9), errInaccurateDateUnits},
{"P1.0M", 8601, 0, strconv.ErrSyntax},
{"P1W", 8601, 7 * 24 * time.Hour, errInaccurateDateUnits},
{"P1.0W", 8601, 0, strconv.ErrSyntax},
{"P1D", 8601, 24 * time.Hour, errInaccurateDateUnits},
{"P1.0D", 8601, 0, strconv.ErrSyntax},
{"P1W1S", 8601, 0, strconv.ErrSyntax},
{"-P1Y2M3W4DT5H6M7.8S", 8601, -(time.Duration(14*daysPerYear/12*24*60*60*1e9) + time.Duration((3*7+4)*24*60*60*1e9) + 5*time.Hour + 6*time.Minute + 7*time.Second + 800*time.Millisecond), errInaccurateDateUnits},
{"-p1y2m3w4dt5h6m7.8s", 8601, -(time.Duration(14*daysPerYear/12*24*60*60*1e9) + time.Duration((3*7+4)*24*60*60*1e9) + 5*time.Hour + 6*time.Minute + 7*time.Second + 800*time.Millisecond), errInaccurateDateUnits},
{"P0Y0M0DT1H2M3S", 8601, 1*time.Hour + 2*time.Minute + 3*time.Second, errInaccurateDateUnits},
{"PT0.0000000001S", 8601, 0, nil},
{"PT0.0000000005S", 8601, 0, nil},
{"PT0.000000000500000000S", 8601, 0, nil},
{"PT0.000000000499999999S", 8601, 0, nil},
{"PT2562047H47M16.854775808S", 8601, 0, strconv.ErrRange},
{"-PT2562047H47M16.854775809S", 8601, 0, strconv.ErrRange},
{"PT9223372036.854775807S", 8601, math.MaxInt64, nil},
{"PT9223372036.854775808S", 8601, 0, strconv.ErrRange},
{"-PT9223372036.854775808S", 8601, math.MinInt64, nil},
{"-PT9223372036.854775809S", 8601, 0, strconv.ErrRange},
{"PT18446744073709551616S", 8601, 0, strconv.ErrRange},
{"PT5124096H", 8601, 0, strconv.ErrRange},
{"PT2562047.7880152155019444H", 8601, math.MaxInt64, nil},
{"PT2562047.7880152155022222H", 8601, 0, strconv.ErrRange},
{"PT5124094H94M33.709551616S", 8601, 0, strconv.ErrRange},
} }
func TestParseDuration(t *testing.T) { func TestParseDuration(t *testing.T) {
@ -147,10 +231,8 @@ func TestParseDuration(t *testing.T) {
switch err := a.unmarshal([]byte(tt.in)); { switch err := a.unmarshal([]byte(tt.in)); {
case a.td != tt.want: case a.td != tt.want:
t.Errorf("parseDuration(%q, %s) = %v, want %v", tt.in, baseLabel(tt.base), a.td, tt.want) t.Errorf("parseDuration(%q, %s) = %v, want %v", tt.in, baseLabel(tt.base), a.td, tt.want)
case (err == nil) && tt.wantErr: case !errors.Is(err, tt.wantErr):
t.Errorf("parseDuration(%q, %s) error is nil, want non-nil", tt.in, baseLabel(tt.base)) t.Errorf("parseDuration(%q, %s) error = %v, want %v", tt.in, baseLabel(tt.base), err, tt.wantErr)
case (err != nil) && !tt.wantErr:
t.Errorf("parseDuration(%q, %s) error is non-nil, want nil", tt.in, baseLabel(tt.base))
} }
} }
} }
@ -161,7 +243,7 @@ func FuzzFormatDuration(f *testing.F) {
} }
f.Fuzz(func(t *testing.T, want int64) { f.Fuzz(func(t *testing.T, want int64) {
var buf []byte var buf []byte
for _, base := range [...]uint64{1e0, 1e3, 1e6, 1e9} { for _, base := range [...]uint64{1e0, 1e3, 1e6, 1e9, 8601} {
a := durationArshaler{td: time.Duration(want), base: base} a := durationArshaler{td: time.Duration(want), base: base}
buf, _ = a.appendMarshal(buf[:0]) buf, _ = a.appendMarshal(buf[:0])
switch err := a.unmarshal(buf); { switch err := a.unmarshal(buf); {
@ -179,9 +261,11 @@ func FuzzParseDuration(f *testing.F) {
f.Add([]byte(tt.in)) f.Add([]byte(tt.in))
} }
f.Fuzz(func(t *testing.T, in []byte) { f.Fuzz(func(t *testing.T, in []byte) {
for _, base := range [...]uint64{1e0, 1e3, 1e6, 1e9, 60} { for _, base := range [...]uint64{1e0, 1e3, 1e6, 1e9, 8601} {
a := durationArshaler{base: base} a := durationArshaler{base: base}
if err := a.unmarshal(in); err == nil && base != 60 { switch err := a.unmarshal(in); {
case err != nil: // nothing else to check
case base != 8601:
if n, err := jsonwire.ConsumeNumber(in); err != nil || n != len(in) { if n, err := jsonwire.ConsumeNumber(in); err != nil || n != len(in) {
t.Fatalf("parseDuration(%q) error is nil for invalid JSON number", in) t.Fatalf("parseDuration(%q) error is nil for invalid JSON number", in)
} }
@ -239,26 +323,26 @@ var parseTimeTestdata = []struct {
in string in string
base uint64 base uint64
want time.Time want time.Time
wantErr bool wantErr error
}{ }{
{"0", 1e0, time.Unix(0, 0).UTC(), false}, {"0", 1e0, time.Unix(0, 0).UTC(), nil},
{"0.", 1e0, time.Time{}, true}, {"0.", 1e0, time.Time{}, strconv.ErrSyntax},
{"0.0", 1e0, time.Unix(0, 0).UTC(), false}, {"0.0", 1e0, time.Unix(0, 0).UTC(), nil},
{"0.00", 1e0, time.Unix(0, 0).UTC(), false}, {"0.00", 1e0, time.Unix(0, 0).UTC(), nil},
{"00.0", 1e0, time.Time{}, true}, {"00.0", 1e0, time.Time{}, strconv.ErrSyntax},
{"+0", 1e0, time.Time{}, true}, {"+0", 1e0, time.Time{}, strconv.ErrSyntax},
{"1e0", 1e0, time.Time{}, true}, {"1e0", 1e0, time.Time{}, strconv.ErrSyntax},
{"1234567890123456789012345678901234567890", 1e0, time.Time{}, true}, {"1234567890123456789012345678901234567890", 1e0, time.Time{}, strconv.ErrRange},
{"9223372036854775808000.000000", 1e3, time.Time{}, true}, {"9223372036854775808000.000000", 1e3, time.Time{}, strconv.ErrRange},
{"9223372036854775807999999.9999", 1e6, time.Unix(math.MaxInt64, 1e9-1).UTC(), false}, {"9223372036854775807999999.9999", 1e6, time.Unix(math.MaxInt64, 1e9-1).UTC(), nil},
{"9223372036854775807999999999.9", 1e9, time.Unix(math.MaxInt64, 1e9-1).UTC(), false}, {"9223372036854775807999999999.9", 1e9, time.Unix(math.MaxInt64, 1e9-1).UTC(), nil},
{"9223372036854775807.999999999x", 1e0, time.Time{}, true}, {"9223372036854775807.999999999x", 1e0, time.Time{}, strconv.ErrSyntax},
{"9223372036854775807000000000", 1e9, time.Unix(math.MaxInt64, 0).UTC(), false}, {"9223372036854775807000000000", 1e9, time.Unix(math.MaxInt64, 0).UTC(), nil},
{"-9223372036854775808", 1e0, time.Unix(math.MinInt64, 0).UTC(), false}, {"-9223372036854775808", 1e0, time.Unix(math.MinInt64, 0).UTC(), nil},
{"-9223372036854775808000.000001", 1e3, time.Time{}, true}, {"-9223372036854775808000.000001", 1e3, time.Time{}, strconv.ErrRange},
{"-9223372036854775808000000.0001", 1e6, time.Unix(math.MinInt64, 0).UTC(), false}, {"-9223372036854775808000000.0001", 1e6, time.Unix(math.MinInt64, 0).UTC(), nil},
{"-9223372036854775808000000000.x", 1e9, time.Time{}, true}, {"-9223372036854775808000000000.x", 1e9, time.Time{}, strconv.ErrSyntax},
{"-1234567890123456789012345678901234567890", 1e9, time.Time{}, true}, {"-1234567890123456789012345678901234567890", 1e9, time.Time{}, strconv.ErrRange},
} }
func TestParseTime(t *testing.T) { func TestParseTime(t *testing.T) {
@ -267,10 +351,8 @@ func TestParseTime(t *testing.T) {
switch err := a.unmarshal([]byte(tt.in)); { switch err := a.unmarshal([]byte(tt.in)); {
case a.tt != tt.want: case a.tt != tt.want:
t.Errorf("parseTime(%q, %s) = time.Unix(%d, %d), want time.Unix(%d, %d)", tt.in, baseLabel(tt.base), a.tt.Unix(), a.tt.Nanosecond(), tt.want.Unix(), tt.want.Nanosecond()) t.Errorf("parseTime(%q, %s) = time.Unix(%d, %d), want time.Unix(%d, %d)", tt.in, baseLabel(tt.base), a.tt.Unix(), a.tt.Nanosecond(), tt.want.Unix(), tt.want.Nanosecond())
case (err == nil) && tt.wantErr: case !errors.Is(err, tt.wantErr):
t.Errorf("parseTime(%q, %s) = (time.Unix(%d, %d), nil), want non-nil error", tt.in, baseLabel(tt.base), a.tt.Unix(), a.tt.Nanosecond()) t.Errorf("parseTime(%q, %s) error = %v, want %v", tt.in, baseLabel(tt.base), err, tt.wantErr)
case (err != nil) && !tt.wantErr:
t.Errorf("parseTime(%q, %s) error is non-nil, want nil", tt.in, baseLabel(tt.base))
} }
} }
} }

View file

@ -267,12 +267,13 @@ var arshalTestdata = []struct {
new: func() any { return new(jsonArshalerV2) }, new: func() any { return new(jsonArshalerV2) },
skipV1: true, skipV1: true,
}, { }, {
/* TODO(https://go.dev/issue/71631): Re-enable this test case.
name: "Duration", name: "Duration",
raw: []byte(`"1h1m1s"`), raw: []byte(`"1h1m1s"`),
val: addr(time.Hour + time.Minute + time.Second), val: addr(time.Hour + time.Minute + time.Second),
new: func() any { return new(time.Duration) }, new: func() any { return new(time.Duration) },
skipV1: true, skipV1: true,
}, { }, { */
name: "Time", name: "Time",
raw: []byte(`"2006-01-02T22:04:05Z"`), raw: []byte(`"2006-01-02T22:04:05Z"`),
val: addr(time.Unix(1136239445, 0).UTC()), val: addr(time.Unix(1136239445, 0).UTC()),

View file

@ -9,6 +9,11 @@
// primitive data types such as booleans, strings, and numbers, // primitive data types such as booleans, strings, and numbers,
// in addition to structured data types such as objects and arrays. // in addition to structured data types such as objects and arrays.
// //
// This package (encoding/json/v2) is experimental,
// and not subject to the Go 1 compatibility promise.
// It only exists when building with the GOEXPERIMENT=jsonv2 environment variable set.
// Most users should use [encoding/json].
//
// [Marshal] and [Unmarshal] encode and decode Go values // [Marshal] and [Unmarshal] encode and decode Go values
// to/from JSON text contained within a []byte. // to/from JSON text contained within a []byte.
// [MarshalWrite] and [UnmarshalRead] operate on JSON text // [MarshalWrite] and [UnmarshalRead] operate on JSON text

View file

@ -412,6 +412,7 @@ func Example_formatFlags() {
TimeUnixSec time.Time `json:",format:unix"` TimeUnixSec time.Time `json:",format:unix"`
DurationSecs time.Duration `json:",format:sec"` DurationSecs time.Duration `json:",format:sec"`
DurationNanos time.Duration `json:",format:nano"` DurationNanos time.Duration `json:",format:nano"`
DurationISO8601 time.Duration `json:",format:iso8601"`
}{ }{
BytesBase64: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, BytesBase64: []byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
BytesHex: [8]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, BytesHex: [8]byte{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef},
@ -423,6 +424,7 @@ func Example_formatFlags() {
TimeUnixSec: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC), TimeUnixSec: time.Date(2000, 1, 1, 0, 0, 0, 0, time.UTC),
DurationSecs: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond, DurationSecs: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationNanos: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond, DurationNanos: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
DurationISO8601: 12*time.Hour + 34*time.Minute + 56*time.Second + 7*time.Millisecond + 8*time.Microsecond + 9*time.Nanosecond,
} }
b, err := json.Marshal(&value) b, err := json.Marshal(&value)
@ -452,7 +454,8 @@ func Example_formatFlags() {
// "TimeDateOnly": "2000-01-01", // "TimeDateOnly": "2000-01-01",
// "TimeUnixSec": 946684800, // "TimeUnixSec": 946684800,
// "DurationSecs": 45296.007008009, // "DurationSecs": 45296.007008009,
// "DurationNanos": 45296007008009 // "DurationNanos": 45296007008009,
// "DurationISO8601": "PT12H34M56.007008009S"
// } // }
} }

View file

@ -404,6 +404,7 @@ type fieldOptions struct {
// the JSON member name and other features. // the JSON member name and other features.
func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err error) { func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool, err error) {
tag, hasTag := sf.Tag.Lookup("json") tag, hasTag := sf.Tag.Lookup("json")
tagOrig := tag
// Check whether this field is explicitly ignored. // Check whether this field is explicitly ignored.
if tag == "-" { if tag == "-" {
@ -453,6 +454,13 @@ func parseFieldOptions(sf reflect.StructField) (out fieldOptions, ignored bool,
err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", sf.Name, name)) err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q with invalid UTF-8", sf.Name, name))
name = string([]rune(name)) // replace invalid UTF-8 with utf8.RuneError name = string([]rune(name)) // replace invalid UTF-8 with utf8.RuneError
} }
if name == "-" && tag[0] == '-' {
defer func() { // defer to let other errors take precedence
err = cmp.Or(err, fmt.Errorf("Go struct field %s has JSON object name %q; either "+
"use `json:\"-\"` to ignore the field or "+
"use `json:\"'-'%s` to specify %q as the name", sf.Name, out.name, strings.TrimPrefix(strconv.Quote(tagOrig), `"-`), name))
}()
}
if err2 == nil { if err2 == nil {
out.hasName = true out.hasName = true
out.name = name out.name = name

View file

@ -502,6 +502,19 @@ func TestParseTagOptions(t *testing.T) {
}{}, }{},
wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`}, wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`},
wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"), wantErr: errors.New("Go struct field V has malformed `json` tag: invalid trailing ',' character"),
}, {
name: jsontest.Name("DashCommaOmitEmpty"),
in: struct {
V int `json:"-,omitempty"`
}{},
wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
wantErr: errors.New("Go struct field V has JSON object name \"-\"; either use `json:\"-\"` to ignore the field or use `json:\"'-',omitempty\"` to specify \"-\" as the name"),
}, {
name: jsontest.Name("QuotedDashCommaOmitEmpty"),
in: struct {
V int `json:"'-',omitempty"`
}{},
wantOpts: fieldOptions{hasName: true, name: "-", quotedName: `"-"`, omitempty: true},
}, { }, {
name: jsontest.Name("QuotedDashName"), name: jsontest.Name("QuotedDashName"),
in: struct { in: struct {

View file

@ -1195,6 +1195,27 @@ var unmarshalTests = []struct {
out: []int{1, 2, 0, 4, 5}, out: []int{1, 2, 0, 4, 5},
err: &UnmarshalTypeError{Value: "bool", Type: reflect.TypeFor[int](), Field: "2", Offset: len64(`[1,2,`)}, err: &UnmarshalTypeError{Value: "bool", Type: reflect.TypeFor[int](), Field: "2", Offset: len64(`[1,2,`)},
}, },
{
CaseName: Name("DashComma"),
in: `{"-":"hello"}`,
ptr: new(struct {
F string `json:"-,"`
}),
out: struct {
F string `json:"-,"`
}{"hello"},
},
{
CaseName: Name("DashCommaOmitEmpty"),
in: `{"-":"hello"}`,
ptr: new(struct {
F string `json:"-,omitempty"`
}),
out: struct {
F string `json:"-,omitempty"`
}{"hello"},
},
} }
func TestMarshal(t *testing.T) { func TestMarshal(t *testing.T) {

View file

@ -1038,6 +1038,7 @@ func TestMergeComposite(t *testing.T) {
// //
// https://go.dev/issue/10275 // https://go.dev/issue/10275
func TestTimeDurations(t *testing.T) { func TestTimeDurations(t *testing.T) {
t.SkipNow() // TODO(https://go.dev/issue/71631): The default representation of time.Duration is still undecided.
for _, json := range jsonPackages { for _, json := range jsonPackages {
t.Run(path.Join("Marshal", json.Version), func(t *testing.T) { t.Run(path.Join("Marshal", json.Version), func(t *testing.T) {
got, err := json.Marshal(time.Minute) got, err := json.Marshal(time.Minute)

View file

@ -68,7 +68,10 @@ import (
// slice, map, or string of length zero. // slice, map, or string of length zero.
// //
// As a special case, if the field tag is "-", the field is always omitted. // As a special case, if the field tag is "-", the field is always omitted.
// Note that a field with name "-" can still be generated using the tag "-,". // JSON names containing commas or quotes, or names identical to "" or "-",
// can be specified using a single-quoted string literal, where the syntax
// is identical to the Go grammar for a double-quoted string literal,
// but instead uses single quotes as the delimiters.
// //
// Examples of struct field tags and their meanings: // Examples of struct field tags and their meanings:
// //
@ -89,7 +92,7 @@ import (
// Field int `json:"-"` // Field int `json:"-"`
// //
// // Field appears in JSON as key "-". // // Field appears in JSON as key "-".
// Field int `json:"-,"` // Field int `json:"'-'"`
// //
// The "omitzero" option specifies that the field should be omitted // The "omitzero" option specifies that the field should be omitted
// from the encoding if the field has a zero value, according to rules: // from the encoding if the field has a zero value, according to rules:

22
src/go/doc/testdata/issue62640.0.golden vendored Normal file
View file

@ -0,0 +1,22 @@
//
PACKAGE issue62640
IMPORTPATH
testdata/issue62640
FILENAMES
testdata/issue62640.go
TYPES
//
type E struct{}
// F should be hidden within S because of the S.F field.
func (E) F()
//
type S struct {
E
F int
}

22
src/go/doc/testdata/issue62640.1.golden vendored Normal file
View file

@ -0,0 +1,22 @@
//
PACKAGE issue62640
IMPORTPATH
testdata/issue62640
FILENAMES
testdata/issue62640.go
TYPES
//
type E struct{}
// F should be hidden within S because of the S.F field.
func (E) F()
//
type S struct {
E
F int
}

25
src/go/doc/testdata/issue62640.2.golden vendored Normal file
View file

@ -0,0 +1,25 @@
//
PACKAGE issue62640
IMPORTPATH
testdata/issue62640
FILENAMES
testdata/issue62640.go
TYPES
//
type E struct{}
// F should be hidden within S because of the S.F field.
func (E) F()
//
type S struct {
E
F int
}
// F should be hidden within S because of the S.F field.
func (S) F()

15
src/go/doc/testdata/issue62640.go vendored Normal file
View file

@ -0,0 +1,15 @@
// Copyright 2025 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.
package issue62640
type E struct{}
// F should be hidden within S because of the S.F field.
func (E) F() {}
type S struct {
E
F int
}

View file

@ -364,6 +364,11 @@ func TestTypesInfo(t *testing.T) {
// go.dev/issue/47895 // go.dev/issue/47895
{`package p; import "unsafe"; type S struct { f int }; var s S; var _ = unsafe.Offsetof(s.f)`, `s.f`, `int`}, {`package p; import "unsafe"; type S struct { f int }; var s S; var _ = unsafe.Offsetof(s.f)`, `s.f`, `int`},
// go.dev/issue/74303. Note that interface field types are synthetic, so
// even though `func()` doesn't appear in the source, it appears in the
// syntax tree.
{`package p; type T interface { M(int) }`, `func(int)`, `func(int)`},
// go.dev/issue/50093 // go.dev/issue/50093
{`package u0a; func _[_ interface{int}]() {}`, `int`, `int`}, {`package u0a; func _[_ interface{int}]() {}`, `int`, `int`},
{`package u1a; func _[_ interface{~int}]() {}`, `~int`, `~int`}, {`package u1a; func _[_ interface{~int}]() {}`, `~int`, `~int`},

View file

@ -176,19 +176,17 @@ func (check *Checker) interfaceType(ityp *Interface, iface *ast.InterfaceType, d
name := f.Names[0] name := f.Names[0]
if name.Name == "_" { if name.Name == "_" {
check.error(name, BlankIfaceMethod, "methods must have a unique non-blank name") check.error(name, BlankIfaceMethod, "methods must have a unique non-blank name")
continue // ignore method continue // ignore
} }
// Type-check method declaration. typ := check.typ(f.Type)
// Note: Don't call check.typ(f.Type) as that would record sig, _ := typ.(*Signature)
// the method incorrectly as a type expression in Info.Types. if sig == nil {
ftyp, _ := f.Type.(*ast.FuncType) if isValid(typ) {
if ftyp == nil { check.errorf(f.Type, InvalidSyntaxTree, "%s is not a method signature", typ)
check.errorf(f.Type, InvalidSyntaxTree, "%s is not a method signature", f.Type) }
continue // ignore method continue // ignore
} }
sig := new(Signature)
check.funcType(sig, nil, ftyp)
// The go/parser doesn't accept method type parameters but an ast.FuncType may have them. // The go/parser doesn't accept method type parameters but an ast.FuncType may have them.
if sig.tparams != nil { if sig.tparams != nil {

View file

@ -43,17 +43,19 @@ type Value struct {
ptr unsafe.Pointer ptr unsafe.Pointer
// flag holds metadata about the value. // flag holds metadata about the value.
// The lowest bits are flag bits: //
// The lowest five bits give the Kind of the value, mirroring typ.Kind().
//
// The next set of bits are flag bits:
// - flagStickyRO: obtained via unexported not embedded field, so read-only // - flagStickyRO: obtained via unexported not embedded field, so read-only
// - flagEmbedRO: obtained via unexported embedded field, so read-only // - flagEmbedRO: obtained via unexported embedded field, so read-only
// - flagIndir: val holds a pointer to the data // - flagIndir: val holds a pointer to the data
// - flagAddr: v.CanAddr is true (implies flagIndir) // - flagAddr: v.CanAddr is true (implies flagIndir and ptr is non-nil)
// Value cannot represent method values. // - flagMethod: v is a method value.
// The next five bits give the Kind of the value.
// This repeats typ.Kind() except for method values.
// The remaining 23+ bits give a method number for method values.
// If flag.kind() != Func, code can assume that flagMethod is unset.
// If ifaceIndir(typ), code can assume that flagIndir is set. // If ifaceIndir(typ), code can assume that flagIndir is set.
//
// The remaining 22+ bits give a method number for method values.
// If flag.kind() != Func, code can assume that flagMethod is unset.
flag flag
// A method value represents a curried method invocation // A method value represents a curried method invocation

View file

@ -8,6 +8,7 @@
package synctest package synctest
import ( import (
"internal/abi"
"unsafe" "unsafe"
) )
@ -22,14 +23,25 @@ func Wait()
//go:linkname IsInBubble //go:linkname IsInBubble
func IsInBubble() bool func IsInBubble() bool
// Associate associates p with the current bubble. // Association is the state of a pointer's bubble association.
// It returns false if p has an existing association with a different bubble. type Association int
func Associate[T any](p *T) (ok bool) {
return associate(unsafe.Pointer(p)) const (
Unbubbled = Association(iota) // not associated with any bubble
CurrentBubble // associated with the current bubble
OtherBubble // associated with a different bubble
)
// Associate attempts to associate p with the current bubble.
// It returns the new association status of p.
func Associate[T any](p *T) Association {
// Ensure p escapes to permit us to attach a special to it.
escapedP := abi.Escape(p)
return Association(associate(unsafe.Pointer(escapedP)))
} }
//go:linkname associate //go:linkname associate
func associate(p unsafe.Pointer) bool func associate(p unsafe.Pointer) int
// Disassociate disassociates p from any bubble. // Disassociate disassociates p from any bubble.
func Disassociate[T any](p *T) { func Disassociate[T any](p *T) {

View file

@ -731,6 +731,34 @@ func TestWaitGroupMovedBetweenBubblesAfterWait(t *testing.T) {
}) })
} }
var testWaitGroupLinkerAllocatedWG sync.WaitGroup
func TestWaitGroupLinkerAllocated(t *testing.T) {
synctest.Run(func() {
// This WaitGroup is probably linker-allocated and has no span,
// so we won't be able to add a special to it associating it with
// this bubble.
//
// Operations on it may not be durably blocking,
// but they shouldn't fail.
testWaitGroupLinkerAllocatedWG.Go(func() {})
testWaitGroupLinkerAllocatedWG.Wait()
})
}
var testWaitGroupHeapAllocatedWG = new(sync.WaitGroup)
func TestWaitGroupHeapAllocated(t *testing.T) {
synctest.Run(func() {
// This package-scoped WaitGroup var should have been heap-allocated,
// so we can associate it with a bubble.
testWaitGroupHeapAllocatedWG.Add(1)
go testWaitGroupHeapAllocatedWG.Wait()
synctest.Wait()
testWaitGroupHeapAllocatedWG.Done()
})
}
func TestHappensBefore(t *testing.T) { func TestHappensBefore(t *testing.T) {
// Use two parallel goroutines accessing different vars to ensure that // Use two parallel goroutines accessing different vars to ensure that
// we correctly account for multiple goroutines in the bubble. // we correctly account for multiple goroutines in the bubble.

View file

@ -192,6 +192,11 @@ func Mkdirat(dirfd syscall.Handle, name string, mode uint32) error {
} }
func Deleteat(dirfd syscall.Handle, name string, options uint32) error { func Deleteat(dirfd syscall.Handle, name string, options uint32) error {
if name == "." {
// NtOpenFile's documentation isn't explicit about what happens when deleting ".".
// Make this an error consistent with that of POSIX.
return syscall.EINVAL
}
objAttrs := &OBJECT_ATTRIBUTES{} objAttrs := &OBJECT_ATTRIBUTES{}
if err := objAttrs.init(dirfd, name); err != nil { if err := objAttrs.init(dirfd, name); err != nil {
return err return err

View file

@ -8,6 +8,7 @@ import (
"fmt" "fmt"
"iter" "iter"
"math" "math"
"strconv"
"strings" "strings"
"time" "time"
@ -812,6 +813,10 @@ func (e Event) String() string {
switch kind := e.Kind(); kind { switch kind := e.Kind(); kind {
case EventMetric: case EventMetric:
m := e.Metric() m := e.Metric()
v := m.Value.String()
if m.Value.Kind() == ValueString {
v = strconv.Quote(v)
}
fmt.Fprintf(&sb, " Name=%q Value=%s", m.Name, m.Value) fmt.Fprintf(&sb, " Name=%q Value=%s", m.Name, m.Value)
case EventLabel: case EventLabel:
l := e.Label() l := e.Label()

View file

@ -103,7 +103,7 @@ func MutatorUtilizationV2(events []Event, flags UtilFlags) [][]MutatorUtil {
if m.Name != "/sched/gomaxprocs:threads" { if m.Name != "/sched/gomaxprocs:threads" {
break break
} }
gomaxprocs := int(m.Value.ToUint64()) gomaxprocs := int(m.Value.Uint64())
if len(ps) > gomaxprocs { if len(ps) > gomaxprocs {
if flags&UtilPerProc != 0 { if flags&UtilPerProc != 0 {
// End each P's series. // End each P's series.

View file

@ -13,6 +13,7 @@ import (
"log" "log"
"os" "os"
"runtime" "runtime"
"runtime/debug"
"runtime/trace" "runtime/trace"
"time" "time"
) )
@ -36,11 +37,25 @@ func makeTree(depth int) *node {
} }
} }
func initTree(n *node) {
if n == nil {
return
}
for i := range n.data {
n.data[i] = 'a'
}
for i := range n.children {
initTree(n.children[i])
}
}
var trees [16]*node var trees [16]*node
var ballast *[16]*[1024]*node var ballast *[16]*[1024]*node
var sink [][]byte var sink []*node
func main() { func main() {
debug.SetMemoryLimit(50 << 20)
for i := range trees { for i := range trees {
trees[i] = makeTree(6) trees[i] = makeTree(6)
} }
@ -55,13 +70,17 @@ func main() {
} }
procs := runtime.GOMAXPROCS(-1) procs := runtime.GOMAXPROCS(-1)
sink = make([][]byte, procs) sink = make([]*node, procs)
for i := 0; i < procs; i++ { for i := 0; i < procs; i++ {
i := i i := i
go func() { go func() {
for { for {
sink[i] = make([]byte, 4<<10) sink[i] = makeTree(3)
for range 5 {
initTree(sink[i])
runtime.Gosched()
}
} }
}() }()
} }

View file

@ -135,7 +135,7 @@ func (v *Validator) Event(ev trace.Event) error {
switch m.Value.Kind() { switch m.Value.Kind() {
case trace.ValueUint64: case trace.ValueUint64:
// Just make sure it doesn't panic. // Just make sure it doesn't panic.
_ = m.Value.ToUint64() _ = m.Value.Uint64()
} }
case trace.EventLabel: case trace.EventLabel:
l := ev.Label() l := ev.Label()

View file

@ -582,13 +582,30 @@ func testTraceProg(t *testing.T, progName string, extra func(t *testing.T, trace
testPath := filepath.Join("./testdata/testprog", progName) testPath := filepath.Join("./testdata/testprog", progName)
testName := progName testName := progName
runTest := func(t *testing.T, stress bool, extraGODEBUG string) { runTest := func(t *testing.T, stress bool, extraGODEBUG string) {
// Run the program and capture the trace, which is always written to stdout. // Build the program.
cmd := testenv.Command(t, testenv.GoToolPath(t), "run") binFile, err := os.CreateTemp("", progName)
if race.Enabled { if err != nil {
cmd.Args = append(cmd.Args, "-race") t.Fatalf("failed to create temporary output file: %v", err)
} }
cmd.Args = append(cmd.Args, testPath) bin := binFile.Name()
cmd.Env = append(os.Environ(), "GOEXPERIMENT=rangefunc") binFile.Close()
t.Cleanup(func() {
os.Remove(bin)
})
buildCmd := testenv.CommandContext(t, t.Context(), testenv.GoToolPath(t), "build", "-o", bin)
if race.Enabled {
buildCmd.Args = append(buildCmd.Args, "-race")
}
buildCmd.Args = append(buildCmd.Args, testPath)
buildCmd.Env = append(os.Environ(), "GOEXPERIMENT=rangefunc")
buildOutput, err := buildCmd.CombinedOutput()
if err != nil {
t.Fatalf("failed to build %s: %v: output:\n%s", testPath, err, buildOutput)
}
// Run the program and capture the trace, which is always written to stdout.
cmd := testenv.CommandContext(t, t.Context(), bin)
// Add a stack ownership check. This is cheap enough for testing. // Add a stack ownership check. This is cheap enough for testing.
godebug := "tracecheckstackownership=1" godebug := "tracecheckstackownership=1"
if stress { if stress {

View file

@ -35,24 +35,27 @@ func (v Value) Kind() ValueKind {
return v.kind return v.kind
} }
// ToUint64 returns the uint64 value for a ValueUint64. // Uint64 returns the uint64 value for a ValueUint64.
// //
// Panics if this Value's Kind is not ValueUint64. // Panics if this Value's Kind is not ValueUint64.
func (v Value) ToUint64() uint64 { func (v Value) Uint64() uint64 {
if v.kind != ValueUint64 { if v.kind != ValueUint64 {
panic("ToUint64 called on Value of a different Kind") panic("Uint64 called on Value of a different Kind")
} }
return v.scalar return v.scalar
} }
// ToString returns the uint64 value for a ValueString. // String returns the string value for a ValueString, and otherwise
// // a string representation of the value for other kinds of values.
// Panics if this Value's Kind is not ValueString. func (v Value) String() string {
func (v Value) ToString() string { if v.kind == ValueString {
if v.kind != ValueString {
panic("ToString called on Value of a different Kind")
}
return unsafe.String((*byte)(v.pointer), int(v.scalar)) return unsafe.String((*byte)(v.pointer), int(v.scalar))
}
switch v.kind {
case ValueUint64:
return fmt.Sprintf("Value{Uint64(%d)}", v.Uint64())
}
return "Value{Bad}"
} }
func uint64Value(x uint64) Value { func uint64Value(x uint64) Value {
@ -62,14 +65,3 @@ func uint64Value(x uint64) Value {
func stringValue(s string) Value { func stringValue(s string) Value {
return Value{kind: ValueString, scalar: uint64(len(s)), pointer: unsafe.Pointer(unsafe.StringData(s))} return Value{kind: ValueString, scalar: uint64(len(s)), pointer: unsafe.Pointer(unsafe.StringData(s))}
} }
// String returns the string representation of the value.
func (v Value) String() string {
switch v.Kind() {
case ValueUint64:
return fmt.Sprintf("Value{Uint64(%d)}", v.ToUint64())
case ValueString:
return fmt.Sprintf("Value{String(%s)}", v.ToString())
}
return "Value{Bad}"
}

View file

@ -136,7 +136,7 @@ func (c *CrossOriginProtection) Check(req *Request) error {
if c.isRequestExempt(req) { if c.isRequestExempt(req) {
return nil return nil
} }
return errors.New("cross-origin request detected from Sec-Fetch-Site header") return errCrossOriginRequest
} }
origin := req.Header.Get("Origin") origin := req.Header.Get("Origin")
@ -159,10 +159,15 @@ func (c *CrossOriginProtection) Check(req *Request) error {
if c.isRequestExempt(req) { if c.isRequestExempt(req) {
return nil return nil
} }
return errors.New("cross-origin request detected, and/or browser is out of date: " + return errCrossOriginRequestFromOldBrowser
"Sec-Fetch-Site is missing, and Origin does not match Host")
} }
var (
errCrossOriginRequest = errors.New("cross-origin request detected from Sec-Fetch-Site header")
errCrossOriginRequestFromOldBrowser = errors.New("cross-origin request detected, and/or browser is out of date: " +
"Sec-Fetch-Site is missing, and Origin does not match Host")
)
// isRequestExempt checks the bypasses which require taking a lock, and should // isRequestExempt checks the bypasses which require taking a lock, and should
// be deferred until the last moment. // be deferred until the last moment.
func (c *CrossOriginProtection) isRequestExempt(req *Request) bool { func (c *CrossOriginProtection) isRequestExempt(req *Request) bool {

View file

@ -24,9 +24,6 @@ import (
// BUG(mikio): On JS and Plan 9, methods and functions related // BUG(mikio): On JS and Plan 9, methods and functions related
// to IPConn are not implemented. // to IPConn are not implemented.
// BUG(mikio): On Windows, the File method of IPConn is not
// implemented.
// IPAddr represents the address of an IP end point. // IPAddr represents the address of an IP end point.
type IPAddr struct { type IPAddr struct {
IP IP IP IP

View file

@ -14,7 +14,7 @@ import (
"time" "time"
) )
// BUG(mikio): On JS and Windows, the File method of TCPConn and // BUG(mikio): On JS, the File method of TCPConn and
// TCPListener is not implemented. // TCPListener is not implemented.
// TCPAddr represents the address of a TCP end point. // TCPAddr represents the address of a TCP end point.

View file

@ -14,9 +14,6 @@ import (
// BUG(mikio): On Plan 9, the ReadMsgUDP and // BUG(mikio): On Plan 9, the ReadMsgUDP and
// WriteMsgUDP methods of UDPConn are not implemented. // WriteMsgUDP methods of UDPConn are not implemented.
// BUG(mikio): On Windows, the File method of UDPConn is not
// implemented.
// BUG(mikio): On JS, methods and functions related to UDPConn are not // BUG(mikio): On JS, methods and functions related to UDPConn are not
// implemented. // implemented.

View file

@ -17,7 +17,7 @@
// //
// Note that the examples in this package assume a Unix system. // Note that the examples in this package assume a Unix system.
// They may not run on Windows, and they do not run in the Go Playground // They may not run on Windows, and they do not run in the Go Playground
// used by golang.org and godoc.org. // used by go.dev and pkg.go.dev.
// //
// # Executables in the current directory // # Executables in the current directory
// //

View file

@ -8,6 +8,7 @@ package os
import ( import (
"errors" "errors"
"internal/filepathlite"
"internal/stringslite" "internal/stringslite"
"sync/atomic" "sync/atomic"
"syscall" "syscall"
@ -173,6 +174,12 @@ func rootRemove(r *Root, name string) error {
if err := checkPathEscapesLstat(r, name); err != nil { if err := checkPathEscapesLstat(r, name); err != nil {
return &PathError{Op: "removeat", Path: name, Err: err} return &PathError{Op: "removeat", Path: name, Err: err}
} }
if endsWithDot(name) {
// We don't want to permit removing the root itself, so check for that.
if filepathlite.Clean(name) == "." {
return &PathError{Op: "removeat", Path: name, Err: errPathEscapes}
}
}
if err := Remove(joinPath(r.root.name, name)); err != nil { if err := Remove(joinPath(r.root.name, name)); err != nil {
return &PathError{Op: "removeat", Path: name, Err: underlyingError(err)} return &PathError{Op: "removeat", Path: name, Err: underlyingError(err)}
} }

View file

@ -1052,10 +1052,18 @@ func (h *mheap) allocUserArenaChunk() *mspan {
h.initSpan(s, spanAllocHeap, spc, base, userArenaChunkPages) h.initSpan(s, spanAllocHeap, spc, base, userArenaChunkPages)
s.isUserArenaChunk = true s.isUserArenaChunk = true
s.elemsize -= userArenaChunkReserveBytes() s.elemsize -= userArenaChunkReserveBytes()
s.limit = s.base() + s.elemsize
s.freeindex = 1 s.freeindex = 1
s.allocCount = 1 s.allocCount = 1
// Adjust s.limit down to the object-containing part of the span.
//
// This is just to create a slightly tighter bound on the limit.
// It's totally OK if the garbage collector, in particular
// conservative scanning, can temporarily observes an inflated
// limit. It will simply mark the whole chunk or just skip it
// since we're in the mark phase anyway.
s.limit = s.base() + s.elemsize
// Adjust size to include redzone. // Adjust size to include redzone.
if asanenabled { if asanenabled {
s.elemsize -= redZoneSize(s.elemsize) s.elemsize -= redZoneSize(s.elemsize)

View file

@ -39,7 +39,7 @@ func GOMAXPROCS(n int) int {
lock(&sched.lock) lock(&sched.lock)
ret := int(gomaxprocs) ret := int(gomaxprocs)
if n <= 0 || n == ret { if n <= 0 {
unlock(&sched.lock) unlock(&sched.lock)
return ret return ret
} }
@ -52,6 +52,12 @@ func GOMAXPROCS(n int) int {
lock(&computeMaxProcsLock) lock(&computeMaxProcsLock)
unlock(&computeMaxProcsLock) unlock(&computeMaxProcsLock)
if n == ret {
// sched.customGOMAXPROCS set, but no need to actually STW
// since the gomaxprocs itself isn't changing.
return ret
}
stw := stopTheWorldGC(stwGOMAXPROCS) stw := stopTheWorldGC(stwGOMAXPROCS)
// newprocs will be processed by startTheWorld // newprocs will be processed by startTheWorld

View file

@ -1911,3 +1911,9 @@ func (b BitCursor) Write(data *byte, cnt uintptr) {
func (b BitCursor) Offset(cnt uintptr) BitCursor { func (b BitCursor) Offset(cnt uintptr) BitCursor {
return BitCursor{b: b.b.offset(cnt)} return BitCursor{b: b.b.offset(cnt)}
} }
const (
BubbleAssocUnbubbled = bubbleAssocUnbubbled
BubbleAssocCurrentBubble = bubbleAssocCurrentBubble
BubbleAssocOtherBubble = bubbleAssocOtherBubble
)

View file

@ -253,6 +253,14 @@ func (c *mcache) allocLarge(size uintptr, noscan bool) *mspan {
// Put the large span in the mcentral swept list so that it's // Put the large span in the mcentral swept list so that it's
// visible to the background sweeper. // visible to the background sweeper.
mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s) mheap_.central[spc].mcentral.fullSwept(mheap_.sweepgen).push(s)
// Adjust s.limit down to the object-containing part of the span.
//
// This is just to create a slightly tighter bound on the limit.
// It's totally OK if the garbage collector, in particular
// conservative scanning, can temporarily observes an inflated
// limit. It will simply mark the whole object or just skip it
// since we're in the mark phase anyway.
s.limit = s.base() + size s.limit = s.base() + size
s.initHeapBits() s.initHeapBits()
return s return s

View file

@ -250,13 +250,10 @@ func (c *mcentral) uncacheSpan(s *mspan) {
// grow allocates a new empty span from the heap and initializes it for c's size class. // grow allocates a new empty span from the heap and initializes it for c's size class.
func (c *mcentral) grow() *mspan { func (c *mcentral) grow() *mspan {
npages := uintptr(gc.SizeClassToNPages[c.spanclass.sizeclass()]) npages := uintptr(gc.SizeClassToNPages[c.spanclass.sizeclass()])
size := uintptr(gc.SizeClassToSize[c.spanclass.sizeclass()])
s := mheap_.alloc(npages, c.spanclass) s := mheap_.alloc(npages, c.spanclass)
if s == nil { if s == nil {
return nil return nil
} }
s.limit = s.base() + size*uintptr(s.nelems)
s.initHeapBits() s.initHeapBits()
return s return s
} }

View file

@ -231,6 +231,7 @@ func sysReserveAlignedSbrk(size, align uintptr) (unsafe.Pointer, uintptr) {
memFree(unsafe.Pointer(end), endLen) memFree(unsafe.Pointer(end), endLen)
} }
memCheck() memCheck()
unlock(&memlock)
return unsafe.Pointer(pAligned), size return unsafe.Pointer(pAligned), size
} }

View file

@ -338,11 +338,6 @@ func blockUntilEmptyFinalizerQueue(timeout int64) bool {
return false return false
} }
//go:linkname unique_runtime_blockUntilEmptyFinalizerQueue unique.runtime_blockUntilEmptyFinalizerQueue
func unique_runtime_blockUntilEmptyFinalizerQueue(timeout int64) bool {
return blockUntilEmptyFinalizerQueue(timeout)
}
// SetFinalizer sets the finalizer associated with obj to the provided // SetFinalizer sets the finalizer associated with obj to the provided
// finalizer function. When the garbage collector finds an unreachable block // finalizer function. When the garbage collector finds an unreachable block
// with an associated finalizer, it clears the association and runs // with an associated finalizer, it clears the association and runs

View file

@ -1048,7 +1048,7 @@ func gcMarkTermination(stw worldStop) {
// N.B. The execution tracer is not aware of this status // N.B. The execution tracer is not aware of this status
// transition and handles it specially based on the // transition and handles it specially based on the
// wait reason. // wait reason.
casGToWaitingForGC(curgp, _Grunning, waitReasonGarbageCollection) casGToWaitingForSuspendG(curgp, _Grunning, waitReasonGarbageCollection)
// Run gc on the g0 stack. We do this so that the g stack // Run gc on the g0 stack. We do this so that the g stack
// we're currently running on will no longer change. Cuts // we're currently running on will no longer change. Cuts
@ -1522,7 +1522,8 @@ func gcBgMarkWorker(ready chan struct{}) {
systemstack(func() { systemstack(func() {
// Mark our goroutine preemptible so its stack // Mark our goroutine preemptible so its stack
// can be scanned. This lets two mark workers // can be scanned or observed by the execution
// tracer. This, for example, lets two mark workers
// scan each other (otherwise, they would // scan each other (otherwise, they would
// deadlock). We must not modify anything on // deadlock). We must not modify anything on
// the G stack. However, stack shrinking is // the G stack. However, stack shrinking is
@ -1532,7 +1533,7 @@ func gcBgMarkWorker(ready chan struct{}) {
// N.B. The execution tracer is not aware of this status // N.B. The execution tracer is not aware of this status
// transition and handles it specially based on the // transition and handles it specially based on the
// wait reason. // wait reason.
casGToWaitingForGC(gp, _Grunning, waitReasonGCWorkerActive) casGToWaitingForSuspendG(gp, _Grunning, waitReasonGCWorkerActive)
switch pp.gcMarkWorkerMode { switch pp.gcMarkWorkerMode {
default: default:
throw("gcBgMarkWorker: unexpected gcMarkWorkerMode") throw("gcBgMarkWorker: unexpected gcMarkWorkerMode")

View file

@ -227,7 +227,7 @@ func markroot(gcw *gcWork, i uint32, flushBgCredit bool) int64 {
userG := getg().m.curg userG := getg().m.curg
selfScan := gp == userG && readgstatus(userG) == _Grunning selfScan := gp == userG && readgstatus(userG) == _Grunning
if selfScan { if selfScan {
casGToWaitingForGC(userG, _Grunning, waitReasonGarbageCollectionScan) casGToWaitingForSuspendG(userG, _Grunning, waitReasonGarbageCollectionScan)
} }
// TODO: suspendG blocks (and spins) until gp // TODO: suspendG blocks (and spins) until gp
@ -682,7 +682,7 @@ func gcAssistAlloc1(gp *g, scanWork int64) {
} }
// gcDrainN requires the caller to be preemptible. // gcDrainN requires the caller to be preemptible.
casGToWaitingForGC(gp, _Grunning, waitReasonGCAssistMarking) casGToWaitingForSuspendG(gp, _Grunning, waitReasonGCAssistMarking)
// drain own cached work first in the hopes that it // drain own cached work first in the hopes that it
// will be more cache friendly. // will be more cache friendly.

View file

@ -111,6 +111,26 @@ func (o *spanScanOwnership) or(v spanScanOwnership) spanScanOwnership {
} }
func (imb *spanInlineMarkBits) init(class spanClass) { func (imb *spanInlineMarkBits) init(class spanClass) {
if imb == nil {
// This nil check and throw is almost pointless. Normally we would
// expect imb to never be nil. However, this is called on potentially
// freshly-allocated virtual memory. As of 2025, the compiler-inserted
// nil check is not a branch but a memory read that we expect to fault
// if the pointer really is nil.
//
// However, this causes a read of the page, and operating systems may
// take it as a hint to back the accessed memory with a read-only zero
// page. However, we immediately write to this memory, which can then
// force operating systems to have to update the page table and flush
// the TLB, causing a lot of churn for programs that are short-lived
// and monotonically grow in size.
//
// This nil check is thus an explicit branch instead of what the compiler
// would insert circa 2025, which is a memory read instruction.
//
// See go.dev/issue/74375 for details.
throw("runtime: span inline mark bits nil?")
}
*imb = spanInlineMarkBits{} *imb = spanInlineMarkBits{}
imb.class = class imb.class = class
} }

View file

@ -312,8 +312,10 @@ type heapArena struct {
// during marking. // during marking.
pageSpecials [pagesPerArena / 8]uint8 pageSpecials [pagesPerArena / 8]uint8
// pageUseSpanDartboard is a bitmap that indicates which spans are // pageUseSpanInlineMarkBits is a bitmap where each bit corresponds
// heap spans and also gcUsesSpanDartboard. // to a span, as only spans one page in size can have inline mark bits.
// The bit indicates that the span has a spanInlineMarkBits struct
// stored directly at the top end of the span's memory.
pageUseSpanInlineMarkBits [pagesPerArena / 8]uint8 pageUseSpanInlineMarkBits [pagesPerArena / 8]uint8
// checkmarks stores the debug.gccheckmark state. It is only // checkmarks stores the debug.gccheckmark state. It is only
@ -1445,7 +1447,6 @@ func (h *mheap) initSpan(s *mspan, typ spanAllocType, spanclass spanClass, base,
if typ.manual() { if typ.manual() {
s.manualFreeList = 0 s.manualFreeList = 0
s.nelems = 0 s.nelems = 0
s.limit = s.base() + s.npages*pageSize
s.state.set(mSpanManual) s.state.set(mSpanManual)
} else { } else {
// We must set span properties before the span is published anywhere // We must set span properties before the span is published anywhere
@ -1486,6 +1487,9 @@ func (h *mheap) initSpan(s *mspan, typ spanAllocType, spanclass spanClass, base,
s.gcmarkBits = newMarkBits(uintptr(s.nelems)) s.gcmarkBits = newMarkBits(uintptr(s.nelems))
s.allocBits = newAllocBits(uintptr(s.nelems)) s.allocBits = newAllocBits(uintptr(s.nelems))
// Adjust s.limit down to the object-containing part of the span.
s.limit = s.base() + uintptr(s.elemsize)*uintptr(s.nelems)
// It's safe to access h.sweepgen without the heap lock because it's // It's safe to access h.sweepgen without the heap lock because it's
// only ever updated with the world stopped and we run on the // only ever updated with the world stopped and we run on the
// systemstack which blocks a STW transition. // systemstack which blocks a STW transition.
@ -1785,6 +1789,7 @@ func (span *mspan) init(base uintptr, npages uintptr) {
span.list = nil span.list = nil
span.startAddr = base span.startAddr = base
span.npages = npages span.npages = npages
span.limit = base + npages*gc.PageSize // see go.dev/issue/74288; adjusted later for heap spans
span.allocCount = 0 span.allocCount = 0
span.spanclass = 0 span.spanclass = 0
span.elemsize = 0 span.elemsize = 0

View file

@ -1431,10 +1431,6 @@ func tryRecordGoroutineProfile(gp1 *g, pcbuf []uintptr, yield func()) {
// so here we check _Gdead first. // so here we check _Gdead first.
return return
} }
if isSystemGoroutine(gp1, false) {
// System goroutines should not appear in the profile.
return
}
for { for {
prev := gp1.goroutineProfiled.Load() prev := gp1.goroutineProfiled.Load()
@ -1472,6 +1468,17 @@ func tryRecordGoroutineProfile(gp1 *g, pcbuf []uintptr, yield func()) {
// stack), or from the scheduler in preparation to execute gp1 (running on the // stack), or from the scheduler in preparation to execute gp1 (running on the
// system stack). // system stack).
func doRecordGoroutineProfile(gp1 *g, pcbuf []uintptr) { func doRecordGoroutineProfile(gp1 *g, pcbuf []uintptr) {
if isSystemGoroutine(gp1, false) {
// System goroutines should not appear in the profile.
// Check this here and not in tryRecordGoroutineProfile because isSystemGoroutine
// may change on a goroutine while it is executing, so while the scheduler might
// see a system goroutine, goroutineProfileWithLabelsConcurrent might not, and
// this inconsistency could cause invariants to be violated, such as trying to
// record the stack of a running goroutine below. In short, we still want system
// goroutines to participate in the same state machine on gp1.goroutineProfiled as
// everything else, we just don't record the stack in the profile.
return
}
if readgstatus(gp1) == _Grunning { if readgstatus(gp1) == _Grunning {
print("doRecordGoroutineProfile gp1=", gp1.goid, "\n") print("doRecordGoroutineProfile gp1=", gp1.goid, "\n")
throw("cannot read stack of running goroutine") throw("cannot read stack of running goroutine")

View file

@ -1808,6 +1808,45 @@ func TestGoroutineProfileCoro(t *testing.T) {
goroutineProf.WriteTo(io.Discard, 1) goroutineProf.WriteTo(io.Discard, 1)
} }
// This test tries to provoke a situation wherein the finalizer goroutine is
// erroneously inspected by the goroutine profiler in such a way that could
// cause a crash. See go.dev/issue/74090.
func TestGoroutineProfileIssue74090(t *testing.T) {
testenv.MustHaveParallelism(t)
goroutineProf := Lookup("goroutine")
// T is a pointer type so it won't be allocated by the tiny
// allocator, which can lead to its finalizer not being called
// during this test.
type T *byte
for range 10 {
// We use finalizers for this test because finalizers transition between
// system and user goroutine on each call, since there's substantially
// more work to do to set up a finalizer call. Cleanups, on the other hand,
// transition once for a whole batch, and so are less likely to trigger
// the failure. Under stress testing conditions this test fails approximately
// 5 times every 1000 executions on a 64 core machine without the appropriate
// fix, which is not ideal but if this test crashes at all, it's a clear
// signal that something is broken.
var objs []*T
for range 10000 {
obj := new(T)
runtime.SetFinalizer(obj, func(_ interface{}) {})
objs = append(objs, obj)
}
objs = nil
// Queue up all the finalizers.
runtime.GC()
// Try to run a goroutine profile concurrently with finalizer execution
// to trigger the bug.
var w strings.Builder
goroutineProf.WriteTo(&w, 1)
}
}
func BenchmarkGoroutine(b *testing.B) { func BenchmarkGoroutine(b *testing.B) {
withIdle := func(n int, fn func(b *testing.B)) func(b *testing.B) { withIdle := func(n int, fn func(b *testing.B)) func(b *testing.B) {
return func(b *testing.B) { return func(b *testing.B) {

View file

@ -1347,13 +1347,13 @@ func casGToWaiting(gp *g, old uint32, reason waitReason) {
casgstatus(gp, old, _Gwaiting) casgstatus(gp, old, _Gwaiting)
} }
// casGToWaitingForGC transitions gp from old to _Gwaiting, and sets the wait reason. // casGToWaitingForSuspendG transitions gp from old to _Gwaiting, and sets the wait reason.
// The wait reason must be a valid isWaitingForGC wait reason. // The wait reason must be a valid isWaitingForSuspendG wait reason.
// //
// Use this over casgstatus when possible to ensure that a waitreason is set. // Use this over casgstatus when possible to ensure that a waitreason is set.
func casGToWaitingForGC(gp *g, old uint32, reason waitReason) { func casGToWaitingForSuspendG(gp *g, old uint32, reason waitReason) {
if !reason.isWaitingForGC() { if !reason.isWaitingForSuspendG() {
throw("casGToWaitingForGC with non-isWaitingForGC wait reason") throw("casGToWaitingForSuspendG with non-isWaitingForSuspendG wait reason")
} }
casGToWaiting(gp, old, reason) casGToWaiting(gp, old, reason)
} }
@ -1487,23 +1487,7 @@ func stopTheWorld(reason stwReason) worldStop {
gp := getg() gp := getg()
gp.m.preemptoff = reason.String() gp.m.preemptoff = reason.String()
systemstack(func() { systemstack(func() {
// Mark the goroutine which called stopTheWorld preemptible so its
// stack may be scanned.
// This lets a mark worker scan us while we try to stop the world
// since otherwise we could get in a mutual preemption deadlock.
// We must not modify anything on the G stack because a stack shrink
// may occur. A stack shrink is otherwise OK though because in order
// to return from this function (and to leave the system stack) we
// must have preempted all goroutines, including any attempting
// to scan our stack, in which case, any stack shrinking will
// have already completed by the time we exit.
//
// N.B. The execution tracer is not aware of this status
// transition and handles it specially based on the
// wait reason.
casGToWaitingForGC(gp, _Grunning, waitReasonStoppingTheWorld)
stopTheWorldContext = stopTheWorldWithSema(reason) // avoid write to stack stopTheWorldContext = stopTheWorldWithSema(reason) // avoid write to stack
casgstatus(gp, _Gwaiting, _Grunning)
}) })
return stopTheWorldContext return stopTheWorldContext
} }
@ -1592,7 +1576,30 @@ var gcsema uint32 = 1
// //
// Returns the STW context. When starting the world, this context must be // Returns the STW context. When starting the world, this context must be
// passed to startTheWorldWithSema. // passed to startTheWorldWithSema.
//
//go:systemstack
func stopTheWorldWithSema(reason stwReason) worldStop { func stopTheWorldWithSema(reason stwReason) worldStop {
// Mark the goroutine which called stopTheWorld preemptible so its
// stack may be scanned by the GC or observed by the execution tracer.
//
// This lets a mark worker scan us or the execution tracer take our
// stack while we try to stop the world since otherwise we could get
// in a mutual preemption deadlock.
//
// We must not modify anything on the G stack because a stack shrink
// may occur, now that we switched to _Gwaiting, specifically if we're
// doing this during the mark phase (mark termination excepted, since
// we know that stack scanning is done by that point). A stack shrink
// is otherwise OK though because in order to return from this function
// (and to leave the system stack) we must have preempted all
// goroutines, including any attempting to scan our stack, in which
// case, any stack shrinking will have already completed by the time we
// exit.
//
// N.B. The execution tracer is not aware of this status transition and
// andles it specially based on the wait reason.
casGToWaitingForSuspendG(getg().m.curg, _Grunning, waitReasonStoppingTheWorld)
trace := traceAcquire() trace := traceAcquire()
if trace.ok() { if trace.ok() {
trace.STWStart(reason) trace.STWStart(reason)
@ -1700,6 +1707,9 @@ func stopTheWorldWithSema(reason stwReason) worldStop {
worldStopped() worldStopped()
// Switch back to _Grunning, now that the world is stopped.
casgstatus(getg().m.curg, _Gwaiting, _Grunning)
return worldStop{ return worldStop{
reason: reason, reason: reason,
startedStopping: start, startedStopping: start,
@ -2068,15 +2078,23 @@ found:
func forEachP(reason waitReason, fn func(*p)) { func forEachP(reason waitReason, fn func(*p)) {
systemstack(func() { systemstack(func() {
gp := getg().m.curg gp := getg().m.curg
// Mark the user stack as preemptible so that it may be scanned. // Mark the user stack as preemptible so that it may be scanned
// Otherwise, our attempt to force all P's to a safepoint could // by the GC or observed by the execution tracer. Otherwise, our
// result in a deadlock as we attempt to preempt a worker that's // attempt to force all P's to a safepoint could result in a
// trying to preempt us (e.g. for a stack scan). // deadlock as we attempt to preempt a goroutine that's trying
// to preempt us (e.g. for a stack scan).
//
// We must not modify anything on the G stack because a stack shrink
// may occur. A stack shrink is otherwise OK though because in order
// to return from this function (and to leave the system stack) we
// must have preempted all goroutines, including any attempting
// to scan our stack, in which case, any stack shrinking will
// have already completed by the time we exit.
// //
// N.B. The execution tracer is not aware of this status // N.B. The execution tracer is not aware of this status
// transition and handles it specially based on the // transition and handles it specially based on the
// wait reason. // wait reason.
casGToWaitingForGC(gp, _Grunning, reason) casGToWaitingForSuspendG(gp, _Grunning, reason)
forEachPInternal(fn) forEachPInternal(fn)
casgstatus(gp, _Gwaiting, _Grunning) casgstatus(gp, _Gwaiting, _Grunning)
}) })

View file

@ -1163,17 +1163,17 @@ func (w waitReason) isMutexWait() bool {
w == waitReasonSyncRWMutexLock w == waitReasonSyncRWMutexLock
} }
func (w waitReason) isWaitingForGC() bool { func (w waitReason) isWaitingForSuspendG() bool {
return isWaitingForGC[w] return isWaitingForSuspendG[w]
} }
// isWaitingForGC indicates that a goroutine is only entering _Gwaiting and // isWaitingForSuspendG indicates that a goroutine is only entering _Gwaiting and
// setting a waitReason because it needs to be able to let the GC take ownership // setting a waitReason because it needs to be able to let the suspendG
// of its stack. The G is always actually executing on the system stack, in // (used by the GC and the execution tracer) take ownership of its stack.
// these cases. // The G is always actually executing on the system stack in these cases.
// //
// TODO(mknyszek): Consider replacing this with a new dedicated G status. // TODO(mknyszek): Consider replacing this with a new dedicated G status.
var isWaitingForGC = [len(waitReasonStrings)]bool{ var isWaitingForSuspendG = [len(waitReasonStrings)]bool{
waitReasonStoppingTheWorld: true, waitReasonStoppingTheWorld: true,
waitReasonGCMarkTermination: true, waitReasonGCMarkTermination: true,
waitReasonGarbageCollection: true, waitReasonGarbageCollection: true,

View file

@ -1212,14 +1212,14 @@ func isShrinkStackSafe(gp *g) bool {
return false return false
} }
// We also can't copy the stack while tracing is enabled, and // We also can't copy the stack while tracing is enabled, and
// gp is in _Gwaiting solely to make itself available to the GC. // gp is in _Gwaiting solely to make itself available to suspendG.
// In these cases, the G is actually executing on the system // In these cases, the G is actually executing on the system
// stack, and the execution tracer may want to take a stack trace // stack, and the execution tracer may want to take a stack trace
// of the G's stack. Note: it's safe to access gp.waitreason here. // of the G's stack. Note: it's safe to access gp.waitreason here.
// We're only checking if this is true if we took ownership of the // We're only checking if this is true if we took ownership of the
// G with the _Gscan bit. This prevents the goroutine from transitioning, // G with the _Gscan bit. This prevents the goroutine from transitioning,
// which prevents gp.waitreason from changing. // which prevents gp.waitreason from changing.
if traceEnabled() && readgstatus(gp)&^_Gscan == _Gwaiting && gp.waitreason.isWaitingForGC() { if traceEnabled() && readgstatus(gp)&^_Gscan == _Gwaiting && gp.waitreason.isWaitingForSuspendG() {
return false return false
} }
return true return true

View file

@ -363,6 +363,13 @@ type specialBubble struct {
bubbleid uint64 bubbleid uint64
} }
// Keep these in sync with internal/synctest.
const (
bubbleAssocUnbubbled = iota // not associated with any bubble
bubbleAssocCurrentBubble // associated with the current bubble
bubbleAssocOtherBubble // associated with a different bubble
)
// getOrSetBubbleSpecial checks the special record for p's bubble membership. // getOrSetBubbleSpecial checks the special record for p's bubble membership.
// //
// If add is true and p is not associated with any bubble, // If add is true and p is not associated with any bubble,
@ -371,10 +378,12 @@ type specialBubble struct {
// It returns ok==true if p is associated with bubbleid // It returns ok==true if p is associated with bubbleid
// (including if a new association was added), // (including if a new association was added),
// and ok==false if not. // and ok==false if not.
func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (ok bool) { func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (assoc int) {
span := spanOfHeap(uintptr(p)) span := spanOfHeap(uintptr(p))
if span == nil { if span == nil {
throw("getOrSetBubbleSpecial on invalid pointer") // This is probably a package var.
// We can't attach a special to it, so always consider it unbubbled.
return bubbleAssocUnbubbled
} }
// Ensure that the span is swept. // Ensure that the span is swept.
@ -393,7 +402,11 @@ func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (ok bool
// p is already associated with a bubble. // p is already associated with a bubble.
// Return true iff it's the same bubble. // Return true iff it's the same bubble.
s := (*specialBubble)((unsafe.Pointer)(*iter)) s := (*specialBubble)((unsafe.Pointer)(*iter))
ok = s.bubbleid == bubbleid if s.bubbleid == bubbleid {
assoc = bubbleAssocCurrentBubble
} else {
assoc = bubbleAssocOtherBubble
}
} else if add { } else if add {
// p is not associated with a bubble, // p is not associated with a bubble,
// and we've been asked to add an association. // and we've been asked to add an association.
@ -404,23 +417,23 @@ func getOrSetBubbleSpecial(p unsafe.Pointer, bubbleid uint64, add bool) (ok bool
s.special.next = *iter s.special.next = *iter
*iter = (*special)(unsafe.Pointer(s)) *iter = (*special)(unsafe.Pointer(s))
spanHasSpecials(span) spanHasSpecials(span)
ok = true assoc = bubbleAssocCurrentBubble
} else { } else {
// p is not associated with a bubble. // p is not associated with a bubble.
ok = false assoc = bubbleAssocUnbubbled
} }
unlock(&span.speciallock) unlock(&span.speciallock)
releasem(mp) releasem(mp)
return ok return assoc
} }
// synctest_associate associates p with the current bubble. // synctest_associate associates p with the current bubble.
// It returns false if p is already associated with a different bubble. // It returns false if p is already associated with a different bubble.
// //
//go:linkname synctest_associate internal/synctest.associate //go:linkname synctest_associate internal/synctest.associate
func synctest_associate(p unsafe.Pointer) (ok bool) { func synctest_associate(p unsafe.Pointer) int {
return getOrSetBubbleSpecial(p, getg().bubble.id, true) return getOrSetBubbleSpecial(p, getg().bubble.id, true)
} }
@ -435,5 +448,5 @@ func synctest_disassociate(p unsafe.Pointer) {
// //
//go:linkname synctest_isAssociated internal/synctest.isAssociated //go:linkname synctest_isAssociated internal/synctest.isAssociated
func synctest_isAssociated(p unsafe.Pointer) bool { func synctest_isAssociated(p unsafe.Pointer) bool {
return getOrSetBubbleSpecial(p, getg().bubble.id, false) return getOrSetBubbleSpecial(p, getg().bubble.id, false) == bubbleAssocCurrentBubble
} }

View file

@ -5,6 +5,8 @@
package runtime_test package runtime_test
import ( import (
"internal/synctest"
"runtime"
"testing" "testing"
) )
@ -15,3 +17,13 @@ func TestSynctest(t *testing.T) {
t.Fatalf("output:\n%s\n\nwanted:\n%s", output, want) t.Fatalf("output:\n%s\n\nwanted:\n%s", output, want)
} }
} }
// TestSynctestAssocConsts verifies that constants defined
// in both runtime and internal/synctest match.
func TestSynctestAssocConsts(t *testing.T) {
if runtime.BubbleAssocUnbubbled != synctest.Unbubbled ||
runtime.BubbleAssocCurrentBubble != synctest.CurrentBubble ||
runtime.BubbleAssocOtherBubble != synctest.OtherBubble {
t.Fatal("mismatch: runtime.BubbleAssoc? != synctest.*")
}
}

View file

@ -133,6 +133,20 @@ func UpdateGOMAXPROCS() {
mustSetCPUMax(path, 200000) mustSetCPUMax(path, 200000)
mustNotChangeMaxProcs(3) mustNotChangeMaxProcs(3)
// Re-enable updates. Change is immediately visible.
runtime.SetDefaultGOMAXPROCS()
procs = runtime.GOMAXPROCS(0)
println("GOMAXPROCS:", procs)
if procs != 2 {
panic(fmt.Sprintf("GOMAXPROCS got %d want %d", procs, 2))
}
// Setting GOMAXPROCS to itself also disables updates, despite not
// changing the value itself.
runtime.GOMAXPROCS(runtime.GOMAXPROCS(0))
mustSetCPUMax(path, 300000)
mustNotChangeMaxProcs(2)
println("OK") println("OK")
} }

View file

@ -331,7 +331,7 @@ func StopTrace() {
// //
// traceAdvanceSema must not be held. // traceAdvanceSema must not be held.
// //
// traceAdvance is called by golang.org/x/exp/trace using linkname. // traceAdvance is called by runtime/trace and golang.org/x/exp/trace using linkname.
// //
//go:linkname traceAdvance //go:linkname traceAdvance
func traceAdvance(stopTrace bool) { func traceAdvance(stopTrace bool) {
@ -376,7 +376,7 @@ func traceAdvance(stopTrace bool) {
me := getg().m.curg me := getg().m.curg
// We don't have to handle this G status transition because we // We don't have to handle this G status transition because we
// already eliminated ourselves from consideration above. // already eliminated ourselves from consideration above.
casGToWaitingForGC(me, _Grunning, waitReasonTraceGoroutineStatus) casGToWaitingForSuspendG(me, _Grunning, waitReasonTraceGoroutineStatus)
// We need to suspend and take ownership of the G to safely read its // We need to suspend and take ownership of the G to safely read its
// goid. Note that we can't actually emit the event at this point // goid. Note that we can't actually emit the event at this point
// because we might stop the G in a window where it's unsafe to write // because we might stop the G in a window where it's unsafe to write
@ -956,7 +956,7 @@ func traceReader() *g {
// scheduled and should be. Callers should first check that // scheduled and should be. Callers should first check that
// (traceEnabled() || traceShuttingDown()) is true. // (traceEnabled() || traceShuttingDown()) is true.
func traceReaderAvailable() *g { func traceReaderAvailable() *g {
// There are three conditions under which we definitely want to schedule // There are two conditions under which we definitely want to schedule
// the reader: // the reader:
// - The reader is lagging behind in finishing off the last generation. // - The reader is lagging behind in finishing off the last generation.
// In this case, trace buffers could even be empty, but the trace // In this case, trace buffers could even be empty, but the trace
@ -965,12 +965,10 @@ func traceReaderAvailable() *g {
// - The reader has pending work to process for it's reader generation // - The reader has pending work to process for it's reader generation
// (assuming readerGen is not lagging behind). Note that we also want // (assuming readerGen is not lagging behind). Note that we also want
// to be careful *not* to schedule the reader if there's no work to do. // to be careful *not* to schedule the reader if there's no work to do.
// - The trace is shutting down. The trace stopper blocks on the reader
// to finish, much like trace advancement.
// //
// We also want to be careful not to schedule the reader if there's no // We also want to be careful not to schedule the reader if there's no
// reason to. // reason to.
if trace.flushedGen.Load() == trace.readerGen.Load() || trace.workAvailable.Load() || trace.shutdown.Load() { if trace.flushedGen.Load() == trace.readerGen.Load() || trace.workAvailable.Load() {
return trace.reader.Load() return trace.reader.Load()
} }
return nil return nil

View file

@ -126,11 +126,12 @@ func goStatusToTraceGoStatus(status uint32, wr waitReason) tracev2.GoStatus {
// There are a number of cases where a G might end up in // There are a number of cases where a G might end up in
// _Gwaiting but it's actually running in a non-preemptive // _Gwaiting but it's actually running in a non-preemptive
// state but needs to present itself as preempted to the // state but needs to present itself as preempted to the
// garbage collector. In these cases, we're not going to // garbage collector and traceAdvance (via suspendG). In
// emit an event, and we want these goroutines to appear in // these cases, we're not going to emit an event, and we
// the final trace as if they're running, not blocked. // want these goroutines to appear in the final trace as
// if they're running, not blocked.
tgs = tracev2.GoWaiting tgs = tracev2.GoWaiting
if status == _Gwaiting && wr.isWaitingForGC() { if status == _Gwaiting && wr.isWaitingForSuspendG() {
tgs = tracev2.GoRunning tgs = tracev2.GoRunning
} }
case _Gdead: case _Gdead:

View file

@ -51,7 +51,7 @@ type traceTime uint64
// nosplit because it's called from exitsyscall and various trace writing functions, // nosplit because it's called from exitsyscall and various trace writing functions,
// which are nosplit. // which are nosplit.
// //
// traceClockNow is called by golang.org/x/exp/trace using linkname. // traceClockNow is called by runtime/trace and golang.org/x/exp/trace using linkname.
// //
//go:linkname traceClockNow //go:linkname traceClockNow
//go:nosplit //go:nosplit

View file

@ -83,13 +83,17 @@ func (wg *WaitGroup) Add(delta int) {
race.Disable() race.Disable()
defer race.Enable() defer race.Enable()
} }
bubbled := false
if synctest.IsInBubble() { if synctest.IsInBubble() {
// If Add is called from within a bubble, then all Add calls must be made // If Add is called from within a bubble, then all Add calls must be made
// from the same bubble. // from the same bubble.
if !synctest.Associate(wg) { switch synctest.Associate(wg) {
case synctest.Unbubbled:
case synctest.OtherBubble:
// wg is already associated with a different bubble. // wg is already associated with a different bubble.
fatal("sync: WaitGroup.Add called from multiple synctest bubbles") fatal("sync: WaitGroup.Add called from multiple synctest bubbles")
} else { case synctest.CurrentBubble:
bubbled = true
state := wg.state.Or(waitGroupBubbleFlag) state := wg.state.Or(waitGroupBubbleFlag)
if state != 0 && state&waitGroupBubbleFlag == 0 { if state != 0 && state&waitGroupBubbleFlag == 0 {
// Add has been called from outside this bubble. // Add has been called from outside this bubble.
@ -98,7 +102,7 @@ func (wg *WaitGroup) Add(delta int) {
} }
} }
state := wg.state.Add(uint64(delta) << 32) state := wg.state.Add(uint64(delta) << 32)
if state&waitGroupBubbleFlag != 0 && !synctest.IsInBubble() { if state&waitGroupBubbleFlag != 0 && !bubbled {
// Add has been called from within a synctest bubble (and we aren't in one). // Add has been called from within a synctest bubble (and we aren't in one).
fatal("sync: WaitGroup.Add called from inside and outside synctest bubble") fatal("sync: WaitGroup.Add called from inside and outside synctest bubble")
} }
@ -116,7 +120,7 @@ func (wg *WaitGroup) Add(delta int) {
if w != 0 && delta > 0 && v == int32(delta) { if w != 0 && delta > 0 && v == int32(delta) {
panic("sync: WaitGroup misuse: Add called concurrently with Wait") panic("sync: WaitGroup misuse: Add called concurrently with Wait")
} }
if v == 0 && state&waitGroupBubbleFlag != 0 { if v == 0 && bubbled {
// Disassociate the WaitGroup from its bubble. // Disassociate the WaitGroup from its bubble.
synctest.Disassociate(wg) synctest.Disassociate(wg)
if w == 0 { if w == 0 {

View file

@ -0,0 +1,15 @@
// Copyright 2025 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.
package synctest_test
import "testing"
// helperLog is a t.Helper which logs.
// Since it is a helper, the log prefix should contain
// the caller's file, not helper_test.go.
func helperLog(t *testing.T, s string) {
t.Helper()
t.Log(s)
}

View file

@ -93,6 +93,11 @@
// A [sync.WaitGroup] becomes associated with a bubble on the first // A [sync.WaitGroup] becomes associated with a bubble on the first
// call to Add or Go. Once a WaitGroup is associated with a bubble, // call to Add or Go. Once a WaitGroup is associated with a bubble,
// calling Add or Go from outside that bubble is a fatal error. // calling Add or Go from outside that bubble is a fatal error.
// (As a technical limitation, a WaitGroup defined as a package
// variable, such as "var wg sync.WaitGroup", cannot be associated
// with a bubble and operations on it may not be durably blocking.
// This limitation does not apply to a *WaitGroup stored in a
// package variable, such as "var wg = new(sync.WaitGroup)".)
// //
// [sync.Cond.Wait] is durably blocking. Waking a goroutine in a bubble // [sync.Cond.Wait] is durably blocking. Waking a goroutine in a bubble
// blocked on Cond.Wait from outside the bubble is a fatal error. // blocked on Cond.Wait from outside the bubble is a fatal error.

View file

@ -140,6 +140,18 @@ func TestRun(t *testing.T) {
}) })
} }
func TestHelper(t *testing.T) {
runTest(t, []string{"-test.v"}, func() {
synctest.Test(t, func(t *testing.T) {
helperLog(t, "log in helper")
})
}, `^=== RUN TestHelper
synctest_test.go:.* log in helper
--- PASS: TestHelper.*
PASS
$`)
}
func wantPanic(t *testing.T, want string) { func wantPanic(t *testing.T, want string) {
if e := recover(); e != nil { if e := recover(); e != nil {
if got := fmt.Sprint(e); got != want { if got := fmt.Sprint(e); got != want {

View file

@ -1261,6 +1261,9 @@ func (c *common) Skipped() bool {
// When printing file and line information, that function will be skipped. // When printing file and line information, that function will be skipped.
// Helper may be called simultaneously from multiple goroutines. // Helper may be called simultaneously from multiple goroutines.
func (c *common) Helper() { func (c *common) Helper() {
if c.isSynctest {
c = c.parent
}
c.mu.Lock() c.mu.Lock()
defer c.mu.Unlock() defer c.mu.Unlock()
if c.helperPCs == nil { if c.helperPCs == nil {