reflect: add protopath and protorange packages

The protopath package provides a means to programmatically represent
a sequence of protobuf reflection operations.
The protorange package traverses through a message and
calls a user-provided function as it iterates.

This feature sets the groundwork for the often requested feature
of being able to exclude certain fields when merging or serializing.

package protopath
    type Path []Step
    type Step struct{ ... }
        func Root(protoreflect.MessageDescriptor) Step
        func FieldAccess(protoreflect.FieldDescriptor) Step
        func UnknownAccess() Step
        func ListIndex(int) Step
        func MapIndex(protoreflect.MapKey) Step
        func AnyExpand(protoreflect.MessageDescriptor) Step
        func (Step) Kind() StepKind
        func (Step) FieldDescriptor() protoreflect.FieldDescriptor
        func (Step) MessageDescriptor() protoreflect.MessageDescriptor
        func (Step) ListIndex() int
        func (Step) MapIndex() protoreflect.MapKey
        func (Step) String() string
    type StepKind int
        const RootStep StepKind
        const FieldAccessStep StepKind
        const UnknownAccessStep StepKind
        const ListIndexStep StepKind
        const MapIndexStep StepKind
        const AnyExpandStep StepKind
    type Values struct {
        Path   Path
        Values []protoreflect.Value
    }
    func (Values) Index(int) (out struct{ ... })
    func (Values) Len() int
    func (Values) String() string

package protorange
    var Break error
    var Terminate error
    func Range(protoreflect.Message, func(protopath.Values) error) error
    type Options struct {
        Stable bool
        Resolver interface { ... }
    }
    func (Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error

Change-Id: I29cbd5142fe169d78367d54a95d37801888b64f4
Reviewed-on: https://go-review.googlesource.com/c/protobuf/+/236540
Trust: Joe Tsai <joetsai@digital-static.net>
Reviewed-by: Damien Neil <dneil@google.com>
diff --git a/internal/encoding/text/encode.go b/internal/encoding/text/encode.go
index aa66bdd..da289cc 100644
--- a/internal/encoding/text/encode.go
+++ b/internal/encoding/text/encode.go
@@ -263,3 +263,8 @@
 func (e *Encoder) Reset(es encoderState) {
 	e.encoderState = es
 }
+
+// AppendString appends the escaped form of the input string to b.
+func AppendString(b []byte, s string) []byte {
+	return appendString(b, s, false)
+}
diff --git a/internal/msgfmt/format.go b/internal/msgfmt/format.go
index 6561b8b..a319550 100644
--- a/internal/msgfmt/format.go
+++ b/internal/msgfmt/format.go
@@ -30,8 +30,15 @@
 	return string(appendMessage(nil, m.ProtoReflect()))
 }
 
+// FormatValue returns a formatted string for an arbitrary value.
+func FormatValue(v protoreflect.Value, fd protoreflect.FieldDescriptor) string {
+	return string(appendValue(nil, v, fd))
+}
+
 func appendValue(b []byte, v protoreflect.Value, fd protoreflect.FieldDescriptor) []byte {
 	switch v := v.Interface().(type) {
+	case nil:
+		return append(b, "<invalid>"...)
 	case bool, int32, int64, uint32, uint64, float32, float64:
 		return append(b, fmt.Sprint(v)...)
 	case string:
@@ -39,7 +46,7 @@
 	case []byte:
 		return append(b, strconv.Quote(string(v))...)
 	case protoreflect.EnumNumber:
-		return appendEnum(b, v, fd.Enum())
+		return appendEnum(b, v, fd)
 	case protoreflect.Message:
 		return appendMessage(b, v)
 	case protoreflect.List:
@@ -51,9 +58,11 @@
 	}
 }
 
-func appendEnum(b []byte, v protoreflect.EnumNumber, ed protoreflect.EnumDescriptor) []byte {
-	if ev := ed.Values().ByNumber(v); ev != nil {
-		return append(b, ev.Name()...)
+func appendEnum(b []byte, v protoreflect.EnumNumber, fd protoreflect.FieldDescriptor) []byte {
+	if fd != nil {
+		if ev := fd.Enum().Values().ByNumber(v); ev != nil {
+			return append(b, ev.Name()...)
+		}
 	}
 	return strconv.AppendInt(b, int64(v), 10)
 }
diff --git a/internal/testprotos/news/news.pb.go b/internal/testprotos/news/news.pb.go
new file mode 100644
index 0000000..bb728a6
--- /dev/null
+++ b/internal/testprotos/news/news.pb.go
@@ -0,0 +1,421 @@
+// Copyright 2020 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.
+
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// source: internal/testprotos/news/news.proto
+
+package news
+
+import (
+	protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+	protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+	anypb "google.golang.org/protobuf/types/known/anypb"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+	reflect "reflect"
+	sync "sync"
+)
+
+type Article_Status int32
+
+const (
+	Article_DRAFT     Article_Status = 0
+	Article_PUBLISHED Article_Status = 1
+	Article_REVOKED   Article_Status = 2
+)
+
+// Enum value maps for Article_Status.
+var (
+	Article_Status_name = map[int32]string{
+		0: "DRAFT",
+		1: "PUBLISHED",
+		2: "REVOKED",
+	}
+	Article_Status_value = map[string]int32{
+		"DRAFT":     0,
+		"PUBLISHED": 1,
+		"REVOKED":   2,
+	}
+)
+
+func (x Article_Status) Enum() *Article_Status {
+	p := new(Article_Status)
+	*p = x
+	return p
+}
+
+func (x Article_Status) String() string {
+	return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (Article_Status) Descriptor() protoreflect.EnumDescriptor {
+	return file_internal_testprotos_news_news_proto_enumTypes[0].Descriptor()
+}
+
+func (Article_Status) Type() protoreflect.EnumType {
+	return &file_internal_testprotos_news_news_proto_enumTypes[0]
+}
+
+func (x Article_Status) Number() protoreflect.EnumNumber {
+	return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use Article_Status.Descriptor instead.
+func (Article_Status) EnumDescriptor() ([]byte, []int) {
+	return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{0, 0}
+}
+
+type Article struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Author      string                 `protobuf:"bytes,1,opt,name=author,proto3" json:"author,omitempty"`
+	Date        *timestamppb.Timestamp `protobuf:"bytes,2,opt,name=date,proto3" json:"date,omitempty"`
+	Title       string                 `protobuf:"bytes,3,opt,name=title,proto3" json:"title,omitempty"`
+	Content     string                 `protobuf:"bytes,4,opt,name=content,proto3" json:"content,omitempty"`
+	Status      Article_Status         `protobuf:"varint,8,opt,name=status,proto3,enum=google.golang.org.Article_Status" json:"status,omitempty"`
+	Tags        []string               `protobuf:"bytes,7,rep,name=tags,proto3" json:"tags,omitempty"`
+	Attachments []*anypb.Any           `protobuf:"bytes,6,rep,name=attachments,proto3" json:"attachments,omitempty"`
+}
+
+func (x *Article) Reset() {
+	*x = Article{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_internal_testprotos_news_news_proto_msgTypes[0]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *Article) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Article) ProtoMessage() {}
+
+func (x *Article) ProtoReflect() protoreflect.Message {
+	mi := &file_internal_testprotos_news_news_proto_msgTypes[0]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use Article.ProtoReflect.Descriptor instead.
+func (*Article) Descriptor() ([]byte, []int) {
+	return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Article) GetAuthor() string {
+	if x != nil {
+		return x.Author
+	}
+	return ""
+}
+
+func (x *Article) GetDate() *timestamppb.Timestamp {
+	if x != nil {
+		return x.Date
+	}
+	return nil
+}
+
+func (x *Article) GetTitle() string {
+	if x != nil {
+		return x.Title
+	}
+	return ""
+}
+
+func (x *Article) GetContent() string {
+	if x != nil {
+		return x.Content
+	}
+	return ""
+}
+
+func (x *Article) GetStatus() Article_Status {
+	if x != nil {
+		return x.Status
+	}
+	return Article_DRAFT
+}
+
+func (x *Article) GetTags() []string {
+	if x != nil {
+		return x.Tags
+	}
+	return nil
+}
+
+func (x *Article) GetAttachments() []*anypb.Any {
+	if x != nil {
+		return x.Attachments
+	}
+	return nil
+}
+
+type BinaryAttachment struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Data []byte `protobuf:"bytes,2,opt,name=data,proto3" json:"data,omitempty"`
+}
+
+func (x *BinaryAttachment) Reset() {
+	*x = BinaryAttachment{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_internal_testprotos_news_news_proto_msgTypes[1]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *BinaryAttachment) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*BinaryAttachment) ProtoMessage() {}
+
+func (x *BinaryAttachment) ProtoReflect() protoreflect.Message {
+	mi := &file_internal_testprotos_news_news_proto_msgTypes[1]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use BinaryAttachment.ProtoReflect.Descriptor instead.
+func (*BinaryAttachment) Descriptor() ([]byte, []int) {
+	return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *BinaryAttachment) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *BinaryAttachment) GetData() []byte {
+	if x != nil {
+		return x.Data
+	}
+	return nil
+}
+
+type KeyValueAttachment struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	Name string            `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"`
+	Data map[string]string `protobuf:"bytes,2,rep,name=data,proto3" json:"data,omitempty" protobuf_key:"bytes,1,opt,name=key,proto3" protobuf_val:"bytes,2,opt,name=value,proto3"`
+}
+
+func (x *KeyValueAttachment) Reset() {
+	*x = KeyValueAttachment{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_internal_testprotos_news_news_proto_msgTypes[2]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *KeyValueAttachment) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*KeyValueAttachment) ProtoMessage() {}
+
+func (x *KeyValueAttachment) ProtoReflect() protoreflect.Message {
+	mi := &file_internal_testprotos_news_news_proto_msgTypes[2]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use KeyValueAttachment.ProtoReflect.Descriptor instead.
+func (*KeyValueAttachment) Descriptor() ([]byte, []int) {
+	return file_internal_testprotos_news_news_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *KeyValueAttachment) GetName() string {
+	if x != nil {
+		return x.Name
+	}
+	return ""
+}
+
+func (x *KeyValueAttachment) GetData() map[string]string {
+	if x != nil {
+		return x.Data
+	}
+	return nil
+}
+
+var File_internal_testprotos_news_news_proto protoreflect.FileDescriptor
+
+var file_internal_testprotos_news_news_proto_rawDesc = []byte{
+	0x0a, 0x23, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73, 0x74, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x11, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67, 0x6f,
+	0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x1a, 0x19, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65,
+	0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2f, 0x61, 0x6e, 0x79, 0x2e, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x1a, 0x1f, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2f, 0x70, 0x72, 0x6f, 0x74,
+	0x6f, 0x62, 0x75, 0x66, 0x2f, 0x74, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x2e, 0x70,
+	0x72, 0x6f, 0x74, 0x6f, 0x22, 0xb9, 0x02, 0x0a, 0x07, 0x41, 0x72, 0x74, 0x69, 0x63, 0x6c, 0x65,
+	0x12, 0x16, 0x0a, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
+	0x52, 0x06, 0x61, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x12, 0x2e, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x65,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61,
+	0x6d, 0x70, 0x52, 0x04, 0x64, 0x61, 0x74, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x69, 0x74, 0x6c,
+	0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x74, 0x69, 0x74, 0x6c, 0x65, 0x12, 0x18,
+	0x0a, 0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52,
+	0x07, 0x63, 0x6f, 0x6e, 0x74, 0x65, 0x6e, 0x74, 0x12, 0x39, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74,
+	0x75, 0x73, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x21, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c,
+	0x65, 0x2e, 0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x41, 0x72, 0x74,
+	0x69, 0x63, 0x6c, 0x65, 0x2e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x52, 0x06, 0x73, 0x74, 0x61,
+	0x74, 0x75, 0x73, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x61, 0x67, 0x73, 0x18, 0x07, 0x20, 0x03, 0x28,
+	0x09, 0x52, 0x04, 0x74, 0x61, 0x67, 0x73, 0x12, 0x36, 0x0a, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63,
+	0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x18, 0x06, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x14, 0x2e, 0x67,
+	0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x41,
+	0x6e, 0x79, 0x52, 0x0b, 0x61, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x22,
+	0x2f, 0x0a, 0x06, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x09, 0x0a, 0x05, 0x44, 0x52, 0x41,
+	0x46, 0x54, 0x10, 0x00, 0x12, 0x0d, 0x0a, 0x09, 0x50, 0x55, 0x42, 0x4c, 0x49, 0x53, 0x48, 0x45,
+	0x44, 0x10, 0x01, 0x12, 0x0b, 0x0a, 0x07, 0x52, 0x45, 0x56, 0x4f, 0x4b, 0x45, 0x44, 0x10, 0x02,
+	0x22, 0x3a, 0x0a, 0x10, 0x42, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68,
+	0x6d, 0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0xa6, 0x01, 0x0a,
+	0x12, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d,
+	0x65, 0x6e, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x09, 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x43, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18,
+	0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x67,
+	0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2e, 0x4b, 0x65, 0x79, 0x56, 0x61, 0x6c,
+	0x75, 0x65, 0x41, 0x74, 0x74, 0x61, 0x63, 0x68, 0x6d, 0x65, 0x6e, 0x74, 0x2e, 0x44, 0x61, 0x74,
+	0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x1a, 0x37, 0x0a, 0x09,
+	0x44, 0x61, 0x74, 0x61, 0x45, 0x6e, 0x74, 0x72, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x6b, 0x65, 0x79,
+	0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6b, 0x65, 0x79, 0x12, 0x14, 0x0a, 0x05, 0x76,
+	0x61, 0x6c, 0x75, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x76, 0x61, 0x6c, 0x75,
+	0x65, 0x3a, 0x02, 0x38, 0x01, 0x42, 0x35, 0x5a, 0x33, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e,
+	0x67, 0x6f, 0x6c, 0x61, 0x6e, 0x67, 0x2e, 0x6f, 0x72, 0x67, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f,
+	0x62, 0x75, 0x66, 0x2f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x2f, 0x74, 0x65, 0x73,
+	0x74, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x73, 0x2f, 0x6e, 0x65, 0x77, 0x73, 0x62, 0x06, 0x70, 0x72,
+	0x6f, 0x74, 0x6f, 0x33,
+}
+
+var (
+	file_internal_testprotos_news_news_proto_rawDescOnce sync.Once
+	file_internal_testprotos_news_news_proto_rawDescData = file_internal_testprotos_news_news_proto_rawDesc
+)
+
+func file_internal_testprotos_news_news_proto_rawDescGZIP() []byte {
+	file_internal_testprotos_news_news_proto_rawDescOnce.Do(func() {
+		file_internal_testprotos_news_news_proto_rawDescData = protoimpl.X.CompressGZIP(file_internal_testprotos_news_news_proto_rawDescData)
+	})
+	return file_internal_testprotos_news_news_proto_rawDescData
+}
+
+var file_internal_testprotos_news_news_proto_enumTypes = make([]protoimpl.EnumInfo, 1)
+var file_internal_testprotos_news_news_proto_msgTypes = make([]protoimpl.MessageInfo, 4)
+var file_internal_testprotos_news_news_proto_goTypes = []interface{}{
+	(Article_Status)(0),           // 0: google.golang.org.Article.Status
+	(*Article)(nil),               // 1: google.golang.org.Article
+	(*BinaryAttachment)(nil),      // 2: google.golang.org.BinaryAttachment
+	(*KeyValueAttachment)(nil),    // 3: google.golang.org.KeyValueAttachment
+	nil,                           // 4: google.golang.org.KeyValueAttachment.DataEntry
+	(*timestamppb.Timestamp)(nil), // 5: google.protobuf.Timestamp
+	(*anypb.Any)(nil),             // 6: google.protobuf.Any
+}
+var file_internal_testprotos_news_news_proto_depIdxs = []int32{
+	5, // 0: google.golang.org.Article.date:type_name -> google.protobuf.Timestamp
+	0, // 1: google.golang.org.Article.status:type_name -> google.golang.org.Article.Status
+	6, // 2: google.golang.org.Article.attachments:type_name -> google.protobuf.Any
+	4, // 3: google.golang.org.KeyValueAttachment.data:type_name -> google.golang.org.KeyValueAttachment.DataEntry
+	4, // [4:4] is the sub-list for method output_type
+	4, // [4:4] is the sub-list for method input_type
+	4, // [4:4] is the sub-list for extension type_name
+	4, // [4:4] is the sub-list for extension extendee
+	0, // [0:4] is the sub-list for field type_name
+}
+
+func init() { file_internal_testprotos_news_news_proto_init() }
+func file_internal_testprotos_news_news_proto_init() {
+	if File_internal_testprotos_news_news_proto != nil {
+		return
+	}
+	if !protoimpl.UnsafeEnabled {
+		file_internal_testprotos_news_news_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*Article); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_internal_testprotos_news_news_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*BinaryAttachment); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_internal_testprotos_news_news_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*KeyValueAttachment); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+	}
+	type x struct{}
+	out := protoimpl.TypeBuilder{
+		File: protoimpl.DescBuilder{
+			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+			RawDescriptor: file_internal_testprotos_news_news_proto_rawDesc,
+			NumEnums:      1,
+			NumMessages:   4,
+			NumExtensions: 0,
+			NumServices:   0,
+		},
+		GoTypes:           file_internal_testprotos_news_news_proto_goTypes,
+		DependencyIndexes: file_internal_testprotos_news_news_proto_depIdxs,
+		EnumInfos:         file_internal_testprotos_news_news_proto_enumTypes,
+		MessageInfos:      file_internal_testprotos_news_news_proto_msgTypes,
+	}.Build()
+	File_internal_testprotos_news_news_proto = out.File
+	file_internal_testprotos_news_news_proto_rawDesc = nil
+	file_internal_testprotos_news_news_proto_goTypes = nil
+	file_internal_testprotos_news_news_proto_depIdxs = nil
+}
diff --git a/internal/testprotos/news/news.proto b/internal/testprotos/news/news.proto
new file mode 100644
index 0000000..bf56f3b
--- /dev/null
+++ b/internal/testprotos/news/news.proto
@@ -0,0 +1,38 @@
+// Copyright 2020 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.
+
+syntax = "proto3";
+
+package google.golang.org;
+
+import "google/protobuf/any.proto";
+import "google/protobuf/timestamp.proto";
+
+option go_package = "google.golang.org/protobuf/internal/testprotos/news";
+
+message Article {
+	enum Status {
+		DRAFT = 0;
+		PUBLISHED = 1;
+		REVOKED = 2;
+	}
+
+	string author = 1;
+	google.protobuf.Timestamp date = 2;
+	string title = 3;
+	string content = 4;
+	Status status = 8;
+	repeated string tags = 7;
+	repeated google.protobuf.Any attachments = 6;
+}
+
+message BinaryAttachment {
+	string name = 1;
+	bytes data = 2;
+}
+
+message KeyValueAttachment {
+	string name = 1;
+	map<string, string> data = 2;
+}
\ No newline at end of file
diff --git a/reflect/protopath/path.go b/reflect/protopath/path.go
new file mode 100644
index 0000000..07f839d
--- /dev/null
+++ b/reflect/protopath/path.go
@@ -0,0 +1,121 @@
+// Copyright 2020 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 protopath provides functionality for
+// representing a sequence of protobuf reflection operations on a message.
+package protopath
+
+import (
+	"fmt"
+
+	"google.golang.org/protobuf/internal/msgfmt"
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// NOTE: The Path and Values are separate types here since there are use cases
+// where you would like to "address" some value in a message with just the path
+// and don't have the value information available.
+//
+// This is different from how "github.com/google/go-cmp/cmp".Path operates,
+// which combines both path and value information together.
+// Since the cmp package itself is the only one ever constructing a cmp.Path,
+// it will always have the value available.
+
+// Path is a sequence of protobuf reflection steps applied to some root
+// protobuf message value to arrive at the current value.
+// The first step must be a Root step.
+type Path []Step
+
+// TODO: Provide a Parse function that parses something similar to or
+// perhaps identical to the output of Path.String.
+
+// Index returns the ith step in the path and supports negative indexing.
+// A negative index starts counting from the tail of the Path such that -1
+// refers to the last step, -2 refers to the second-to-last step, and so on.
+// It returns a zero Step value if the index is out-of-bounds.
+func (p Path) Index(i int) Step {
+	if i < 0 {
+		i = len(p) + i
+	}
+	if i < 0 || i >= len(p) {
+		return Step{}
+	}
+	return p[i]
+}
+
+// String returns a structured representation of the path
+// by concatenating the string representation of every path step.
+func (p Path) String() string {
+	var b []byte
+	for _, s := range p {
+		b = s.appendString(b)
+	}
+	return string(b)
+}
+
+// Values is a Path paired with a sequence of values at each step.
+// The lengths of Path and Values must be identical.
+// The first step must be a Root step and
+// the first value must be a concrete message value.
+type Values struct {
+	Path   Path
+	Values []protoreflect.Value
+}
+
+// Len reports the length of the path and values.
+// If the path and values have differing length, it returns the minimum length.
+func (p Values) Len() int {
+	n := len(p.Path)
+	if n > len(p.Values) {
+		n = len(p.Values)
+	}
+	return n
+}
+
+// Index returns the ith step and value and supports negative indexing.
+// A negative index starts counting from the tail of the Values such that -1
+// refers to the last pair, -2 refers to the second-to-last pair, and so on.
+func (p Values) Index(i int) (out struct {
+	Step  Step
+	Value protoreflect.Value
+}) {
+	// NOTE: This returns a single struct instead of two return values so that
+	// callers can make use of the the value in an expression:
+	//	vs.Index(i).Value.Interface()
+	n := p.Len()
+	if i < 0 {
+		i = n + i
+	}
+	if i < 0 || i >= n {
+		return out
+	}
+	out.Step = p.Path[i]
+	out.Value = p.Values[i]
+	return out
+}
+
+// String returns a humanly readable representation of the path and last value.
+// Do not depend on the output being stable.
+//
+// For example:
+//	(path.to.MyMessage).list_field[5].map_field["hello"] = {hello: "world"}
+func (p Values) String() string {
+	n := p.Len()
+	if n == 0 {
+		return ""
+	}
+
+	// Determine the field descriptor associated with the last step.
+	var fd protoreflect.FieldDescriptor
+	last := p.Index(-1)
+	switch last.Step.kind {
+	case FieldAccessStep:
+		fd = last.Step.FieldDescriptor()
+	case MapIndexStep, ListIndexStep:
+		fd = p.Index(-2).Step.FieldDescriptor()
+	}
+
+	// Format the full path with the last value.
+	return fmt.Sprintf("%v = %v", p.Path[:n], msgfmt.FormatValue(last.Value, fd))
+}
diff --git a/reflect/protopath/step.go b/reflect/protopath/step.go
new file mode 100644
index 0000000..95ae85c
--- /dev/null
+++ b/reflect/protopath/step.go
@@ -0,0 +1,241 @@
+// Copyright 2020 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 protopath
+
+import (
+	"fmt"
+	"strconv"
+	"strings"
+
+	"google.golang.org/protobuf/internal/encoding/text"
+	"google.golang.org/protobuf/reflect/protoreflect"
+)
+
+// StepKind identifies the kind of step operation.
+// Each kind of step corresponds with some protobuf reflection operation.
+type StepKind int
+
+const (
+	invalidStep StepKind = iota
+	// RootStep identifies a step as the Root step operation.
+	RootStep
+	// FieldAccessStep identifies a step as the FieldAccess step operation.
+	FieldAccessStep
+	// UnknownAccessStep identifies a step as the UnknownAccess step operation.
+	UnknownAccessStep
+	// ListIndexStep identifies a step as the ListIndex step operation.
+	ListIndexStep
+	// MapIndexStep identifies a step as the MapIndex step operation.
+	MapIndexStep
+	// AnyExpandStep identifies a step as the AnyExpand step operation.
+	AnyExpandStep
+)
+
+func (k StepKind) String() string {
+	switch k {
+	case invalidStep:
+		return "<invalid>"
+	case RootStep:
+		return "Root"
+	case FieldAccessStep:
+		return "FieldAccess"
+	case UnknownAccessStep:
+		return "UnknownAccess"
+	case ListIndexStep:
+		return "ListIndex"
+	case MapIndexStep:
+		return "MapIndex"
+	case AnyExpandStep:
+		return "AnyExpand"
+	default:
+		return fmt.Sprintf("<unknown:%d>", k)
+	}
+}
+
+// Step is a union where only one step operation may be specified at a time.
+// The different kinds of steps are specified by the constants defined for
+// the StepKind type.
+type Step struct {
+	kind StepKind
+	desc protoreflect.Descriptor
+	key  protoreflect.Value
+}
+
+// Root indicates the root message that a path is relative to.
+// It should always (and only ever) be the first step in a path.
+func Root(md protoreflect.MessageDescriptor) Step {
+	if md == nil {
+		panic("nil message descriptor")
+	}
+	return Step{kind: RootStep, desc: md}
+}
+
+// FieldAccess describes access of a field within a message.
+// Extension field accesses are also represented using a FieldAccess and
+// must be provided with a protoreflect.FieldDescriptor
+//
+// Within the context of Values,
+// the type of the previous step value is always a message, and
+// the type of the current step value is determined by the field descriptor.
+func FieldAccess(fd protoreflect.FieldDescriptor) Step {
+	if fd == nil {
+		panic("nil field descriptor")
+	} else if _, ok := fd.(protoreflect.ExtensionTypeDescriptor); !ok && fd.IsExtension() {
+		panic(fmt.Sprintf("extension field %q must implement protoreflect.ExtensionTypeDescriptor", fd.FullName()))
+	}
+	return Step{kind: FieldAccessStep, desc: fd}
+}
+
+// UnknownAccess describes access to the unknown fields within a message.
+//
+// Within the context of Values,
+// the type of the previous step value is always a message, and
+// the type of the current step value is always a bytes type.
+func UnknownAccess() Step {
+	return Step{kind: UnknownAccessStep}
+}
+
+// ListIndex describes index of an element within a list.
+//
+// Within the context of Values,
+// the type of the previous, previous step value is always a message,
+// the type of the previous step value is always a list, and
+// the type of the current step value is determined by the field descriptor.
+func ListIndex(i int) Step {
+	if i < 0 {
+		panic(fmt.Sprintf("invalid list index: %v", i))
+	}
+	return Step{kind: ListIndexStep, key: protoreflect.ValueOfInt64(int64(i))}
+}
+
+// MapIndex describes index of an entry within a map.
+// The key type is determined by field descriptor that the map belongs to.
+//
+// Within the context of Values,
+// the type of the previous previous step value is always a message,
+// the type of the previous step value is always a map, and
+// the type of the current step value is determined by the field descriptor.
+func MapIndex(k protoreflect.MapKey) Step {
+	if !k.IsValid() {
+		panic("invalid map index")
+	}
+	return Step{kind: MapIndexStep, key: k.Value()}
+}
+
+// AnyExpand describes expansion of a google.protobuf.Any message into
+// a structured representation of the underlying message.
+//
+// Within the context of Values,
+// the type of the previous step value is always a google.protobuf.Any message, and
+// the type of the current step value is always a message.
+func AnyExpand(md protoreflect.MessageDescriptor) Step {
+	if md == nil {
+		panic("nil message descriptor")
+	}
+	return Step{kind: AnyExpandStep, desc: md}
+}
+
+// MessageDescriptor returns the message descriptor for Root or AnyExpand steps,
+// otherwise it returns nil.
+func (s Step) MessageDescriptor() protoreflect.MessageDescriptor {
+	switch s.kind {
+	case RootStep, AnyExpandStep:
+		return s.desc.(protoreflect.MessageDescriptor)
+	default:
+		return nil
+	}
+}
+
+// FieldDescriptor returns the field descriptor for FieldAccess steps,
+// otherwise it returns nil.
+func (s Step) FieldDescriptor() protoreflect.FieldDescriptor {
+	switch s.kind {
+	case FieldAccessStep:
+		return s.desc.(protoreflect.FieldDescriptor)
+	default:
+		return nil
+	}
+}
+
+// ListIndex returns the list index for ListIndex steps,
+// otherwise it returns 0.
+func (s Step) ListIndex() int {
+	switch s.kind {
+	case ListIndexStep:
+		return int(s.key.Int())
+	default:
+		return 0
+	}
+}
+
+// MapIndex returns the map key for MapIndex steps,
+// otherwise it returns an invalid map key.
+func (s Step) MapIndex() protoreflect.MapKey {
+	switch s.kind {
+	case MapIndexStep:
+		return s.key.MapKey()
+	default:
+		return protoreflect.MapKey{}
+	}
+}
+
+// Kind reports which kind of step this is.
+func (s Step) Kind() StepKind {
+	return s.kind
+}
+
+func (s Step) String() string {
+	return string(s.appendString(nil))
+}
+
+func (s Step) appendString(b []byte) []byte {
+	switch s.kind {
+	case RootStep:
+		b = append(b, '(')
+		b = append(b, s.desc.FullName()...)
+		b = append(b, ')')
+	case FieldAccessStep:
+		b = append(b, '.')
+		if fd := s.desc.(protoreflect.FieldDescriptor); fd.IsExtension() {
+			b = append(b, '(')
+			b = append(b, strings.Trim(fd.TextName(), "[]")...)
+			b = append(b, ')')
+		} else {
+			b = append(b, fd.TextName()...)
+		}
+	case UnknownAccessStep:
+		b = append(b, '.')
+		b = append(b, '?')
+	case ListIndexStep:
+		b = append(b, '[')
+		b = strconv.AppendInt(b, s.key.Int(), 10)
+		b = append(b, ']')
+	case MapIndexStep:
+		b = append(b, '[')
+		switch k := s.key.Interface().(type) {
+		case bool:
+			b = strconv.AppendBool(b, bool(k)) // e.g., "true" or "false"
+		case int32:
+			b = strconv.AppendInt(b, int64(k), 10) // e.g., "-32"
+		case int64:
+			b = strconv.AppendInt(b, int64(k), 10) // e.g., "-64"
+		case uint32:
+			b = strconv.AppendUint(b, uint64(k), 10) // e.g., "32"
+		case uint64:
+			b = strconv.AppendUint(b, uint64(k), 10) // e.g., "64"
+		case string:
+			b = text.AppendString(b, k) // e.g., `"hello, world"`
+		}
+		b = append(b, ']')
+	case AnyExpandStep:
+		b = append(b, '.')
+		b = append(b, '(')
+		b = append(b, s.desc.FullName()...)
+		b = append(b, ')')
+	default:
+		b = append(b, "<invalid>"...)
+	}
+	return b
+}
diff --git a/reflect/protorange/example_test.go b/reflect/protorange/example_test.go
new file mode 100644
index 0000000..90ceec6
--- /dev/null
+++ b/reflect/protorange/example_test.go
@@ -0,0 +1,307 @@
+// Copyright 2020 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 protorange_test
+
+import (
+	"fmt"
+	"strings"
+	"time"
+
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/internal/detrand"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protopath"
+	"google.golang.org/protobuf/reflect/protorange"
+	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/testing/protopack"
+	"google.golang.org/protobuf/types/known/anypb"
+	"google.golang.org/protobuf/types/known/timestamppb"
+
+	newspb "google.golang.org/protobuf/internal/testprotos/news"
+)
+
+func init() {
+	detrand.Disable()
+}
+
+func mustMarshal(m proto.Message) []byte {
+	b, err := proto.Marshal(m)
+	if err != nil {
+		panic(err)
+	}
+	return b
+}
+
+// Range through every message and clear the unknown fields.
+func Example_discardUnknown() {
+	// Populate the article with unknown fields.
+	m := &newspb.Article{}
+	m.ProtoReflect().SetUnknown(protopack.Message{
+		protopack.Tag{1000, protopack.BytesType}, protopack.String("Hello, world!"),
+	}.Marshal())
+	fmt.Println("has unknown fields?", len(m.ProtoReflect().GetUnknown()) > 0)
+
+	// Range through the message and clear all unknown fields.
+	fmt.Println("clear unknown fields")
+	protorange.Range(m.ProtoReflect(), func(proto protopath.Values) error {
+		m, ok := proto.Index(-1).Value.Interface().(protoreflect.Message)
+		if ok && len(m.GetUnknown()) > 0 {
+			m.SetUnknown(nil)
+		}
+		return nil
+	})
+	fmt.Println("has unknown fields?", len(m.ProtoReflect().GetUnknown()) > 0)
+
+	// Output:
+	// has unknown fields? true
+	// clear unknown fields
+	// has unknown fields? false
+}
+
+// Print the relative paths as Range iterates through a message
+// in a depth-first order.
+func Example_printPaths() {
+	m := &newspb.Article{
+		Author:  "Russ Cox",
+		Date:    timestamppb.New(time.Date(2019, time.November, 8, 0, 0, 0, 0, time.UTC)),
+		Title:   "Go Turns 10",
+		Content: "Happy birthday, Go! This weekend we celebrate the 10th anniversary of the Go release...",
+		Status:  newspb.Article_PUBLISHED,
+		Tags:    []string{"community", "birthday"},
+		Attachments: []*anypb.Any{{
+			TypeUrl: "google.golang.org.BinaryAttachment",
+			Value: mustMarshal(&newspb.BinaryAttachment{
+				Name: "gopher-birthday.png",
+				Data: []byte("<binary data>"),
+			}),
+		}},
+	}
+
+	// Traverse over all reachable values and print the path.
+	protorange.Range(m.ProtoReflect(), func(p protopath.Values) error {
+		fmt.Println(p.Path[1:])
+		return nil
+	})
+
+	// Output:
+	// .author
+	// .date
+	// .date.seconds
+	// .title
+	// .content
+	// .status
+	// .tags
+	// .tags[0]
+	// .tags[1]
+	// .attachments
+	// .attachments[0]
+	// .attachments[0].(google.golang.org.BinaryAttachment)
+	// .attachments[0].(google.golang.org.BinaryAttachment).name
+	// .attachments[0].(google.golang.org.BinaryAttachment).data
+}
+
+// Implement a basic text formatter by ranging through all populated values
+// in a message in depth-first order.
+func Example_formatText() {
+	m := &newspb.Article{
+		Author:  "Brad Fitzpatrick",
+		Date:    timestamppb.New(time.Date(2018, time.February, 16, 0, 0, 0, 0, time.UTC)),
+		Title:   "Go 1.10 is released",
+		Content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10...",
+		Status:  newspb.Article_PUBLISHED,
+		Tags:    []string{"go1.10", "release"},
+		Attachments: []*anypb.Any{{
+			TypeUrl: "google.golang.org.KeyValueAttachment",
+			Value: mustMarshal(&newspb.KeyValueAttachment{
+				Name: "checksums.txt",
+				Data: map[string]string{
+					"go1.10.src.tar.gz":         "07cbb9d0091b846c6aea40bf5bc0cea7",
+					"go1.10.darwin-amd64.pkg":   "cbb38bb6ff6ea86279e01745984445bf",
+					"go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566",
+					"go1.10.windows-amd64.msi":  "57bda02030f58f5d2bf71943e1390123",
+				},
+			}),
+		}},
+	}
+
+	// Print a message in a humanly readable format.
+	var indent []byte
+	protorange.Options{
+		Stable: true,
+	}.Range(m.ProtoReflect(),
+		func(p protopath.Values) error {
+			// Print the key.
+			var fd protoreflect.FieldDescriptor
+			last := p.Index(-1)
+			beforeLast := p.Index(-2)
+			switch last.Step.Kind() {
+			case protopath.FieldAccessStep:
+				fd = last.Step.FieldDescriptor()
+				fmt.Printf("%s%s: ", indent, fd.Name())
+			case protopath.ListIndexStep:
+				fd = beforeLast.Step.FieldDescriptor() // lists always appear in the context of a repeated field
+				fmt.Printf("%s%d: ", indent, last.Step.ListIndex())
+			case protopath.MapIndexStep:
+				fd = beforeLast.Step.FieldDescriptor() // maps always appear in the context of a repeated field
+				fmt.Printf("%s%v: ", indent, last.Step.MapIndex().Interface())
+			case protopath.AnyExpandStep:
+				fmt.Printf("%s[%v]: ", indent, last.Value.Message().Descriptor().FullName())
+			case protopath.UnknownAccessStep:
+				fmt.Printf("%s?: ", indent)
+			}
+
+			// Starting printing the value.
+			switch v := last.Value.Interface().(type) {
+			case protoreflect.Message:
+				fmt.Printf("{\n")
+				indent = append(indent, '\t')
+			case protoreflect.List:
+				fmt.Printf("[\n")
+				indent = append(indent, '\t')
+			case protoreflect.Map:
+				fmt.Printf("{\n")
+				indent = append(indent, '\t')
+			case protoreflect.EnumNumber:
+				var ev protoreflect.EnumValueDescriptor
+				if fd != nil {
+					ev = fd.Enum().Values().ByNumber(v)
+				}
+				if ev != nil {
+					fmt.Printf("%v\n", ev.Name())
+				} else {
+					fmt.Printf("%v\n", v)
+				}
+			case string, []byte:
+				fmt.Printf("%q\n", v)
+			default:
+				fmt.Printf("%v\n", v)
+			}
+			return nil
+		},
+		func(p protopath.Values) error {
+			// Finish printing the value.
+			last := p.Index(-1)
+			switch last.Value.Interface().(type) {
+			case protoreflect.Message:
+				indent = indent[:len(indent)-1]
+				fmt.Printf("%s}\n", indent)
+			case protoreflect.List:
+				indent = indent[:len(indent)-1]
+				fmt.Printf("%s]\n", indent)
+			case protoreflect.Map:
+				indent = indent[:len(indent)-1]
+				fmt.Printf("%s}\n", indent)
+			}
+			return nil
+		},
+	)
+
+	// Output:
+	// {
+	// 	author: "Brad Fitzpatrick"
+	// 	date: {
+	// 		seconds: 1518739200
+	// 	}
+	// 	title: "Go 1.10 is released"
+	// 	content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10..."
+	// 	attachments: [
+	// 		0: {
+	// 			[google.golang.org.KeyValueAttachment]: {
+	// 				name: "checksums.txt"
+	// 				data: {
+	//					go1.10.darwin-amd64.pkg: "cbb38bb6ff6ea86279e01745984445bf"
+	//					go1.10.linux-amd64.tar.gz: "6b3d0e4a5c77352cf4275573817f7566"
+	//					go1.10.src.tar.gz: "07cbb9d0091b846c6aea40bf5bc0cea7"
+	//					go1.10.windows-amd64.msi: "57bda02030f58f5d2bf71943e1390123"
+	// 				}
+	// 			}
+	// 		}
+	// 	]
+	// 	tags: [
+	// 		0: "go1.10"
+	// 		1: "release"
+	// 	]
+	// 	status: PUBLISHED
+	// }
+}
+
+// Scan all protobuf string values for a sensitive word and replace it with
+// a suitable alternative.
+func Example_sanitizeStrings() {
+	m := &newspb.Article{
+		Author:  "Hermione Granger",
+		Date:    timestamppb.New(time.Date(1998, time.May, 2, 0, 0, 0, 0, time.UTC)),
+		Title:   "Harry Potter vanquishes Voldemort once and for all!",
+		Content: "In a final duel between Harry Potter and Lord Voldemort earlier this evening...",
+		Tags:    []string{"HarryPotter", "LordVoldemort"},
+		Attachments: []*anypb.Any{{
+			TypeUrl: "google.golang.org.KeyValueAttachment",
+			Value: mustMarshal(&newspb.KeyValueAttachment{
+				Name: "aliases.txt",
+				Data: map[string]string{
+					"Harry Potter": "The Boy Who Lived",
+					"Tom Riddle":   "Lord Voldemort",
+				},
+			}),
+		}},
+	}
+
+	protorange.Range(m.ProtoReflect(), func(p protopath.Values) error {
+		const (
+			sensitive   = "Voldemort"
+			alternative = "[He-Who-Must-Not-Be-Named]"
+		)
+
+		// Check if there is a sensitive word to redact.
+		last := p.Index(-1)
+		s, ok := last.Value.Interface().(string)
+		if !ok || !strings.Contains(s, sensitive) {
+			return nil
+		}
+		s = strings.Replace(s, sensitive, alternative, -1)
+
+		// Store the redacted string back into the message.
+		beforeLast := p.Index(-2)
+		switch last.Step.Kind() {
+		case protopath.FieldAccessStep:
+			m := beforeLast.Value.Message()
+			fd := last.Step.FieldDescriptor()
+			m.Set(fd, protoreflect.ValueOfString(s))
+		case protopath.ListIndexStep:
+			ls := beforeLast.Value.List()
+			i := last.Step.ListIndex()
+			ls.Set(i, protoreflect.ValueOfString(s))
+		case protopath.MapIndexStep:
+			ms := beforeLast.Value.Map()
+			k := last.Step.MapIndex()
+			ms.Set(k, protoreflect.ValueOfString(s))
+		}
+		return nil
+	})
+
+	fmt.Println(protojson.Format(m))
+
+	// Output:
+	// {
+	//   "author": "Hermione Granger",
+	//   "date": "1998-05-02T00:00:00Z",
+	//   "title": "Harry Potter vanquishes [He-Who-Must-Not-Be-Named] once and for all!",
+	//   "content": "In a final duel between Harry Potter and Lord [He-Who-Must-Not-Be-Named] earlier this evening...",
+	//   "tags": [
+	//     "HarryPotter",
+	//     "Lord[He-Who-Must-Not-Be-Named]"
+	//   ],
+	//   "attachments": [
+	//     {
+	//       "@type": "google.golang.org.KeyValueAttachment",
+	//       "name": "aliases.txt",
+	//       "data": {
+	//         "Harry Potter": "The Boy Who Lived",
+	//         "Tom Riddle": "Lord [He-Who-Must-Not-Be-Named]"
+	//       }
+	//     }
+	//   ]
+	// }
+}
diff --git a/reflect/protorange/range.go b/reflect/protorange/range.go
new file mode 100644
index 0000000..01750f7
--- /dev/null
+++ b/reflect/protorange/range.go
@@ -0,0 +1,315 @@
+// Copyright 2020 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 protorange provides functionality to traverse a message value.
+package protorange
+
+import (
+	"bytes"
+	"errors"
+
+	"google.golang.org/protobuf/internal/genid"
+	"google.golang.org/protobuf/internal/order"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protopath"
+	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/reflect/protoregistry"
+)
+
+var (
+	// Break breaks traversal of children in the current value.
+	// It has no effect when traversing values that are not composite types
+	// (e.g., messages, lists, and maps).
+	Break = errors.New("break traversal of children in current value")
+
+	// Terminate terminates the entire range operation.
+	// All necessary Pop operations continue to be called.
+	Terminate = errors.New("terminate range operation")
+)
+
+// Range performs a depth-first traversal over reachable values in a message.
+//
+// See Options.Range for details.
+func Range(m protoreflect.Message, f func(protopath.Values) error) error {
+	return Options{}.Range(m, f, nil)
+}
+
+// Options configures traversal of a message value tree.
+type Options struct {
+	// Stable specifies whether to visit message fields and map entries
+	// in a stable ordering. If false, then the ordering is undefined and
+	// may be non-deterministic.
+	//
+	// Message fields are visited in ascending order by field number.
+	// Map entries are visited in ascending order, where
+	// boolean keys are ordered such that false sorts before true,
+	// numeric keys are ordered based on the numeric value, and
+	// string keys are lexicographically ordered by Unicode codepoints.
+	Stable bool
+
+	// Resolver is used for looking up types when expanding google.protobuf.Any
+	// messages. If nil, this defaults to using protoregistry.GlobalTypes.
+	// To prevent expansion of Any messages, pass an empty protoregistry.Types:
+	//
+	//	Options{Resolver: (*protoregistry.Types)(nil)}
+	//
+	Resolver interface {
+		protoregistry.ExtensionTypeResolver
+		protoregistry.MessageTypeResolver
+	}
+}
+
+// Range performs a depth-first traversal over reachable values in a message.
+// The first push and the last pop are to push/pop a protopath.Root step.
+// If push or pop return any non-nil error (other than Break or Terminate),
+// it terminates the traversal and is returned by Range.
+//
+// The rules for traversing a message is as follows:
+//
+// • For messages, iterate over every populated known and extension field.
+// Each field is preceded by a push of a protopath.FieldAccess step,
+// followed by recursive application of the rules on the field value,
+// and succeeded by a pop of that step.
+// If the message has unknown fields, then push an protopath.UnknownAccess step
+// followed immediately by pop of that step.
+//
+// • As an exception to the above rule, if the current message is a
+// google.protobuf.Any message, expand the underlying message (if resolvable).
+// The expanded message is preceded by a push of a protopath.AnyExpand step,
+// followed by recursive application of the rules on the underlying message,
+// and succeeded by a pop of that step. Mutations to the expanded message
+// are written back to the Any message when popping back out.
+//
+// • For lists, iterate over every element. Each element is preceded by a push
+// of a protopath.ListIndex step, followed by recursive application of the rules
+// on the list element, and succeeded by a pop of that step.
+//
+// • For maps, iterate over every entry. Each entry is preceded by a push
+// of a protopath.MapIndex step, followed by recursive application of the rules
+// on the map entry value, and succeeded by a pop of that step.
+//
+// Mutations should only be made to the last value, otherwise the effects on
+// traversal will be undefined. If the mutation is made to the last value
+// during to a push, then the effects of the mutation will affect traversal.
+// For example, if the last value is currently a message, and the push function
+// populates a few fields in that message, then the newly modified fields
+// will be traversed.
+//
+// The protopath.Values provided to push functions is only valid until the
+// corresponding pop call and the values provided to a pop call is only valid
+// for the duration of the pop call itself.
+func (o Options) Range(m protoreflect.Message, push, pop func(protopath.Values) error) error {
+	var err error
+	p := new(protopath.Values)
+	if o.Resolver == nil {
+		o.Resolver = protoregistry.GlobalTypes
+	}
+
+	pushStep(p, protopath.Root(m.Descriptor()), protoreflect.ValueOfMessage(m))
+	if push != nil {
+		err = amendError(err, push(*p))
+	}
+	if err == nil {
+		err = o.rangeMessage(p, m, push, pop)
+	}
+	if pop != nil {
+		err = amendError(err, pop(*p))
+	}
+	popStep(p)
+
+	if err == Break || err == Terminate {
+		err = nil
+	}
+	return err
+}
+
+func (o Options) rangeMessage(p *protopath.Values, m protoreflect.Message, push, pop func(protopath.Values) error) (err error) {
+	if ok, err := o.rangeAnyMessage(p, m, push, pop); ok {
+		return err
+	}
+
+	fieldOrder := order.AnyFieldOrder
+	if o.Stable {
+		fieldOrder = order.NumberFieldOrder
+	}
+	order.RangeFields(m, fieldOrder, func(fd protoreflect.FieldDescriptor, v protoreflect.Value) bool {
+		pushStep(p, protopath.FieldAccess(fd), v)
+		if push != nil {
+			err = amendError(err, push(*p))
+		}
+		if err == nil {
+			switch {
+			case fd.IsMap():
+				err = o.rangeMap(p, fd, v.Map(), push, pop)
+			case fd.IsList():
+				err = o.rangeList(p, fd, v.List(), push, pop)
+			case fd.Message() != nil:
+				err = o.rangeMessage(p, v.Message(), push, pop)
+			}
+		}
+		if pop != nil {
+			err = amendError(err, pop(*p))
+		}
+		popStep(p)
+		return err == nil
+	})
+
+	if b := m.GetUnknown(); len(b) > 0 && err == nil {
+		pushStep(p, protopath.UnknownAccess(), protoreflect.ValueOfBytes(b))
+		if push != nil {
+			err = amendError(err, push(*p))
+		}
+		if pop != nil {
+			err = amendError(err, pop(*p))
+		}
+		popStep(p)
+	}
+
+	if err == Break {
+		err = nil
+	}
+	return err
+}
+
+func (o Options) rangeAnyMessage(p *protopath.Values, m protoreflect.Message, push, pop func(protopath.Values) error) (ok bool, err error) {
+	md := m.Descriptor()
+	if md.FullName() != "google.protobuf.Any" {
+		return false, nil
+	}
+
+	fds := md.Fields()
+	url := m.Get(fds.ByNumber(genid.Any_TypeUrl_field_number)).String()
+	val := m.Get(fds.ByNumber(genid.Any_Value_field_number)).Bytes()
+	mt, errFind := o.Resolver.FindMessageByURL(url)
+	if errFind != nil {
+		return false, nil
+	}
+
+	// Unmarshal the raw encoded message value into a structured message value.
+	m2 := mt.New()
+	errUnmarshal := proto.UnmarshalOptions{
+		Merge:        true,
+		AllowPartial: true,
+		Resolver:     o.Resolver,
+	}.Unmarshal(val, m2.Interface())
+	if errUnmarshal != nil {
+		// If the the underlying message cannot be unmarshaled,
+		// then just treat this as an normal message type.
+		return false, nil
+	}
+
+	// Marshal Any before ranging to detect possible mutations.
+	b1, errMarshal := proto.MarshalOptions{
+		AllowPartial:  true,
+		Deterministic: true,
+	}.Marshal(m2.Interface())
+	if errMarshal != nil {
+		return true, errMarshal
+	}
+
+	pushStep(p, protopath.AnyExpand(m2.Descriptor()), protoreflect.ValueOfMessage(m2))
+	if push != nil {
+		err = amendError(err, push(*p))
+	}
+	if err == nil {
+		err = o.rangeMessage(p, m2, push, pop)
+	}
+	if pop != nil {
+		err = amendError(err, pop(*p))
+	}
+	popStep(p)
+
+	// Marshal Any after ranging to detect possible mutations.
+	b2, errMarshal := proto.MarshalOptions{
+		AllowPartial:  true,
+		Deterministic: true,
+	}.Marshal(m2.Interface())
+	if errMarshal != nil {
+		return true, errMarshal
+	}
+
+	// Mutations detected, write the new sequence of bytes to the Any message.
+	if !bytes.Equal(b1, b2) {
+		m.Set(fds.ByNumber(genid.Any_Value_field_number), protoreflect.ValueOfBytes(b2))
+	}
+
+	if err == Break {
+		err = nil
+	}
+	return true, err
+}
+
+func (o Options) rangeList(p *protopath.Values, fd protoreflect.FieldDescriptor, ls protoreflect.List, push, pop func(protopath.Values) error) (err error) {
+	for i := 0; i < ls.Len() && err == nil; i++ {
+		v := ls.Get(i)
+		pushStep(p, protopath.ListIndex(i), v)
+		if push != nil {
+			err = amendError(err, push(*p))
+		}
+		if err == nil && fd.Message() != nil {
+			err = o.rangeMessage(p, v.Message(), push, pop)
+		}
+		if pop != nil {
+			err = amendError(err, pop(*p))
+		}
+		popStep(p)
+	}
+
+	if err == Break {
+		err = nil
+	}
+	return err
+}
+
+func (o Options) rangeMap(p *protopath.Values, fd protoreflect.FieldDescriptor, ms protoreflect.Map, push, pop func(protopath.Values) error) (err error) {
+	keyOrder := order.AnyKeyOrder
+	if o.Stable {
+		keyOrder = order.GenericKeyOrder
+	}
+	order.RangeEntries(ms, keyOrder, func(k protoreflect.MapKey, v protoreflect.Value) bool {
+		pushStep(p, protopath.MapIndex(k), v)
+		if push != nil {
+			err = amendError(err, push(*p))
+		}
+		if err == nil && fd.MapValue().Message() != nil {
+			err = o.rangeMessage(p, v.Message(), push, pop)
+		}
+		if pop != nil {
+			err = amendError(err, pop(*p))
+		}
+		popStep(p)
+		return err == nil
+	})
+
+	if err == Break {
+		err = nil
+	}
+	return err
+}
+
+func pushStep(p *protopath.Values, s protopath.Step, v protoreflect.Value) {
+	p.Path = append(p.Path, s)
+	p.Values = append(p.Values, v)
+}
+
+func popStep(p *protopath.Values) {
+	p.Path = p.Path[:len(p.Path)-1]
+	p.Values = p.Values[:len(p.Values)-1]
+}
+
+// amendErrors amends the previous error with the current error if it is
+// considered more serious. The precedence order for errors is:
+//	nil < Break < Terminate < previous non-nil < current non-nil
+func amendError(prev, curr error) error {
+	switch {
+	case curr == nil:
+		return prev
+	case curr == Break && prev != nil:
+		return prev
+	case curr == Terminate && prev != nil && prev != Break:
+		return prev
+	default:
+		return curr
+	}
+}
diff --git a/reflect/protorange/range_test.go b/reflect/protorange/range_test.go
new file mode 100644
index 0000000..a8ca6a0
--- /dev/null
+++ b/reflect/protorange/range_test.go
@@ -0,0 +1,253 @@
+// Copyright 2020 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 protorange
+
+import (
+	"testing"
+	"time"
+
+	"github.com/google/go-cmp/cmp"
+	"github.com/google/go-cmp/cmp/cmpopts"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protopath"
+	"google.golang.org/protobuf/reflect/protoreflect"
+	"google.golang.org/protobuf/reflect/protoregistry"
+	"google.golang.org/protobuf/testing/protocmp"
+
+	newspb "google.golang.org/protobuf/internal/testprotos/news"
+	anypb "google.golang.org/protobuf/types/known/anypb"
+	timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+)
+
+func mustMarshal(m proto.Message) []byte {
+	b, err := proto.MarshalOptions{Deterministic: true}.Marshal(m)
+	if err != nil {
+		panic(err)
+	}
+	return b
+}
+
+var transformReflectValue = cmp.Transformer("", func(v protoreflect.Value) interface{} {
+	switch v := v.Interface().(type) {
+	case protoreflect.Message:
+		return v.Interface()
+	case protoreflect.Map:
+		ms := map[interface{}]protoreflect.Value{}
+		v.Range(func(k protoreflect.MapKey, v protoreflect.Value) bool {
+			ms[k.Interface()] = v
+			return true
+		})
+		return ms
+	case protoreflect.List:
+		ls := []protoreflect.Value{}
+		for i := 0; i < v.Len(); i++ {
+			ls = append(ls, v.Get(i))
+		}
+		return ls
+	default:
+		return v
+	}
+})
+
+func TestRange(t *testing.T) {
+	m2 := (&newspb.KeyValueAttachment{
+		Name: "checksums.txt",
+		Data: map[string]string{
+			"go1.10.src.tar.gz":         "07cbb9d0091b846c6aea40bf5bc0cea7",
+			"go1.10.darwin-amd64.pkg":   "cbb38bb6ff6ea86279e01745984445bf",
+			"go1.10.linux-amd64.tar.gz": "6b3d0e4a5c77352cf4275573817f7566",
+			"go1.10.windows-amd64.msi":  "57bda02030f58f5d2bf71943e1390123",
+		},
+	}).ProtoReflect()
+	m := (&newspb.Article{
+		Author:  "Brad Fitzpatrick",
+		Date:    timestamppb.New(time.Date(2018, time.February, 16, 0, 0, 0, 0, time.UTC)),
+		Title:   "Go 1.10 is released",
+		Content: "Happy Friday, happy weekend! Today the Go team is happy to announce the release of Go 1.10...",
+		Status:  newspb.Article_PUBLISHED,
+		Tags:    []string{"go1.10", "release"},
+		Attachments: []*anypb.Any{{
+			TypeUrl: "google.golang.org.KeyValueAttachment",
+			Value:   mustMarshal(m2.Interface()),
+		}},
+	}).ProtoReflect()
+
+	// Nil push and pop functions should not panic.
+	noop := func(protopath.Values) error { return nil }
+	Options{}.Range(m, nil, nil)
+	Options{}.Range(m, noop, nil)
+	Options{}.Range(m, nil, noop)
+
+	getByName := func(m protoreflect.Message, s protoreflect.Name) protoreflect.Value {
+		fds := m.Descriptor().Fields()
+		return m.Get(fds.ByName(s))
+	}
+
+	wantPaths := []string{
+		``,
+		`.author`,
+		`.date`,
+		`.date.seconds`,
+		`.title`,
+		`.content`,
+		`.attachments`,
+		`.attachments[0]`,
+		`.attachments[0].(google.golang.org.KeyValueAttachment)`,
+		`.attachments[0].(google.golang.org.KeyValueAttachment).name`,
+		`.attachments[0].(google.golang.org.KeyValueAttachment).data`,
+		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.darwin-amd64.pkg"]`,
+		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.linux-amd64.tar.gz"]`,
+		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.src.tar.gz"]`,
+		`.attachments[0].(google.golang.org.KeyValueAttachment).data["go1.10.windows-amd64.msi"]`,
+		`.tags`,
+		`.tags[0]`,
+		`.tags[1]`,
+		`.status`,
+	}
+	wantValues := []protoreflect.Value{
+		protoreflect.ValueOfMessage(m),
+		getByName(m, "author"),
+		getByName(m, "date"),
+		getByName(getByName(m, "date").Message(), "seconds"),
+		getByName(m, `title`),
+		getByName(m, `content`),
+		getByName(m, `attachments`),
+		getByName(m, `attachments`).List().Get(0),
+		protoreflect.ValueOfMessage(m2),
+		getByName(m2, `name`),
+		getByName(m2, `data`),
+		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.darwin-amd64.pkg").MapKey()),
+		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.linux-amd64.tar.gz").MapKey()),
+		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.src.tar.gz").MapKey()),
+		getByName(m2, `data`).Map().Get(protoreflect.ValueOfString("go1.10.windows-amd64.msi").MapKey()),
+		getByName(m, `tags`),
+		getByName(m, `tags`).List().Get(0),
+		getByName(m, `tags`).List().Get(1),
+		getByName(m, `status`),
+	}
+
+	tests := []struct {
+		resolver interface {
+			protoregistry.ExtensionTypeResolver
+			protoregistry.MessageTypeResolver
+		}
+
+		errorAt     int
+		breakAt     int
+		terminateAt int
+
+		wantPaths  []string
+		wantValues []protoreflect.Value
+		wantError  error
+	}{{
+		wantPaths:  wantPaths,
+		wantValues: wantValues,
+	}, {
+		resolver: (*protoregistry.Types)(nil),
+		wantPaths: append(append(wantPaths[:8:8],
+			`.attachments[0].type_url`,
+			`.attachments[0].value`,
+		), wantPaths[15:]...),
+		wantValues: append(append(wantValues[:8:8],
+			protoreflect.ValueOfString("google.golang.org.KeyValueAttachment"),
+			protoreflect.ValueOfBytes(mustMarshal(m2.Interface())),
+		), wantValues[15:]...),
+	}, {
+		errorAt:    5, // return error within newspb.Article
+		wantPaths:  wantPaths[:5],
+		wantValues: wantValues[:5],
+		wantError:  cmpopts.AnyError,
+	}, {
+		terminateAt: 11, // terminate within newspb.KeyValueAttachment
+		wantPaths:   wantPaths[:11],
+		wantValues:  wantValues[:11],
+	}, {
+		breakAt:    11, // break within newspb.KeyValueAttachment
+		wantPaths:  append(wantPaths[:11:11], wantPaths[15:]...),
+		wantValues: append(wantValues[:11:11], wantValues[15:]...),
+	}, {
+		errorAt:    17, // return error within newspb.Article.Tags
+		wantPaths:  wantPaths[:17],
+		wantValues: wantValues[:17],
+		wantError:  cmpopts.AnyError,
+	}, {
+		breakAt:    17, // break within newspb.Article.Tags
+		wantPaths:  append(wantPaths[:17:17], wantPaths[18:]...),
+		wantValues: append(wantValues[:17:17], wantValues[18:]...),
+	}, {
+		terminateAt: 17, // terminate within newspb.Article.Tags
+		wantPaths:   wantPaths[:17],
+		wantValues:  wantValues[:17],
+	}, {
+		errorAt:    13, // return error within newspb.KeyValueAttachment.Data
+		wantPaths:  wantPaths[:13],
+		wantValues: wantValues[:13],
+		wantError:  cmpopts.AnyError,
+	}, {
+		breakAt:    13, // break within newspb.KeyValueAttachment.Data
+		wantPaths:  append(wantPaths[:13:13], wantPaths[15:]...),
+		wantValues: append(wantValues[:13:13], wantValues[15:]...),
+	}, {
+		terminateAt: 13, // terminate within newspb.KeyValueAttachment.Data
+		wantPaths:   wantPaths[:13],
+		wantValues:  wantValues[:13],
+	}}
+	for _, tt := range tests {
+		t.Run("", func(t *testing.T) {
+			var gotPaths []string
+			var gotValues []protoreflect.Value
+			var stackPaths []string
+			var stackValues []protoreflect.Value
+			gotError := Options{
+				Stable:   true,
+				Resolver: tt.resolver,
+			}.Range(m,
+				func(p protopath.Values) error {
+					gotPaths = append(gotPaths, p.Path[1:].String())
+					stackPaths = append(stackPaths, p.Path[1:].String())
+					gotValues = append(gotValues, p.Index(-1).Value)
+					stackValues = append(stackValues, p.Index(-1).Value)
+					switch {
+					case tt.errorAt > 0 && tt.errorAt == len(gotPaths):
+						return cmpopts.AnyError
+					case tt.breakAt > 0 && tt.breakAt == len(gotPaths):
+						return Break
+					case tt.terminateAt > 0 && tt.terminateAt == len(gotPaths):
+						return Terminate
+					default:
+						return nil
+					}
+				},
+				func(p protopath.Values) error {
+					gotPath := p.Path[1:].String()
+					wantPath := stackPaths[len(stackPaths)-1]
+					if wantPath != gotPath {
+						t.Errorf("%d: pop path mismatch: got %v, want %v", len(gotPaths), gotPath, wantPath)
+					}
+					gotValue := p.Index(-1).Value
+					wantValue := stackValues[len(stackValues)-1]
+					if diff := cmp.Diff(wantValue, gotValue, transformReflectValue, protocmp.Transform()); diff != "" {
+						t.Errorf("%d: pop value mismatch (-want +got):\n%v", len(gotValues), diff)
+					}
+					stackPaths = stackPaths[:len(stackPaths)-1]
+					stackValues = stackValues[:len(stackValues)-1]
+					return nil
+				},
+			)
+			if n := len(stackPaths) + len(stackValues); n > 0 {
+				t.Errorf("stack mismatch: got %d unpopped items", n)
+			}
+			if diff := cmp.Diff(tt.wantPaths, gotPaths); diff != "" {
+				t.Errorf("paths mismatch (-want +got):\n%s", diff)
+			}
+			if diff := cmp.Diff(tt.wantValues, gotValues, transformReflectValue, protocmp.Transform()); diff != "" {
+				t.Errorf("values mismatch (-want +got):\n%s", diff)
+			}
+			if !cmp.Equal(gotError, tt.wantError, cmpopts.EquateErrors()) {
+				t.Errorf("error mismatch: got %v, want %v", gotError, tt.wantError)
+			}
+		})
+	}
+}