Merge branch 'master' into selfref
diff --git a/pretty/examples_test.go b/pretty/examples_test.go
index df3b32e..8578b95 100644
--- a/pretty/examples_test.go
+++ b/pretty/examples_test.go
@@ -308,3 +308,66 @@
 	// + Stolen: false,
 	//  }
 }
+
+type ListNode struct {
+	Value int
+	Next  *ListNode
+}
+
+func circular(nodes int) *ListNode {
+	final := &ListNode{
+		Value: nodes,
+	}
+	final.Next = final
+
+	recent := final
+	for i := nodes - 1; i > 0; i-- {
+		n := &ListNode{
+			Value: i,
+			Next:  recent,
+		}
+		final.Next = n
+		recent = n
+	}
+	return recent
+}
+
+func ExamplePrint_withCycles() {
+	pretty.CycleTracker.Print(circular(3))
+
+	// Output:
+	// <#1> {
+	//  Value: 1,
+	//  Next: {
+	//   Value: 2,
+	//   Next: {
+	//    Value: 3,
+	//    Next: <see #1>,
+	//   },
+	//  },
+	// }
+}
+
+func ExampleCompare_withCycles() {
+	got, want := circular(3), circular(3)
+
+	// Make the got one broken
+	got.Next.Next.Next = got.Next
+
+	fmt.Printf("Diff: (-got +want)\n%s", pretty.CycleTracker.Compare(got, want))
+
+	// Output:
+	// Diff: (-got +want)
+	// -{
+	// +<#1> {
+	//   Value: 1,
+	// - Next: <#1> {
+	// + Next: {
+	//    Value: 2,
+	//    Next: {
+	//     Value: 3,
+	//     Next: <see #1>,
+	//    },
+	//   },
+	//  }
+}
diff --git a/pretty/public.go b/pretty/public.go
index 948691a..2ad2067 100644
--- a/pretty/public.go
+++ b/pretty/public.go
@@ -66,6 +66,13 @@
 	// method and the input will be provided as a value.  In that case,
 	// use a function that calls .String on the formal value parameter.
 	Formatter map[reflect.Type]interface{}
+
+	// If TrackCycles is enabled, pretty will detect and track
+	// self-referential structures. If a self-referential structure (aka a
+	// "recursive" value) is detected, numbered placeholders will be emitted.
+	//
+	// Pointer tracking is disabled by default for performance reasons.
+	TrackCycles bool
 }
 
 // Default Config objects
@@ -88,14 +95,27 @@
 	DefaultConfig = &Config{
 		Formatter: DefaultFormatter,
 	}
+
+	// CycleTracker is a convenience config for formatting and comparing recursive structures.
+	CycleTracker = &Config{
+		Diffable:    true,
+		Formatter:   DefaultFormatter,
+		TrackCycles: true,
+	}
 )
 
 func (cfg *Config) fprint(buf *bytes.Buffer, vals ...interface{}) {
+	ref := &reflector{
+		Config: cfg,
+	}
+	if cfg.TrackCycles {
+		ref.pointerTracker = new(pointerTracker)
+	}
 	for i, val := range vals {
 		if i > 0 {
 			buf.WriteByte('\n')
 		}
-		cfg.val2node(reflect.ValueOf(val)).WriteTo(buf, "", cfg)
+		newFormatter(cfg, buf).write(ref.val2node(reflect.ValueOf(val)))
 	}
 }
 
diff --git a/pretty/reflect.go b/pretty/reflect.go
index e743a9a..5cd30b7 100644
--- a/pretty/reflect.go
+++ b/pretty/reflect.go
@@ -29,25 +29,109 @@
 	return reflect.DeepEqual(val.Interface(), z)
 }
 
-func (c *Config) val2node(val reflect.Value) node {
-	// TODO(kevlar): pointer tracking?
+// pointerTracker is a helper for tracking pointer chasing to detect cycles.
+type pointerTracker struct {
+	addrs map[uintptr]int // addr[address] = seen count
 
+	lastID int
+	ids    map[uintptr]int // ids[address] = id
+}
+
+// track tracks following a reference (pointer, slice, map, etc).  Every call to
+// track should be paired with a call to untrack.
+func (p *pointerTracker) track(ptr uintptr) {
+	if p.addrs == nil {
+		p.addrs = make(map[uintptr]int)
+	}
+	p.addrs[ptr]++
+}
+
+// untrack registers that we have backtracked over the reference to the pointer.
+func (p *pointerTracker) untrack(ptr uintptr) {
+	p.addrs[ptr]--
+	if p.addrs[ptr] == 0 {
+		delete(p.addrs, ptr)
+	}
+}
+
+// seen returns whether the pointer was previously seen along this path.
+func (p *pointerTracker) seen(ptr uintptr) bool {
+	_, ok := p.addrs[ptr]
+	return ok
+}
+
+// keep allocates an ID for the given address and returns it.
+func (p *pointerTracker) keep(ptr uintptr) int {
+	if p.ids == nil {
+		p.ids = make(map[uintptr]int)
+	}
+	if _, ok := p.ids[ptr]; !ok {
+		p.lastID++
+		p.ids[ptr] = p.lastID
+	}
+	return p.ids[ptr]
+}
+
+// id returns the ID for the given address.
+func (p *pointerTracker) id(ptr uintptr) (int, bool) {
+	if p.ids == nil {
+		p.ids = make(map[uintptr]int)
+	}
+	id, ok := p.ids[ptr]
+	return id, ok
+}
+
+// reflector adds local state to the recursive reflection logic.
+type reflector struct {
+	*Config
+	*pointerTracker
+}
+
+// follow handles following a possiblly-recursive reference to the given value
+// from the given ptr address.
+func (r *reflector) follow(ptr uintptr, val reflect.Value) node {
+	if r.pointerTracker == nil {
+		// Tracking disabled
+		return r.val2node(val)
+	}
+
+	// If a parent already followed this, emit a reference marker
+	if r.seen(ptr) {
+		id := r.keep(ptr)
+		return ref{id}
+	}
+
+	// Track the pointer we're following while on this recursive branch
+	r.track(ptr)
+	defer r.untrack(ptr)
+	n := r.val2node(val)
+
+	// If the recursion used this ptr, wrap it with a target marker
+	if id, ok := r.id(ptr); ok {
+		return target{id, n}
+	}
+
+	// Otherwise, return the node unadulterated
+	return n
+}
+
+func (r *reflector) val2node(val reflect.Value) node {
 	if !val.IsValid() {
 		return rawVal("nil")
 	}
 
 	if val.CanInterface() {
 		v := val.Interface()
-		if formatter, ok := c.Formatter[val.Type()]; ok {
+		if formatter, ok := r.Formatter[val.Type()]; ok {
 			if formatter != nil {
 				res := reflect.ValueOf(formatter).Call([]reflect.Value{val})
 				return rawVal(res[0].Interface().(string))
 			}
 		} else {
-			if s, ok := v.(fmt.Stringer); ok && c.PrintStringers {
+			if s, ok := v.(fmt.Stringer); ok && r.PrintStringers {
 				return stringVal(s.String())
 			}
-			if t, ok := v.(encoding.TextMarshaler); ok && c.PrintTextMarshalers {
+			if t, ok := v.(encoding.TextMarshaler); ok && r.PrintTextMarshalers {
 				if raw, err := t.MarshalText(); err == nil { // if NOT an error
 					return stringVal(string(raw))
 				}
@@ -56,28 +140,53 @@
 	}
 
 	switch kind := val.Kind(); kind {
-	case reflect.Ptr, reflect.Interface:
+	case reflect.Ptr:
 		if val.IsNil() {
 			return rawVal("nil")
 		}
-		return c.val2node(val.Elem())
+		return r.follow(val.Pointer(), val.Elem())
+	case reflect.Interface:
+		if val.IsNil() {
+			return rawVal("nil")
+		}
+		return r.val2node(val.Elem())
 	case reflect.String:
 		return stringVal(val.String())
-	case reflect.Slice, reflect.Array:
+	case reflect.Slice:
+		n := list{}
+		length := val.Len()
+		ptr := val.Pointer()
+		for i := 0; i < length; i++ {
+			n = append(n, r.follow(ptr, val.Index(i)))
+		}
+		return n
+	case reflect.Array:
 		n := list{}
 		length := val.Len()
 		for i := 0; i < length; i++ {
-			n = append(n, c.val2node(val.Index(i)))
+			n = append(n, r.val2node(val.Index(i)))
 		}
 		return n
 	case reflect.Map:
-		n := keyvals{}
+		// Extract the keys and sort them for stable iteration
 		keys := val.MapKeys()
+		pairs := make([]mapPair, 0, len(keys))
 		for _, key := range keys {
-			// TODO(kevlar): Support arbitrary type keys?
-			n = append(n, keyval{compactString(c.val2node(key)), c.val2node(val.MapIndex(key))})
+			pairs = append(pairs, mapPair{
+				key:   new(formatter).compactString(r.val2node(key)), // can't be cyclic
+				value: val.MapIndex(key),
+			})
 		}
-		sort.Sort(n)
+		sort.Sort(byKey(pairs))
+
+		// Process the keys into the final representation
+		ptr, n := val.Pointer(), keyvals{}
+		for _, pair := range pairs {
+			n = append(n, keyval{
+				key: pair.key,
+				val: r.follow(ptr, pair.value),
+			})
+		}
 		return n
 	case reflect.Struct:
 		n := keyvals{}
@@ -85,14 +194,14 @@
 		fields := typ.NumField()
 		for i := 0; i < fields; i++ {
 			sf := typ.Field(i)
-			if !c.IncludeUnexported && sf.PkgPath != "" {
+			if !r.IncludeUnexported && sf.PkgPath != "" {
 				continue
 			}
 			field := val.Field(i)
-			if c.SkipZeroFields && isZeroVal(field) {
+			if r.SkipZeroFields && isZeroVal(field) {
 				continue
 			}
-			n = append(n, keyval{sf.Name, c.val2node(field)})
+			n = append(n, keyval{sf.Name, r.val2node(field)})
 		}
 		return n
 	case reflect.Bool:
@@ -119,3 +228,14 @@
 
 	return rawVal(val.String())
 }
+
+type mapPair struct {
+	key   string
+	value reflect.Value
+}
+
+type byKey []mapPair
+
+func (v byKey) Len() int           { return len(v) }
+func (v byKey) Swap(i, j int)      { v[i], v[j] = v[j], v[i] }
+func (v byKey) Less(i, j int) bool { return v[i].key < v[j].key }
diff --git a/pretty/reflect_test.go b/pretty/reflect_test.go
index ef5008e..626d78a 100644
--- a/pretty/reflect_test.go
+++ b/pretty/reflect_test.go
@@ -121,7 +121,10 @@
 	}
 
 	for _, test := range tests {
-		if got, want := DefaultConfig.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) {
+		ref := &reflector{
+			Config: DefaultConfig,
+		}
+		if got, want := ref.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) {
 			t.Errorf("%s: got %#v, want %#v", test.desc, got, want)
 		}
 	}
@@ -196,15 +199,142 @@
 				{"Date", stringVal("2009-02-13 23:31:30 +0000 UTC")},
 			},
 		},
+		{
+			desc: "circular list",
+			raw:  circular(3),
+			cfg:  CycleTracker,
+			want: target{1, keyvals{
+				{"Value", rawVal("1")},
+				{"Next", keyvals{
+					{"Value", rawVal("2")},
+					{"Next", keyvals{
+						{"Value", rawVal("3")},
+						{"Next", ref{1}},
+					}},
+				}},
+			}},
+		},
+		{
+			desc: "self referential maps",
+			raw:  selfRef(),
+			cfg:  CycleTracker,
+			want: target{1, keyvals{
+				{"ID", rawVal("1")},
+				{"Child", keyvals{
+					{"2", target{2, keyvals{
+						{"ID", rawVal("2")},
+						{"Child", keyvals{
+							{"3", target{3, keyvals{
+								{"ID", rawVal("3")},
+								{"Child", keyvals{
+									{"1", ref{1}},
+									{"2", ref{2}},
+									{"3", ref{3}},
+								}},
+							}}},
+						}},
+					}}},
+				}},
+			}},
+		},
+		{
+			desc: "maps of cycles",
+			raw: map[string]*ListNode{
+				"1. one":   circular(1),
+				"2. two":   circular(2),
+				"3. three": circular(1),
+			},
+			cfg: CycleTracker,
+			want: keyvals{
+				{"1. one", target{1, keyvals{
+					{"Value", rawVal("1")},
+					{"Next", ref{1}},
+				}}},
+				{"2. two", target{2, keyvals{
+					{"Value", rawVal("1")},
+					{"Next", keyvals{
+						{"Value", rawVal("2")},
+						{"Next", ref{2}},
+					}},
+				}}},
+				{"3. three", target{3, keyvals{
+					{"Value", rawVal("1")},
+					{"Next", ref{3}},
+				}}},
+			},
+		},
 	}
 
 	for _, test := range tests {
-		if got, want := test.cfg.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) {
-			t.Errorf("%s: got %#v, want %#v", test.desc, got, want)
+		ref := &reflector{
+			Config: test.cfg,
+		}
+		if test.cfg.TrackCycles {
+			ref.pointerTracker = new(pointerTracker)
+		}
+		if got, want := ref.val2node(reflect.ValueOf(test.raw)), test.want; !reflect.DeepEqual(got, want) {
+			t.Run(test.desc, func(t *testing.T) {
+				t.Errorf(" got %#v", got)
+				t.Errorf("want %#v", want)
+				t.Errorf("Diff: (-got +want)\n%s", Compare(got, want))
+			})
 		}
 	}
 }
 
+type ListNode struct {
+	Value int
+	Next  *ListNode
+}
+
+func circular(nodes int) *ListNode {
+	final := &ListNode{
+		Value: nodes,
+	}
+	final.Next = final
+
+	recent := final
+	for i := nodes - 1; i > 0; i-- {
+		n := &ListNode{
+			Value: i,
+			Next:  recent,
+		}
+		final.Next = n
+		recent = n
+	}
+	return recent
+}
+
+type SelfReferential struct {
+	ID    int
+	Child map[int]*SelfReferential
+}
+
+func selfRef() *SelfReferential {
+	sr1 := &SelfReferential{
+		ID:    1,
+		Child: make(map[int]*SelfReferential),
+	}
+	sr2 := &SelfReferential{
+		ID:    2,
+		Child: make(map[int]*SelfReferential),
+	}
+	sr3 := &SelfReferential{
+		ID:    3,
+		Child: make(map[int]*SelfReferential),
+	}
+
+	// Build a cycle
+	sr1.Child[2] = sr2
+	sr2.Child[3] = sr3
+	sr3.Child[1] = sr1
+
+	// Throw in some other stuff for funzies
+	sr3.Child[2] = sr2
+	sr3.Child[3] = sr3
+	return sr1
+}
+
 func BenchmarkVal2node(b *testing.B) {
 	benchmarks := []struct {
 		desc string
@@ -214,7 +344,8 @@
 		{
 			desc: "struct",
 			cfg:  DefaultConfig,
-			raw:  struct{ Zaphod, Ford string }{"beeblebrox", "prefect"}},
+			raw:  struct{ Zaphod, Ford string }{"beeblebrox", "prefect"},
+		},
 		{
 			desc: "map",
 			cfg:  DefaultConfig,
@@ -224,12 +355,62 @@
 				[2]int{1, 3}:  "home",
 			},
 		},
+		{
+			desc: "track/struct",
+			cfg:  CycleTracker,
+			raw:  struct{ Zaphod, Ford string }{"beeblebrox", "prefect"},
+		},
+		{
+			desc: "track/map",
+			cfg:  CycleTracker,
+			raw: map[[2]int]string{
+				[2]int{-1, 2}: "school",
+				[2]int{0, 0}:  "origin",
+				[2]int{1, 3}:  "home",
+			},
+		},
+		{
+			desc: "circlist/small",
+			cfg:  CycleTracker,
+			raw:  circular(3),
+		},
+		{
+			desc: "circlist/med",
+			cfg:  CycleTracker,
+			raw:  circular(300),
+		},
+		{
+			desc: "circlist/large",
+			cfg:  CycleTracker,
+			raw:  circular(3000),
+		},
+		{
+			desc: "mapofcirc/small",
+			cfg:  CycleTracker,
+			raw: map[string]*ListNode{
+				"1. one":   circular(1),
+				"2. two":   circular(2),
+				"3. three": circular(1),
+			},
+		},
+		{
+			desc: "selfrefmap/small",
+			cfg:  CycleTracker,
+			raw:  selfRef,
+		},
 	}
 
 	for _, bench := range benchmarks {
 		b.Run(bench.desc, func(b *testing.B) {
+			b.ReportAllocs()
 			for i := 0; i < b.N; i++ {
-				bench.cfg.val2node(reflect.ValueOf(bench.raw))
+				ref := &reflector{
+					Config: bench.cfg,
+				}
+				if bench.cfg.TrackCycles {
+					ref.pointerTracker = new(pointerTracker)
+				}
+				ref.val2node(reflect.ValueOf(bench.raw))
 			}
 		})
 	}
diff --git a/pretty/structure.go b/pretty/structure.go
index a2f3bb7..d876f60 100644
--- a/pretty/structure.go
+++ b/pretty/structure.go
@@ -15,16 +15,56 @@
 package pretty
 
 import (
+	"bufio"
 	"bytes"
+	"fmt"
+	"io"
 	"strconv"
 	"strings"
 )
 
-type node interface {
-	WriteTo(w *bytes.Buffer, indent string, cfg *Config)
+// a formatter stores stateful formatting information as well as being
+// an io.Writer for simplicity.
+type formatter struct {
+	*bufio.Writer
+	*Config
+
+	// Self-referential structure tracking
+	tagNumbers map[int]int // tagNumbers[id] = <#n>
 }
 
-func compactString(n node) string {
+// newFormatter creates a new buffered formatter.  For the output to be written
+// to the given writer, this must be accompanied by a call to write (or Flush).
+func newFormatter(cfg *Config, w io.Writer) *formatter {
+	return &formatter{
+		Writer:     bufio.NewWriter(w),
+		Config:     cfg,
+		tagNumbers: make(map[int]int),
+	}
+}
+
+func (f *formatter) write(n node) {
+	defer f.Flush()
+	n.format(f, "")
+}
+
+func (f *formatter) tagFor(id int) int {
+	if tag, ok := f.tagNumbers[id]; ok {
+		return tag
+	}
+	if f.tagNumbers == nil {
+		return 0
+	}
+	tag := len(f.tagNumbers) + 1
+	f.tagNumbers[id] = tag
+	return tag
+}
+
+type node interface {
+	format(f *formatter, indent string)
+}
+
+func (f *formatter) compactString(n node) string {
 	switch k := n.(type) {
 	case stringVal:
 		return string(k)
@@ -33,20 +73,22 @@
 	}
 
 	buf := new(bytes.Buffer)
-	n.WriteTo(buf, "", &Config{Compact: true})
+	f2 := newFormatter(&Config{Compact: true}, buf)
+	f2.tagNumbers = f.tagNumbers // reuse tagNumbers just in case
+	f2.write(n)
 	return buf.String()
 }
 
 type stringVal string
 
-func (str stringVal) WriteTo(w *bytes.Buffer, indent string, cfg *Config) {
-	w.WriteString(strconv.Quote(string(str)))
+func (str stringVal) format(f *formatter, indent string) {
+	f.WriteString(strconv.Quote(string(str)))
 }
 
 type rawVal string
 
-func (r rawVal) WriteTo(w *bytes.Buffer, indent string, cfg *Config) {
-	w.WriteString(string(r))
+func (r rawVal) format(f *formatter, indent string) {
+	f.WriteString(string(r))
 }
 
 type keyval struct {
@@ -56,36 +98,32 @@
 
 type keyvals []keyval
 
-func (l keyvals) Len() int           { return len(l) }
-func (l keyvals) Swap(i, j int)      { l[i], l[j] = l[j], l[i] }
-func (l keyvals) Less(i, j int) bool { return l[i].key < l[j].key }
-
-func (l keyvals) WriteTo(w *bytes.Buffer, indent string, cfg *Config) {
-	w.WriteByte('{')
+func (l keyvals) format(f *formatter, indent string) {
+	f.WriteByte('{')
 
 	switch {
-	case cfg.Compact:
+	case f.Compact:
 		// All on one line:
 		for i, kv := range l {
 			if i > 0 {
-				w.WriteByte(',')
+				f.WriteByte(',')
 			}
-			w.WriteString(kv.key)
-			w.WriteByte(':')
-			kv.val.WriteTo(w, indent, cfg)
+			f.WriteString(kv.key)
+			f.WriteByte(':')
+			kv.val.format(f, indent)
 		}
-	case cfg.Diffable:
-		w.WriteByte('\n')
+	case f.Diffable:
+		f.WriteByte('\n')
 		inner := indent + " "
 		// Each value gets its own line:
 		for _, kv := range l {
-			w.WriteString(inner)
-			w.WriteString(kv.key)
-			w.WriteString(": ")
-			kv.val.WriteTo(w, inner, cfg)
-			w.WriteString(",\n")
+			f.WriteString(inner)
+			f.WriteString(kv.key)
+			f.WriteString(": ")
+			kv.val.format(f, inner)
+			f.WriteString(",\n")
 		}
-		w.WriteString(indent)
+		f.WriteString(indent)
 	default:
 		keyWidth := 0
 		for _, kv := range l {
@@ -99,62 +137,87 @@
 		// First and last line shared with bracket:
 		for i, kv := range l {
 			if i > 0 {
-				w.WriteString(",\n")
-				w.WriteString(alignKey)
+				f.WriteString(",\n")
+				f.WriteString(alignKey)
 			}
-			w.WriteString(kv.key)
-			w.WriteString(": ")
-			w.WriteString(alignValue[len(kv.key):])
-			kv.val.WriteTo(w, inner, cfg)
+			f.WriteString(kv.key)
+			f.WriteString(": ")
+			f.WriteString(alignValue[len(kv.key):])
+			kv.val.format(f, inner)
 		}
 	}
 
-	w.WriteByte('}')
+	f.WriteByte('}')
 }
 
 type list []node
 
-func (l list) WriteTo(w *bytes.Buffer, indent string, cfg *Config) {
-	if max := cfg.ShortList; max > 0 {
-		short := compactString(l)
+func (l list) format(f *formatter, indent string) {
+	if max := f.ShortList; max > 0 {
+		short := f.compactString(l)
 		if len(short) <= max {
-			w.WriteString(short)
+			f.WriteString(short)
 			return
 		}
 	}
 
-	w.WriteByte('[')
+	f.WriteByte('[')
 
 	switch {
-	case cfg.Compact:
+	case f.Compact:
 		// All on one line:
 		for i, v := range l {
 			if i > 0 {
-				w.WriteByte(',')
+				f.WriteByte(',')
 			}
-			v.WriteTo(w, indent, cfg)
+			v.format(f, indent)
 		}
-	case cfg.Diffable:
-		w.WriteByte('\n')
+	case f.Diffable:
+		f.WriteByte('\n')
 		inner := indent + " "
 		// Each value gets its own line:
 		for _, v := range l {
-			w.WriteString(inner)
-			v.WriteTo(w, inner, cfg)
-			w.WriteString(",\n")
+			f.WriteString(inner)
+			v.format(f, inner)
+			f.WriteString(",\n")
 		}
-		w.WriteString(indent)
+		f.WriteString(indent)
 	default:
 		inner := indent + " "
 		// First and last line shared with bracket:
 		for i, v := range l {
 			if i > 0 {
-				w.WriteString(",\n")
-				w.WriteString(inner)
+				f.WriteString(",\n")
+				f.WriteString(inner)
 			}
-			v.WriteTo(w, inner, cfg)
+			v.format(f, inner)
 		}
 	}
 
-	w.WriteByte(']')
+	f.WriteByte(']')
+}
+
+type ref struct {
+	id int
+}
+
+func (r ref) format(f *formatter, indent string) {
+	fmt.Fprintf(f, "<see #%d>", f.tagFor(r.id))
+}
+
+type target struct {
+	id    int
+	value node
+}
+
+func (t target) format(f *formatter, indent string) {
+	tag := fmt.Sprintf("<#%d> ", f.tagFor(t.id))
+	switch {
+	case f.Diffable, f.Compact:
+		// no indent changes
+	default:
+		indent += strings.Repeat(" ", len(tag))
+	}
+	f.WriteString(tag)
+	t.value.format(f, indent)
 }
diff --git a/pretty/structure_test.go b/pretty/structure_test.go
index 5fd2d9a..7fb831c 100644
--- a/pretty/structure_test.go
+++ b/pretty/structure_test.go
@@ -20,7 +20,7 @@
 	"testing"
 )
 
-func TestWriteTo(t *testing.T) {
+func TestFormat(t *testing.T) {
 	tests := []struct {
 		desc string
 		node node
@@ -160,20 +160,75 @@
  },
 ]`,
 		},
+		{
+			desc: "recursive",
+			node: target{1, keyvals{
+				{"Value", rawVal("1")},
+				{"Next", keyvals{
+					{"Value", rawVal("2")},
+					{"Next", keyvals{
+						{"Value", rawVal("3")},
+						{"Next", ref{1}},
+					}},
+				}},
+			}},
+			normal: `
+<#1> {Value: 1,
+      Next:  {Value: 2,
+              Next:  {Value: 3,
+                      Next:  <see #1>}}}`,
+			diffable: `
+<#1> {
+ Value: 1,
+ Next: {
+  Value: 2,
+  Next: {
+   Value: 3,
+   Next: <see #1>,
+  },
+ },
+}`,
+		},
+		{
+			desc: "print in order",
+			node: list{
+				target{2, keyvals{
+					{"Next", ref{1}},
+				}},
+				target{1, keyvals{
+					{"Next", ref{2}},
+				}},
+			},
+			normal: `
+[<#1> {Next: <see #2>},
+ <#2> {Next: <see #1>}]`,
+			diffable: `
+[
+ <#1> {
+  Next: <see #2>,
+ },
+ <#2> {
+  Next: <see #1>,
+ },
+]`,
+		},
 	}
 
+	normal := &Config{}
+	diffable := &Config{Diffable: true}
 	for _, test := range tests {
 		// For readability, we have a newline that won't be there in the output
 		test.normal = strings.TrimPrefix(test.normal, "\n")
 		test.diffable = strings.TrimPrefix(test.diffable, "\n")
 
 		buf := new(bytes.Buffer)
-		test.node.WriteTo(buf, "", &Config{})
+		newFormatter(normal, buf).write(test.node)
 		if got, want := buf.String(), test.normal; got != want {
 			t.Errorf("%s: normal rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want)
 		}
 		buf.Reset()
-		test.node.WriteTo(buf, "", &Config{Diffable: true})
+
+		newFormatter(diffable, buf).write(test.node)
 		if got, want := buf.String(), test.diffable; got != want {
 			t.Errorf("%s: diffable rendendered incorrectly\ngot:\n%s\nwant:\n%s", test.desc, got, want)
 		}
@@ -233,7 +288,7 @@
 	}
 
 	for _, test := range tests {
-		if got, want := compactString(test.node), test.compact; got != want {
+		if got, want := new(formatter).compactString(test.node), test.compact; got != want {
 			t.Errorf("%#v: compact = %q, want %q", test.node, got, want)
 		}
 	}
@@ -277,9 +332,9 @@
 
 	for _, test := range tests {
 		buf := new(bytes.Buffer)
-		test.node.WriteTo(buf, "", cfg)
+		newFormatter(cfg, buf).write(test.node)
 		if got, want := buf.String(), test.want; got != want {
-			t.Errorf("%#v: got:\n%s\nwant:\n%s", test.node, got, want)
+			t.Errorf("%#v:\ngot:\n%s\nwant:\n%s", test.node, got, want)
 		}
 	}
 }
@@ -300,13 +355,13 @@
 
 func benchOpts(b *testing.B, cfg *Config) {
 	buf := new(bytes.Buffer)
-	benchNode.WriteTo(buf, "", cfg)
+	newFormatter(cfg, buf).write(benchNode)
 	b.SetBytes(int64(buf.Len()))
 	b.ResetTimer()
 
 	for i := 0; i < b.N; i++ {
 		buf.Reset()
-		benchNode.WriteTo(buf, "", cfg)
+		newFormatter(cfg, buf).write(benchNode)
 	}
 }