slog: redesign Record

- Return to the implementation consisting of an inline array followed
  by a slice.

- Document the alias problems that can arise.

- Provide Record.Clone to make a deep copy so aliasing bugs can be
  avoided.

- Replace AddAttr with AddAttrs, to permit more efficiently adding
  Attrs, and to provide more implementation flexibility.

- Eliminate random access: support only forward iteration.
  Accomplish it with a method that takes a func(Attr).
  Call this method "Attrs"; the old "Attrs", which returned a []Attr,
  can now be written in terms of the new one.
  We do not provide a way to stop iteration early, because the
  large majority of uses will want to visit all the Attrs.

- Call the constructor NewRecord, not MakeRecord, because Record isn't
  truly a value type.

Change-Id: Iac6843cd8a27e351fc4bde79aa59c6cc2e526e46
Reviewed-on: https://go-review.googlesource.com/c/exp/+/431276
Reviewed-by: Alan Donovan <adonovan@google.com>
Run-TryBot: Jonathan Amsterdam <jba@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
diff --git a/slog/handler.go b/slog/handler.go
index 964db60..a180e47 100644
--- a/slog/handler.go
+++ b/slog/handler.go
@@ -57,10 +57,10 @@
 		b.WriteString(r.Level().String())
 		b.WriteByte(' ')
 	}
-	for i := 0; i < r.NumAttrs(); i++ {
-		fmt.Fprint(&b, r.Attr(i)) // Attr.Format will print key=value
+	r.Attrs(func(a Attr) {
+		fmt.Fprint(&b, a) // Attr.Format will print key=value
 		b.WriteByte(' ')
-	}
+	})
 	b.WriteString(r.Message())
 	return log.Output(4, b.String())
 }
@@ -205,13 +205,12 @@
 		replace(String(key, val))
 	}
 	*buf = append(*buf, h.preformattedAttrs...)
-	for i := 0; i < r.NumAttrs(); i++ {
-		a := r.Attr(i)
+	r.Attrs(func(a Attr) {
 		if rep != nil {
 			a = rep(a)
 		}
 		appendAttr(app, a)
-	}
+	})
 	app.appendEnd()
 	buf.WriteByte('\n')
 
diff --git a/slog/handler_test.go b/slog/handler_test.go
index 3b90dea..8f72301 100644
--- a/slog/handler_test.go
+++ b/slog/handler_test.go
@@ -36,10 +36,8 @@
 
 func TestCommonHandle(t *testing.T) {
 	tm := time.Now()
-	r := MakeRecord(tm, InfoLevel, "message", 1)
-	r.AddAttr(String("a", "one"))
-	r.AddAttr(Int("b", 2))
-	r.AddAttr(Any("", "ignore me"))
+	r := NewRecord(tm, InfoLevel, "message", 1)
+	r.AddAttrs(String("a", "one"), Int("b", 2), Any("", "ignore me"))
 
 	for _, test := range []struct {
 		name string
diff --git a/slog/json_handler_test.go b/slog/json_handler_test.go
index 371c6b3..48e82c0 100644
--- a/slog/json_handler_test.go
+++ b/slog/json_handler_test.go
@@ -21,8 +21,8 @@
 func TestJSONHandler(t *testing.T) {
 	var buf bytes.Buffer
 	h := NewJSONHandler(&buf)
-	r := MakeRecord(testTime, InfoLevel, "m", 0)
-	r.AddAttr(Int("a", 1))
+	r := NewRecord(testTime, InfoLevel, "m", 0)
+	r.AddAttrs(Int("a", 1))
 	if err := h.Handle(r); err != nil {
 		t.Fatal(err)
 	}
diff --git a/slog/logger.go b/slog/logger.go
index 52b8f2c..5464212 100644
--- a/slog/logger.go
+++ b/slog/logger.go
@@ -47,7 +47,7 @@
 	if len(buf) > 0 && buf[len(buf)-1] == '\n' {
 		buf = buf[:len(buf)-1]
 	}
-	r := MakeRecord(time.Now(), InfoLevel, string(buf), depth)
+	r := NewRecord(time.Now(), InfoLevel, string(buf), depth)
 	return origLen, w.h.Handle(r)
 }
 
@@ -113,7 +113,7 @@
 		return
 	}
 	r := l.makeRecord(msg, level, calldepth)
-	setAttrs(&r, args)
+	r.setAttrsFromArgs(args)
 	_ = l.Handler().Handle(r)
 }
 
@@ -127,39 +127,7 @@
 	if useSourceLine {
 		depth += 5
 	}
-	return MakeRecord(time.Now(), level, msg, depth)
-}
-
-const badKey = "!BADKEY"
-
-func setAttrs(r *Record, args []any) {
-	var attr Attr
-	for len(args) > 0 {
-		attr, args = argsToAttr(args)
-		r.AddAttr(attr)
-	}
-}
-
-// argsToAttr turns a prefix of the args slice into an Attr and returns
-// the unused portion of the slice.
-// If args[0] is an Attr, it returns it.
-// If args[0] is a string, it treats the first two elements as
-// a key-value pair.
-// Otherwise, it treats args[0] as a value with a missing key.
-func argsToAttr(args []any) (Attr, []any) {
-	switch x := args[0].(type) {
-	case string:
-		if len(args) == 1 {
-			return String(badKey, x), nil
-		}
-		return Any(x, args[1]), args[2:]
-
-	case Attr:
-		return x, args[1:]
-
-	default:
-		return Any(badKey, x), args[1:]
-	}
+	return NewRecord(time.Now(), level, msg, depth)
 }
 
 // LogAttrs is a more efficient version of [Logger.Log] that accepts only Attrs.
@@ -174,7 +142,7 @@
 		return
 	}
 	r := l.makeRecord(msg, level, calldepth)
-	r.addAttrs(attrs)
+	r.AddAttrs(attrs...)
 	_ = l.Handler().Handle(r)
 }
 
diff --git a/slog/logger_test.go b/slog/logger_test.go
index f6fcb16..7d7bd73 100644
--- a/slog/logger_test.go
+++ b/slog/logger_test.go
@@ -91,7 +91,7 @@
 	l2.Info("m", "c", 3)
 	h := l2.Handler().(*captureHandler)
 	check(h.attrs, Int("a", 1), Int("b", 2))
-	check(h.r.Attrs(), Int("c", 3))
+	check(attrsSlice(h.r), Int("c", 3))
 }
 
 func TestCallDepth(t *testing.T) {
@@ -199,7 +199,7 @@
 		s := "abc"
 		i := 2000
 		d := time.Second
-		wantAllocs(t, 10, func() {
+		wantAllocs(t, 11, func() {
 			dl.Info("hello",
 				"n", i, "s", s, "d", d,
 				"n", i, "s", s, "d", d,
@@ -253,9 +253,9 @@
 		{[]any{"a", 1, "b"}, []Attr{Int("a", 1), String(badKey, "b")}},
 		{[]any{"a", 1, 2, 3}, []Attr{Int("a", 1), Int(badKey, 2), Int(badKey, 3)}},
 	} {
-		r := MakeRecord(time.Time{}, 0, "", 0)
-		setAttrs(&r, test.args)
-		got := r.Attrs()
+		r := NewRecord(time.Time{}, 0, "", 0)
+		r.setAttrsFromArgs(test.args)
+		got := attrsSlice(r)
 		if !attrsEqual(got, test.want) {
 			t.Errorf("%v:\ngot  %v\nwant %v", test.args, got, test.want)
 		}
diff --git a/slog/record.go b/slog/record.go
index 0f78a96..d54afb1 100644
--- a/slog/record.go
+++ b/slog/record.go
@@ -12,6 +12,9 @@
 const nAttrsInline = 5
 
 // A Record holds information about a log event.
+// Copies of a Record share state.
+// Do not modify a Record after handing out a copy to it.
+// Use [Record.Clone] to create a copy with no shared state.
 type Record struct {
 	// The time at which the output method (Log, Info, etc.) was called.
 	time time.Time
@@ -28,26 +31,28 @@
 
 	// Allocation optimization: an inline array sized to hold
 	// the majority of log calls (based on examination of open-source
-	// code). The array holds the end of the sequence of Attrs.
-	tail [nAttrsInline]Attr
+	// code). It holds the start of the list of Attrs.
+	front [nAttrsInline]Attr
 
-	// The number of Attrs in tail.
-	nTail int
+	// The number of Attrs in front.
+	nFront int
 
-	// The sequence of Attrs except for the tail, represented as a functional
-	// list of arrays.
-	attrs list[[nAttrsInline]Attr]
+	// The list of Attrs except for those in front.
+	// Invariants:
+	//   - len(back) > 0 iff nFront == len(front)
+	//   - Unused array elements are zero. Used to detect mistakes.
+	back []Attr
 }
 
-// MakeRecord creates a new Record from the given arguments.
-// Use [Record.AddAttr] to add attributes to the Record.
+// NewRecord creates a Record from the given arguments.
+// Use [Record.AddAttrs] to add attributes to the Record.
 // If calldepth is greater than zero, [Record.SourceLine] will
 // return the file and line number at that depth,
-// where 1 means the caller of MakeRecord.
+// where 1 means the caller of NewRecord.
 //
-// MakeRecord is intended for logging APIs that want to support a [Handler] as
+// NewRecord is intended for logging APIs that want to support a [Handler] as
 // a backend.
-func MakeRecord(t time.Time, level Level, msg string, calldepth int) Record {
+func NewRecord(t time.Time, level Level, msg string, calldepth int) Record {
 	var p uintptr
 	if calldepth > 0 {
 		p = pc(calldepth + 1)
@@ -85,50 +90,97 @@
 	return f.File, f.Line
 }
 
-// Attrs returns a copy of the sequence of Attrs in r.
-func (r *Record) Attrs() []Attr {
-	res := make([]Attr, 0, r.attrs.len()*nAttrsInline+r.nTail)
-	r.attrs = r.attrs.normalize()
-	for _, f := range r.attrs.front {
-		res = append(res, f[:]...)
+// Clone returns a copy of the record with no shared state.
+// The original record and the clone can both be modified
+// without interfering with each other.
+func (r *Record) Clone() Record {
+	c := *r
+	if len(c.back) > 0 {
+		c.back = make([]Attr, len(c.back))
+		copy(c.back, r.back)
 	}
-	for _, a := range r.tail[:r.nTail] {
-		res = append(res, a)
-	}
-	return res
+	return c
 }
 
-// NumAttrs returns the number of Attrs in r.
+// NumAttrs returns the number of attributes in the Record.
 func (r *Record) NumAttrs() int {
-	return r.attrs.len()*nAttrsInline + r.nTail
+	return r.nFront + len(r.back)
 }
 
-// Attr returns the i'th Attr in r.
-func (r *Record) Attr(i int) Attr {
-	if r.attrs.back != nil {
-		r.attrs = r.attrs.normalize()
+// Attrs calls f on each Attr in the Record.
+func (r *Record) Attrs(f func(Attr)) {
+	for i := 0; i < r.nFront; i++ {
+		f(r.front[i])
 	}
-	alen := r.attrs.len() * nAttrsInline
-	if i < alen {
-		return r.attrs.at(i / nAttrsInline)[i%nAttrsInline]
+	for _, a := range r.back {
+		f(a)
 	}
-	return r.tail[i-alen]
 }
 
-// AddAttr appends an attributes to the record's list of attributes.
-// It does not check for duplicate keys.
-func (r *Record) AddAttr(a Attr) {
-	if r.nTail == len(r.tail) {
-		r.attrs = r.attrs.append(r.tail)
-		r.nTail = 0
+// AddAttrs appends the given attrs to the Record's list of Attrs.
+func (r *Record) AddAttrs(attrs ...Attr) {
+	n := copy(r.front[r.nFront:], attrs)
+	r.nFront += n
+	// Check if a copy was modified by slicing past the end
+	// and seeing if the Attr there is non-zero.
+	if cap(r.back) > len(r.back) {
+		end := r.back[:len(r.back)+1][len(r.back)]
+		if end != (Attr{}) {
+			panic("copies of a slog.Record were both modified")
+		}
 	}
-	r.tail[r.nTail] = a
-	r.nTail++
+	r.back = append(r.back, attrs[n:]...)
 }
 
-func (r *Record) addAttrs(attrs []Attr) {
-	// TODO: be cleverer.
-	for _, a := range attrs {
-		r.AddAttr(a)
+func (r *Record) setAttrsFromArgs(args []any) {
+	var a Attr
+	for len(args) > 0 {
+		a, args = argsToAttr(args)
+		if r.nFront < len(r.front) {
+			r.front[r.nFront] = a
+			r.nFront++
+		} else {
+			if r.back == nil {
+				r.back = make([]Attr, 0, countAttrs(args))
+			}
+			r.back = append(r.back, a)
+		}
+	}
+
+}
+
+// countAttrs returns the number of Attrs that would be created from args.
+func countAttrs(args []any) int {
+	n := 0
+	for i := 0; i < len(args); i++ {
+		n++
+		if _, ok := args[i].(string); ok {
+			i++
+		}
+	}
+	return n
+}
+
+const badKey = "!BADKEY"
+
+// argsToAttr turns a prefix of the nonempty args slice into an Attr
+// and returns the unconsumed portion of the slice.
+// If args[0] is an Attr, it returns it.
+// If args[0] is a string, it treats the first two elements as
+// a key-value pair.
+// Otherwise, it treats args[0] as a value with a missing key.
+func argsToAttr(args []any) (Attr, []any) {
+	switch x := args[0].(type) {
+	case string:
+		if len(args) == 1 {
+			return String(badKey, x), nil
+		}
+		return Any(x, args[1]), args[2:]
+
+	case Attr:
+		return x, args[1:]
+
+	default:
+		return Any(badKey, x), args[1:]
 	}
 }
diff --git a/slog/record_test.go b/slog/record_test.go
index 72baf4e..e526d24 100644
--- a/slog/record_test.go
+++ b/slog/record_test.go
@@ -20,7 +20,7 @@
 	if g, w := r.NumAttrs(), len(as); g != w {
 		t.Errorf("NumAttrs: got %d, want %d", g, w)
 	}
-	if got := r.Attrs(); !attrsEqual(got, as) {
+	if got := attrsSlice(r); !attrsEqual(got, as) {
 		t.Errorf("got %v, want %v", got, as)
 	}
 }
@@ -36,7 +36,7 @@
 		{-16, "", false},
 		{1, "record.go", true},
 	} {
-		r := MakeRecord(time.Time{}, 0, "", test.depth)
+		r := NewRecord(time.Time{}, 0, "", test.depth)
 		gotFile, gotLine := r.SourceLine()
 		if i := strings.LastIndexByte(gotFile, '/'); i >= 0 {
 			gotFile = gotFile[i+1:]
@@ -48,7 +48,7 @@
 	}
 }
 
-func TestAliasing(t *testing.T) {
+func TestAliasingAndClone(t *testing.T) {
 	intAttrs := func(from, to int) []Attr {
 		var as []Attr
 		for i := from; i < to; i++ {
@@ -57,38 +57,50 @@
 		return as
 	}
 
-	check := func(r *Record, want []Attr) {
+	check := func(r Record, want []Attr) {
 		t.Helper()
-		got := r.Attrs()
+		got := attrsSlice(r)
 		if !attrsEqual(got, want) {
 			t.Errorf("got %v, want %v", got, want)
 		}
 	}
 
-	r1 := MakeRecord(time.Time{}, 0, "", 0)
-	for i := 0; i < nAttrsInline+3; i++ {
-		r1.AddAttr(Int("k", i))
-	}
-	check(&r1, intAttrs(0, nAttrsInline+3))
+	// Create a record whose Attrs overflow the inline array,
+	// creating a slice in r.back.
+	r1 := NewRecord(time.Time{}, 0, "", 0)
+	r1.AddAttrs(intAttrs(0, nAttrsInline+1)...)
+	// Ensure that r1.back's capacity exceeds its length.
+	b := make([]Attr, len(r1.back), len(r1.back)+1)
+	copy(b, r1.back)
+	r1.back = b
+	// Make a copy that shares state.
 	r2 := r1
-	check(&r2, intAttrs(0, nAttrsInline+3))
-	// if cap(r1.attrs2) <= len(r1.attrs2) {
-	// 	t.Fatal("cap not greater than len")
-	// }
-	r1.AddAttr(Int("k", nAttrsInline+3))
-	r2.AddAttr(Int("k", -1))
-	check(&r1, intAttrs(0, nAttrsInline+4))
-	check(&r2, append(intAttrs(0, nAttrsInline+3), Int("k", -1)))
+	// Adding to both should panic.
+	r1.AddAttrs(Int("p", 0))
+	if !panics(func() { r2.AddAttrs(Int("p", 1)) }) {
+		t.Error("expected panic")
+	}
+	r1Attrs := attrsSlice(r1)
+	// Adding to a clone is fine.
+	r2 = r1.Clone()
+	check(r2, r1Attrs)
+	r2.AddAttrs(Int("p", 2))
+	check(r1, r1Attrs) // r1 is unchanged
+	check(r2, append(slices.Clip(r1Attrs), Int("p", 2)))
 }
 
 func newRecordWithAttrs(as []Attr) Record {
-	r := MakeRecord(time.Now(), InfoLevel, "", 0)
-	for _, a := range as {
-		r.AddAttr(a)
-	}
+	r := NewRecord(time.Now(), InfoLevel, "", 0)
+	r.AddAttrs(as...)
 	return r
 }
 
+func attrsSlice(r Record) []Attr {
+	s := make([]Attr, 0, r.NumAttrs())
+	r.Attrs(func(a Attr) { s = append(s, a) })
+	return s
+}
+
 func attrsEqual(as1, as2 []Attr) bool {
 	return slices.EqualFunc(as1, as2, Attr.Equal)
 }
@@ -105,7 +117,7 @@
 }
 
 func BenchmarkSourceLine(b *testing.B) {
-	r := MakeRecord(time.Now(), InfoLevel, "", 5)
+	r := NewRecord(time.Now(), InfoLevel, "", 5)
 	b.Run("alone", func(b *testing.B) {
 		for i := 0; i < b.N; i++ {
 			file, line := r.SourceLine()
@@ -132,13 +144,11 @@
 	var a Attr
 
 	for i := 0; i < b.N; i++ {
-		r := MakeRecord(time.Time{}, InfoLevel, "", 0)
+		r := NewRecord(time.Time{}, InfoLevel, "", 0)
 		for j := 0; j < nAttrs; j++ {
-			r.AddAttr(Int("k", j))
+			r.AddAttrs(Int("k", j))
 		}
-		for j := 0; j < nAttrs; j++ {
-			a = r.Attr(j)
-		}
+		r.Attrs(func(b Attr) { a = b })
 	}
 	_ = a
 }
diff --git a/slog/text_handler_test.go b/slog/text_handler_test.go
index f95d735..055652b 100644
--- a/slog/text_handler_test.go
+++ b/slog/text_handler_test.go
@@ -54,8 +54,8 @@
 		t.Run(test.name, func(t *testing.T) {
 			var buf bytes.Buffer
 			h := NewTextHandler(&buf)
-			r := MakeRecord(testTime, InfoLevel, "a message", 0)
-			r.AddAttr(test.attr)
+			r := NewRecord(testTime, InfoLevel, "a message", 0)
+			r.AddAttrs(test.attr)
 			if err := h.Handle(r); err != nil {
 				t.Fatal(err)
 			}
@@ -114,7 +114,7 @@
 func TestTextHandlerSource(t *testing.T) {
 	var buf bytes.Buffer
 	h := HandlerOptions{AddSource: true}.NewTextHandler(&buf)
-	r := MakeRecord(testTime, InfoLevel, "m", 2)
+	r := NewRecord(testTime, InfoLevel, "m", 2)
 	if err := h.Handle(r); err != nil {
 		t.Fatal(err)
 	}
@@ -142,8 +142,8 @@
 	var h Handler = NewTextHandler(&buf)
 	h = h.With([]Attr{Duration("dur", time.Minute), Bool("b", true)})
 	// Also test omitting time and level.
-	r := MakeRecord(time.Time{}, 0, "m", 0)
-	r.AddAttr(Int("a", 1))
+	r := NewRecord(time.Time{}, 0, "m", 0)
+	r.AddAttrs(Int("a", 1))
 	if err := h.Handle(r); err != nil {
 		t.Fatal(err)
 	}
@@ -155,9 +155,9 @@
 }
 
 func TestTextHandlerAlloc(t *testing.T) {
-	r := MakeRecord(time.Now(), InfoLevel, "msg", 0)
+	r := NewRecord(time.Now(), InfoLevel, "msg", 0)
 	for i := 0; i < 10; i++ {
-		r.AddAttr(Int("x = y", i))
+		r.AddAttrs(Int("x = y", i))
 	}
 	h := NewTextHandler(io.Discard)
 	wantAllocs(t, 0, func() { h.Handle(r) })