Fix shared state in Env extension (#334)

* Fix shared state in Env extension
* Tests for variable and type isolation in the environment
diff --git a/cel/BUILD.bazel b/cel/BUILD.bazel
index adafc93..73ee4ef 100644
--- a/cel/BUILD.bazel
+++ b/cel/BUILD.bazel
@@ -52,6 +52,8 @@
         "//common/types/ref:go_default_library",
         "//common/types/traits:go_default_library",
         "//interpreter/functions:go_default_library",
+        "//test/proto2pb:go_default_library",
+        "//test/proto3pb:go_default_library",
         "@io_bazel_rules_go//proto/wkt:descriptor_go_proto",
         "@org_golang_google_genproto//googleapis/api/expr/v1alpha1:go_default_library",
     ],
diff --git a/cel/cel_test.go b/cel/cel_test.go
index 0b614a1..bc17c9b 100644
--- a/cel/cel_test.go
+++ b/cel/cel_test.go
@@ -33,6 +33,8 @@
 	"github.com/google/cel-go/parser"
 
 	descpb "github.com/golang/protobuf/protoc-gen-go/descriptor"
+	proto2pb "github.com/google/cel-go/test/proto2pb"
+	proto3pb "github.com/google/cel-go/test/proto3pb"
 	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
 )
 
@@ -703,10 +705,79 @@
 				decls.NewObjectType("google.api.expr.v1alpha1.Expr"), nil),
 		),
 	)
-	e2, _ := e.Extend()
+	e2, _ := e.Extend(
+		CustomTypeAdapter(types.DefaultTypeAdapter),
+		Types(&proto3pb.TestAllTypes{}),
+	)
 	if e == e2 {
 		t.Error("got object equality, wanted separate objects")
 	}
+	if e.TypeAdapter() == e2.TypeAdapter() {
+		t.Error("got the same type adapter, wanted isolated instances.")
+	}
+	if e.TypeProvider() == e2.TypeProvider() {
+		t.Error("got the same type provider, wanted isolated instances.")
+	}
+	e3, _ := e2.Extend()
+	if e2.TypeAdapter() != e3.TypeAdapter() {
+		t.Error("got different type adapters, wanted immutable adapter reference")
+	}
+	if e2.TypeProvider() == e3.TypeProvider() {
+		t.Error("got the same type provider, wanted isolated instances.")
+	}
+}
+
+func Test_EnvExtensionIsolation(t *testing.T) {
+	baseEnv, err := NewEnv(
+		Container("google.expr"),
+		Declarations(
+			decls.NewIdent("age", decls.Int, nil),
+			decls.NewIdent("gender", decls.String, nil),
+			decls.NewIdent("country", decls.String, nil),
+		),
+	)
+	if err != nil {
+		t.Fatal(err)
+	}
+	env1, err := baseEnv.Extend(
+		Types(&proto2pb.TestAllTypes{}),
+		Declarations(decls.NewIdent("name", decls.String, nil)))
+	if err != nil {
+		t.Fatal(err)
+	}
+	env2, err := baseEnv.Extend(
+		Types(&proto3pb.TestAllTypes{}),
+		Declarations(decls.NewIdent("group", decls.String, nil)))
+	if err != nil {
+		t.Fatal(err)
+	}
+	_, issues := env2.Compile(`size(group) > 10
+		&& !has(proto3.test.TestAllTypes{}.single_int32)`)
+	if issues.Err() != nil {
+		t.Fatal(issues.Err())
+	}
+	_, issues = env2.Compile(`size(name) > 10`)
+	if issues.Err() == nil {
+		t.Fatal("env2 contains 'name', but should not")
+	}
+	_, issues = env2.Compile(`!has(proto2.test.TestAllTypes{}.single_int32)`)
+	if issues.Err() == nil {
+		t.Fatal("env2 contains 'proto2.test.TestAllTypes', but should not")
+	}
+
+	_, issues = env1.Compile(`size(name) > 10
+		&& !has(proto2.test.TestAllTypes{}.single_int32)`)
+	if issues.Err() != nil {
+		t.Fatal(issues.Err())
+	}
+	_, issues = env1.Compile("size(group) > 10")
+	if issues.Err() == nil {
+		t.Fatal("env1 contains 'group', but should not")
+	}
+	_, issues = env1.Compile(`!has(proto3.test.TestAllTypes{}.single_int32)`)
+	if issues.Err() == nil {
+		t.Fatal("env1 contains 'proto3.test.TestAllTypes', but should not")
+	}
 }
 
 func Test_ParseAndCheckConcurrently(t *testing.T) {
diff --git a/cel/env.go b/cel/env.go
index 2136b26..7d3756a 100644
--- a/cel/env.go
+++ b/cel/env.go
@@ -205,18 +205,52 @@
 }
 
 // Extend the current environment with additional options to produce a new Env.
+//
+// Note, the extended Env value should not share memory with the original. It is possible, however,
+// that a CustomTypeAdapter or CustomTypeProvider options could provide values which are mutable.
+// To ensure separation of state between extended environments either make sure the TypeAdapter and
+// TypeProvider are immutable, or that their underlying implementations are based on the
+// ref.TypeRegistry which provides a Copy method which will be invoked by this method.
 func (e *Env) Extend(opts ...EnvOption) (*Env, error) {
 	if e.chkErr != nil {
 		return nil, e.chkErr
 	}
+	// Copy slices.
+	decsCopy := make([]*exprpb.Decl, len(e.declarations))
+	macsCopy := make([]parser.Macro, len(e.macros))
+	progOptsCopy := make([]ProgramOption, len(e.progOpts))
+	copy(decsCopy, e.declarations)
+	copy(macsCopy, e.macros)
+	copy(progOptsCopy, e.progOpts)
+
+	// Copy the adapter / provider if they appear to be mutable.
+	adapter := e.adapter
+	provider := e.provider
+	adapterReg, isAdapterReg := e.adapter.(ref.TypeRegistry)
+	providerReg, isProviderReg := e.provider.(ref.TypeRegistry)
+	// In most cases the provider and adapter will be a ref.TypeRegistry;
+	// however, in the rare cases where they are not, they are assumed to
+	// be immutable. Since it is possible to set the TypeProvider separately
+	// from the TypeAdapter, the possible configurations which could use a
+	// TypeRegistry as the base implementation are captured below.
+	if isAdapterReg && isProviderReg && adapterReg == providerReg {
+		reg := providerReg.Copy()
+		adapter = reg
+		provider = reg
+	} else if isProviderReg {
+		provider = providerReg.Copy()
+	} else if isAdapterReg {
+		adapter = adapterReg.Copy()
+	}
+
 	ext := &Env{
-		declarations:                   e.declarations,
-		adapter:                        e.adapter,
+		declarations:                   decsCopy,
+		macros:                         macsCopy,
+		progOpts:                       progOptsCopy,
+		adapter:                        adapter,
 		enableDynamicAggregateLiterals: e.enableDynamicAggregateLiterals,
-		macros:                         e.macros,
 		pkg:                            e.pkg,
-		progOpts:                       e.progOpts,
-		provider:                       e.provider,
+		provider:                       provider,
 	}
 	return ext.configure(opts)
 }
diff --git a/common/types/pb/pb.go b/common/types/pb/pb.go
index 629f9be..656d767 100644
--- a/common/types/pb/pb.go
+++ b/common/types/pb/pb.go
@@ -61,6 +61,15 @@
 	return pbdb
 }
 
+// Copy creates a copy of the current database with its own internal descriptor mapping.
+func (pbdb *Db) Copy() *Db {
+	copy := NewDb()
+	for k, v := range pbdb.revFileDescriptorMap {
+		copy.revFileDescriptorMap[k] = v
+	}
+	return copy
+}
+
 // RegisterDescriptor produces a `FileDescription` from a `FileDescriptorProto` and registers the
 // message and enum types into the `pb.Db`.
 func (pbdb *Db) RegisterDescriptor(fileDesc *descpb.FileDescriptorProto) (*FileDescription, error) {
diff --git a/common/types/provider.go b/common/types/provider.go
index 885f60a..2b14e10 100644
--- a/common/types/provider.go
+++ b/common/types/provider.go
@@ -77,6 +77,19 @@
 	}
 }
 
+// Copy implements the ref.TypeRegistry interface method which copies the current state of the
+// registry into its own memory space.
+func (p *protoTypeRegistry) Copy() ref.TypeRegistry {
+	copy := &protoTypeRegistry{
+		revTypeMap: make(map[string]ref.Type),
+		pbdb:       p.pbdb.Copy(),
+	}
+	for k, v := range p.revTypeMap {
+		copy.revTypeMap[k] = v
+	}
+	return copy
+}
+
 func (p *protoTypeRegistry) EnumValue(enumName string) ref.Val {
 	enumVal, err := p.pbdb.DescribeEnum(enumName)
 	if err != nil {
diff --git a/common/types/ref/provider.go b/common/types/ref/provider.go
index 825f91b..541dbdb 100644
--- a/common/types/ref/provider.go
+++ b/common/types/ref/provider.go
@@ -78,6 +78,9 @@
 	// If a type is provided more than once with an alternative definition, the
 	// call will result in an error.
 	RegisterType(types ...Type) error
+
+	// Copy the TypeRegistry and return a new registry whose mutable state is isolated.
+	Copy() TypeRegistry
 }
 
 // FieldType represents a field's type value and whether that field supports
diff --git a/test/proto2pb/BUILD.bazel b/test/proto2pb/BUILD.bazel
index 57a0620..b6e46a4 100644
--- a/test/proto2pb/BUILD.bazel
+++ b/test/proto2pb/BUILD.bazel
@@ -3,6 +3,7 @@
 
 package(
     default_visibility = [
+        "//cel:__subpackages__",
         "//checker:__subpackages__",
         "//common:__subpackages__",
         "//interpreter:__subpackages__",
diff --git a/test/proto3pb/BUILD.bazel b/test/proto3pb/BUILD.bazel
index 9b9035f..210254c 100644
--- a/test/proto3pb/BUILD.bazel
+++ b/test/proto3pb/BUILD.bazel
@@ -3,6 +3,7 @@
 
 package(
     default_visibility = [
+        "//cel:__subpackages__",
         "//checker:__subpackages__",
         "//common:__subpackages__",
         "//interpreter:__subpackages__",