Repository: google/cel-go
Pull request #1143: Add a new compiler tool which can be used to compile CEL expressions and policies using serialized environment
Author: login: aakash070
Created: 2025-03-12T18:47:33Z | Merged: 2025-03-25T22:32:40Z

This change adds a new compiler tool which can be used to generate an ast for CEL expressions and policies provided in the form of:
1. CEL policy file
2. CEL expression string file
3. CEL checked expression
4. Raw CEL string

The compiler has the ability to extend a serialized environment file to create the environment which is used for compilation. The file can be in either Textproto or YAML format.


Diff:
diff --git a/WORKSPACE b/WORKSPACE
index b52b8319a..f566d7d09 100644
--- a/WORKSPACE
+++ b/WORKSPACE
@@ -101,8 +101,8 @@ go_repository(
 go_repository(
     name = "dev_cel_expr",
     importpath = "cel.dev/expr",
-    sum = "h1:o+Wj235dy4gFYlYin3JsMpp3EEfMrPm/6tdoyjT98S0=",
-    version = "v0.21.2",
+    sum = "h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI=",
+    version = "v0.22.1",
 )
 
 # local_repository(
diff --git a/common/env/env.go b/common/env/env.go
index 07294c696..8e57c42e3 100644
--- a/common/env/env.go
+++ b/common/env/env.go
@@ -836,7 +836,7 @@ func (td *TypeDesc) AsCELType(tp types.Provider) (*types.Type, error) {
 	}
 }
 
-// SerializeTypeDesc converts *types.Type to a serialized format TypeDesc
+// SerializeTypeDesc converts a CEL native *types.Type to a serializable TypeDesc.
 func SerializeTypeDesc(t *types.Type) *TypeDesc {
 	typeName := t.TypeName()
 	if t.Kind() == types.TypeParamKind {
diff --git a/ext/extension_option_factory_test.go b/ext/extension_option_factory_test.go
index f721bb6bf..573155baa 100644
--- a/ext/extension_option_factory_test.go
+++ b/ext/extension_option_factory_test.go
@@ -61,6 +61,9 @@ func TestExtensionOptionFactoryValidBindingsExtension(t *testing.T) {
 		t.Fatalf("ExtensionOptionFactory(%s) returned invalid extension", e.Name)
 	}
 	cfg, err := en.ToConfig("test config")
+	if err != nil {
+		t.Fatalf("ToConfig(%s) returned error: %v", e.Name, err)
+	}
 	if len(cfg.Extensions) != 1 || cfg.Extensions[0].Name != "cel.lib.ext.cel.bindings" || cfg.Extensions[0].Version != "latest" {
 		t.Fatalf("ExtensionOptionFactory(%s) returned invalid extension", e.Name)
 	}
diff --git a/go.mod b/go.mod
index 8bf321c5a..4108e1724 100644
--- a/go.mod
+++ b/go.mod
@@ -5,7 +5,7 @@ go 1.21.1
 toolchain go1.23.0
 
 require (
-	cel.dev/expr v0.21.2
+	cel.dev/expr v0.22.1
 	github.com/antlr4-go/antlr/v4 v4.13.0
 	github.com/stoewer/go-strcase v1.2.0
 	google.golang.org/genproto/googleapis/api v0.0.0-20240826202546-f6391c0de4c7
diff --git a/go.sum b/go.sum
index 69a046f2b..062b316c3 100644
--- a/go.sum
+++ b/go.sum
@@ -1,5 +1,5 @@
-cel.dev/expr v0.21.2 h1:o+Wj235dy4gFYlYin3JsMpp3EEfMrPm/6tdoyjT98S0=
-cel.dev/expr v0.21.2/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI=
+cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
 github.com/antlr4-go/antlr/v4 v4.13.0 h1:lxCg3LAv+EUK6t1i0y1V6/SLeUi0eKEKdhQAlS8TVTI=
 github.com/antlr4-go/antlr/v4 v4.13.0/go.mod h1:pfChB/xh/Unjila75QW7+VU4TSnWnnk9UTnmpPaOR2g=
 github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
diff --git a/policy/BUILD.bazel b/policy/BUILD.bazel
index 875f523c3..f058f1ab9 100644
--- a/policy/BUILD.bazel
+++ b/policy/BUILD.bazel
@@ -15,7 +15,10 @@
 load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
 
 package(
-    default_visibility = ["//policy:__subpackages__"],
+    default_visibility = [
+        "//policy:__subpackages__",
+        "//tools:__subpackages__"
+    ],
     licenses = ["notice"],
 )
 
@@ -67,3 +70,8 @@ go_test(
         "@in_gopkg_yaml_v3//:go_default_library",
     ],
 )
+
+filegroup(
+    name = "k8s_policy_testdata",
+    srcs = glob(["testdata/k8s/*"]),
+)
\ No newline at end of file
diff --git a/policy/testdata/k8s/config.yaml b/policy/testdata/k8s/config.yaml
index 15a32b535..5a2cb3290 100644
--- a/policy/testdata/k8s/config.yaml
+++ b/policy/testdata/k8s/config.yaml
@@ -14,6 +14,10 @@
 
 name: k8s
 extensions:
+  - name: "optional"
+    version: "latest"
+  - name: "bindings"
+    version: "latest"
   - name: "strings"
     version: 2
 variables:
diff --git a/tools/compiler/BUILD.bazel b/tools/compiler/BUILD.bazel
new file mode 100644
index 000000000..0c3e4080b
--- /dev/null
+++ b/tools/compiler/BUILD.bazel
@@ -0,0 +1,75 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
+
+package(
+    default_visibility = ["//visibility:public"],
+    licenses = ["notice"],
+)
+
+go_library(
+    name = "go_default_library",
+    srcs = [
+        "compiler.go",
+    ],
+    importpath = "github.com/google/cel-go/tools/compiler",
+    deps = [
+        "//cel:go_default_library",
+        "//common:go_default_library",
+        "//common/env:go_default_library",
+        "//common/types:go_default_library",
+        "//ext:go_default_library",
+        "//policy:go_default_library",
+        "@dev_cel_expr//:expr",
+        "@dev_cel_expr//conformance:go_default_library",
+        "@in_gopkg_yaml_v3//:go_default_library",
+        "@io_bazel_rules_go//go/runfiles",
+        "@org_golang_google_genproto_googleapis_api//expr/v1alpha1:go_default_library",
+        "@org_golang_google_protobuf//encoding/prototext:go_default_library",
+        "@org_golang_google_protobuf//proto:go_default_library",
+        "@org_golang_google_protobuf//reflect/protoreflect:go_default_library",
+        "@org_golang_google_protobuf//types/descriptorpb:go_default_library",
+    ],
+)
+
+filegroup(
+    name = "compiler_testdata",
+    srcs = glob(["testdata/**"]),
+)
+
+go_test(
+    name = "go_default_test",
+    size = "small",
+    srcs = [
+        "compiler_test.go",
+    ],
+    data = [
+        ":compiler_testdata",
+        "//policy:k8s_policy_testdata",
+    ],
+    embed = [":go_default_library"],
+    deps = [
+        "//cel:go_default_library",
+        "//common/decls:go_default_library",
+        "//common/env:go_default_library",
+        "//common/types:go_default_library",
+        "//ext:go_default_library",
+        "//policy:go_default_library",
+        "@dev_cel_expr//:expr",
+        "@dev_cel_expr//conformance:go_default_library",
+        "@in_gopkg_yaml_v3//:go_default_library",
+        "@org_golang_google_protobuf//types/known/structpb:go_default_library",
+    ],
+)
diff --git a/tools/compiler/compiler.go b/tools/compiler/compiler.go
new file mode 100644
index 000000000..fd590ecf3
--- /dev/null
+++ b/tools/compiler/compiler.go
@@ -0,0 +1,534 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package compiler exposes a standard way to set up a compiler which can be used for CEL
+// expressions and policies.
+package compiler
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"gopkg.in/yaml.v3"
+
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common"
+	"github.com/google/cel-go/common/env"
+	"github.com/google/cel-go/common/types"
+	"github.com/google/cel-go/ext"
+	"github.com/google/cel-go/policy"
+	"google.golang.org/protobuf/encoding/prototext"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+
+	celpb "cel.dev/expr"
+	configpb "cel.dev/expr/conformance"
+	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
+	descpb "google.golang.org/protobuf/types/descriptorpb"
+)
+
+// FileFormat represents the format of the file being loaded.
+type FileFormat int
+
+const (
+	// Unspecified is used when the file format is not determined.
+	Unspecified FileFormat = iota + 1
+	// BinaryProto is used for a binary proto file.
+	BinaryProto
+	// TextProto is used for a text proto file.
+	TextProto
+	// TextYAML is used for a YAML file.
+	TextYAML
+	// CELString is used for a CEL string expression defined in a file with .cel extension.
+	CELString
+	// CELPolicy is used for a CEL policy file with .celpolicy extension.
+	CELPolicy
+)
+
+// PolicyMetadataEnvOption represents a function which accepts a policy metadata map and returns an
+// environment option used to extend the CEL environment.
+//
+// The policy metadata map is generally produced as a byproduct of parsing the policy and it can
+// be optionally customised by providing a custom policy parser.
+type PolicyMetadataEnvOption func(map[string]any) cel.EnvOption
+
+// Compiler interface is used to set up a compiler with the following capabilities:
+// - create a CEL environment
+// - create a policy parser
+// - fetch policy compiler options
+// - fetch policy environment options
+//
+// Note: This compiler is not the same as the CEL expression compiler, rather it provides an
+// abstraction layer which can create the different components needed for parsing and compiling CEL
+// expressions and policies.
+type Compiler interface {
+	// CreateEnv creates a singleton CEL environment with the configured environment options.
+	CreateEnv() (*cel.Env, error)
+	// CreatePolicyParser creates a policy parser using the optionally configured parser options.
+	CreatePolicyParser() (*policy.Parser, error)
+	// PolicyCompilerOptions returns the policy compiler options.
+	PolicyCompilerOptions() []policy.CompilerOption
+	// PolicyMetadataEnvOptions returns the policy metadata environment options.
+	PolicyMetadataEnvOptions() []PolicyMetadataEnvOption
+}
+
+type compiler struct {
+	envOptions               []cel.EnvOption
+	policyParserOptions      []policy.ParserOption
+	policyCompilerOptions    []policy.CompilerOption
+	policyMetadataEnvOptions []PolicyMetadataEnvOption
+	env                      *cel.Env
+}
+
+// NewCompiler creates a new compiler with a set of functional options.
+func NewCompiler(opts ...any) (Compiler, error) {
+	c := &compiler{
+		envOptions:               []cel.EnvOption{},
+		policyParserOptions:      []policy.ParserOption{},
+		policyCompilerOptions:    []policy.CompilerOption{},
+		policyMetadataEnvOptions: []PolicyMetadataEnvOption{},
+	}
+	for _, opt := range opts {
+		switch opt := opt.(type) {
+		case cel.EnvOption:
+			c.envOptions = append(c.envOptions, opt)
+		case policy.ParserOption:
+			c.policyParserOptions = append(c.policyParserOptions, opt)
+		case policy.CompilerOption:
+			c.policyCompilerOptions = append(c.policyCompilerOptions, opt)
+		case PolicyMetadataEnvOption:
+			c.policyMetadataEnvOptions = append(c.policyMetadataEnvOptions, opt)
+		default:
+			return nil, fmt.Errorf("unsupported compiler option: %v", opt)
+		}
+	}
+	return c, nil
+}
+
+// CreateEnv creates a singleton CEL environment with the configured environment options.
+func (c *compiler) CreateEnv() (*cel.Env, error) {
+	if c.env != nil {
+		return c.env, nil
+	}
+	env, err := cel.NewCustomEnv(c.envOptions...)
+	if err != nil {
+		return nil, err
+	}
+	c.env = env
+	return c.env, nil
+}
+
+// CreatePolicyParser creates a policy parser using the optionally configured parser options.
+func (c *compiler) CreatePolicyParser() (*policy.Parser, error) {
+	return policy.NewParser(c.policyParserOptions...)
+}
+
+// PolicyCompilerOptions returns the policy compiler options configured in the compiler.
+func (c *compiler) PolicyCompilerOptions() []policy.CompilerOption {
+	return c.policyCompilerOptions
+}
+
+// PolicyMetadataEnvOptions returns the policy metadata environment options configured in the compiler.
+func (c *compiler) PolicyMetadataEnvOptions() []PolicyMetadataEnvOption {
+	return c.policyMetadataEnvOptions
+}
+
+func loadFile(path string) ([]byte, error) {
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read file %q: %v", path, err)
+	}
+	return data, err
+}
+
+func loadProtoFile(path string, format FileFormat, out protoreflect.ProtoMessage) error {
+	unmarshaller := proto.Unmarshal
+	if format == TextProto {
+		unmarshaller = prototext.Unmarshal
+	}
+	data, err := loadFile(path)
+	if err != nil {
+		return err
+	}
+	return unmarshaller(data, out)
+}
+
+func inferFileFormat(path string) FileFormat {
+	extension := filepath.Ext(path)
+	switch extension {
+	case ".textproto":
+		return TextProto
+	case ".yaml":
+		return TextYAML
+	case ".binarypb", ".fds":
+		return BinaryProto
+	case ".cel":
+		return CELString
+	case ".celpolicy":
+		return CELPolicy
+	default:
+		return Unspecified
+	}
+}
+
+// EnvironmentFile returns an EnvOption which loads a serialized CEL environment from a file.
+// The file must be in one of the following formats:
+// - Textproto
+// - YAML
+// - Binarypb
+func EnvironmentFile(path string) cel.EnvOption {
+	return func(e *cel.Env) (*cel.Env, error) {
+		format := inferFileFormat(path)
+		if format != TextProto && format != TextYAML && format != BinaryProto {
+			return nil, fmt.Errorf("file extension must be one of .textproto, .yaml, .binarypb: found %v", format)
+		}
+		var envConfig *env.Config
+		var fileDescriptorSet *descpb.FileDescriptorSet
+		switch format {
+		case TextProto, BinaryProto:
+			pbEnv := &configpb.Environment{}
+			var err error
+			if err = loadProtoFile(path, format, pbEnv); err != nil {
+				return nil, err
+			}
+			envConfig, fileDescriptorSet, err = envProtoToConfig(pbEnv)
+			if err != nil {
+				return nil, err
+			}
+		case TextYAML:
+			envConfig = &env.Config{}
+			data, err := loadFile(path)
+			if err != nil {
+				return nil, err
+			}
+			err = yaml.Unmarshal(data, envConfig)
+			if err != nil {
+				return nil, fmt.Errorf("yaml.Unmarshal failed to map CEL environment: %w", err)
+			}
+		default:
+			return nil, fmt.Errorf("unsupported format: %v, file: %s", format, path)
+		}
+		var envOpts []cel.EnvOption
+		if fileDescriptorSet != nil {
+			envOpts = append(envOpts, cel.TypeDescs(fileDescriptorSet))
+		}
+		envOpts = append(envOpts, cel.FromConfig(envConfig, ext.ExtensionOptionFactory))
+		var err error
+		e, err = e.Extend(envOpts...)
+		if err != nil {
+			return nil, fmt.Errorf("e.Extend() with env options %v failed: %w", envOpts, err)
+		}
+		return e, nil
+	}
+}
+
+func envProtoToConfig(pbEnv *configpb.Environment) (*env.Config, *descpb.FileDescriptorSet, error) {
+	if pbEnv == nil {
+		return nil, nil, fmt.Errorf("proto environment is not set")
+	}
+	envConfig := env.NewConfig(pbEnv.GetName())
+	envConfig.Description = pbEnv.GetDescription()
+	envConfig.SetContainer(pbEnv.GetContainer())
+	for _, imp := range pbEnv.GetImports() {
+		envConfig.AddImports(env.NewImport(imp.GetName()))
+	}
+	stdLib, err := envToStdLib(pbEnv)
+	if err != nil {
+		return nil, nil, err
+	}
+	envConfig.SetStdLib(stdLib)
+	extensions := make([]*env.Extension, 0, len(pbEnv.GetExtensions()))
+	for _, extension := range pbEnv.GetExtensions() {
+		extensions = append(extensions, &env.Extension{Name: extension.GetName(), Version: extension.GetVersion()})
+	}
+	envConfig.AddExtensions(extensions...)
+	if contextVariable := pbEnv.GetContextVariable(); contextVariable != nil {
+		envConfig.SetContextVariable(env.NewContextVariable(contextVariable.GetTypeName()))
+	}
+	functions, variables, err := protoDeclToFunctionsAndVariables(pbEnv.GetDeclarations())
+	if err != nil {
+		return nil, nil, err
+	}
+	envConfig.AddFunctions(functions...)
+	envConfig.AddVariables(variables...)
+	validators, err := envToValidators(pbEnv)
+	if err != nil {
+		return nil, nil, err
+	}
+	envConfig.AddValidators(validators...)
+	features, err := envToFeatures(pbEnv)
+	if err != nil {
+		return nil, nil, err
+	}
+	envConfig.AddFeatures(features...)
+	fileDescriptorSet := pbEnv.GetMessageTypeExtension()
+	return envConfig, fileDescriptorSet, nil
+}
+
+func envToFeatures(pbEnv *configpb.Environment) ([]*env.Feature, error) {
+	features := make([]*env.Feature, 0, len(pbEnv.GetFeatures())+1)
+	for _, feature := range pbEnv.GetFeatures() {
+		features = append(features, env.NewFeature(feature.GetName(), feature.GetEnabled()))
+	}
+	if pbEnv.GetEnableMacroCallTracking() {
+		features = append(features, env.NewFeature("cel.feature.macro_call_tracking", true))
+	}
+	return features, nil
+}
+
+func envToValidators(pbEnv *configpb.Environment) ([]*env.Validator, error) {
+	validators := make([]*env.Validator, 0, len(pbEnv.GetValidators()))
+	for _, pbValidator := range pbEnv.GetValidators() {
+		validator := env.NewValidator(pbValidator.GetName())
+		config := map[string]any{}
+		for k, v := range pbValidator.GetConfig() {
+			val := types.DefaultTypeAdapter.NativeToValue(v)
+			config[k] = val
+		}
+		validator.SetConfig(config)
+		validators = append(validators, validator)
+	}
+	return validators, nil
+}
+
+func protoDeclToFunctionsAndVariables(declarations []*celpb.Decl) ([]*env.Function, []*env.Variable, error) {
+	functions := make([]*env.Function, 0, len(declarations))
+	variables := make([]*env.Variable, 0, len(declarations))
+	for _, decl := range declarations {
+		switch decl.GetDeclKind().(type) {
+		case *celpb.Decl_Function:
+			fn, err := protoDeclToFunction(decl)
+			if err != nil {
+				return nil, nil, fmt.Errorf("protoDeclToFunction(%s) failed to create function: %w", decl.GetName(), err)
+			}
+			functions = append(functions, fn)
+		case *celpb.Decl_Ident:
+			t, err := types.ProtoAsType(decl.GetIdent().GetType())
+			if err != nil {
+				return nil, nil, fmt.Errorf("types.ProtoAsType(%s) failed to create type: %w", decl.GetIdent().GetType(), err)
+			}
+			variables = append(variables, env.NewVariable(decl.GetName(), env.SerializeTypeDesc(t)))
+		}
+	}
+	return functions, variables, nil
+}
+
+func envToStdLib(pbEnv *configpb.Environment) (*env.LibrarySubset, error) {
+	stdLib := env.NewLibrarySubset()
+	pbEnvStdLib := pbEnv.GetStdlib()
+	if pbEnvStdLib == nil {
+		if pbEnv.GetDisableStandardCelDeclarations() {
+			stdLib.SetDisabled(true)
+			return stdLib, nil
+		}
+		return nil, nil
+	}
+	if !stdLib.Disabled {
+		stdLib.SetDisabled(pbEnvStdLib.GetDisabled())
+	}
+	stdLib.SetDisableMacros(pbEnvStdLib.GetDisableMacros())
+	stdLib.AddIncludedMacros(pbEnvStdLib.GetIncludeMacros()...)
+	stdLib.AddExcludedMacros(pbEnvStdLib.GetExcludeMacros()...)
+	if pbEnvStdLib.GetIncludeFunctions() != nil {
+		for _, includeFn := range pbEnvStdLib.GetIncludeFunctions() {
+			if includeFn.GetFunction() != nil {
+				fn, err := protoDeclToFunction(includeFn)
+				if err != nil {
+					return nil, err
+				}
+				stdLib.AddIncludedFunctions(fn)
+			} else {
+				return nil, fmt.Errorf("IncludeFunctions must specify a function decl")
+			}
+		}
+	}
+	if pbEnvStdLib.GetExcludeFunctions() != nil {
+		for _, excludeFn := range pbEnvStdLib.GetExcludeFunctions() {
+			if excludeFn.GetFunction() != nil {
+				fn, err := protoDeclToFunction(excludeFn)
+				if err != nil {
+					return nil, err
+				}
+				stdLib.AddExcludedFunctions(fn)
+			} else {
+				return nil, fmt.Errorf("ExcludeFunctions must specify a function decl")
+			}
+		}
+	}
+	return stdLib, nil
+}
+
+func protoDeclToFunction(decl *celpb.Decl) (*env.Function, error) {
+	declFn := decl.GetFunction()
+	if declFn == nil {
+		return nil, nil
+	}
+	overloads := make([]*env.Overload, 0, len(declFn.GetOverloads()))
+	for _, o := range declFn.GetOverloads() {
+		args := make([]*env.TypeDesc, 0, len(o.GetParams()))
+		for _, p := range o.GetParams() {
+			t, err := types.ProtoAsType(p)
+			if err != nil {
+				return nil, err
+			}
+			args = append(args, env.SerializeTypeDesc(t))
+		}
+		res, err := types.ProtoAsType(o.GetResultType())
+		if err != nil {
+			return nil, err
+		}
+		ret := env.SerializeTypeDesc(res)
+		if o.IsInstanceFunction {
+			overloads = append(overloads, env.NewMemberOverload(o.GetOverloadId(), args[0], args[1:], ret))
+		} else {
+			overloads = append(overloads, env.NewOverload(o.GetOverloadId(), args, ret))
+		}
+	}
+	return env.NewFunction(decl.GetName(), overloads...), nil
+}
+
+// TypeDescriptorSetFile returns an EnvOption which loads type descriptors from a file.
+// The file must be in binary format.
+func TypeDescriptorSetFile(path string) cel.EnvOption {
+	return func(e *cel.Env) (*cel.Env, error) {
+		format := inferFileFormat(path)
+		if format != BinaryProto {
+			return nil, fmt.Errorf("type descriptor must be in binary format")
+		}
+		fds := &descpb.FileDescriptorSet{}
+		if err := loadProtoFile(path, BinaryProto, fds); err != nil {
+			return nil, err
+		}
+		var err error
+		e, err = e.Extend(cel.TypeDescs(fds))
+		if err != nil {
+			return nil, fmt.Errorf("e.Extend() with type descriptor set %v failed: %w", fds, err)
+		}
+		return e, nil
+	}
+}
+
+// InputExpression is an interface for an expression which can be compiled into a CEL AST and return
+// an optional policy metadata map.
+type InputExpression interface {
+	// CreateAST creates a CEL AST from the input expression using the provided compiler.
+	CreateAST(Compiler) (*cel.Ast, map[string]any, error)
+}
+
+// CompiledExpression is an InputExpression which loads a CheckedExpr from a file.
+type CompiledExpression struct {
+	Path string
+}
+
+// CreateAST creates a CEL AST from a checked expression file.
+// The file must be in one of the following formats:
+// - Binarypb
+// - Textproto
+func (c *CompiledExpression) CreateAST(_ Compiler) (*cel.Ast, map[string]any, error) {
+	var expr exprpb.CheckedExpr
+	format := inferFileFormat(c.Path)
+	if format != BinaryProto && format != TextProto {
+		return nil, nil, fmt.Errorf("file extension must be .binarypb or .textproto: found %v", format)
+	}
+	if err := loadProtoFile(c.Path, format, &expr); err != nil {
+		return nil, nil, err
+	}
+	return cel.CheckedExprToAst(&expr), nil, nil
+}
+
+// FileExpression is an InputExpression which loads a CEL expression or policy from a file.
+type FileExpression struct {
+	Path string
+}
+
+// CreateAST creates a CEL AST from a file using the provided compiler:
+// - All policy metadata options as executed using the policy metadata map to extend the
+// environment.
+// - All policy compiler options are passed on to compile the parsed policy.
+//
+// The file must be in one of the following formats:
+// - .cel: CEL string expression
+// - .celpolicy: CEL policy
+func (f *FileExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any, error) {
+	e, err := compiler.CreateEnv()
+	if err != nil {
+		return nil, nil, err
+	}
+	data, err := loadFile(f.Path)
+	if err != nil {
+		return nil, nil, err
+	}
+	format := inferFileFormat(f.Path)
+	switch format {
+	case CELString:
+		src := common.NewStringSource(string(data), f.Path)
+		ast, iss := e.CompileSource(src)
+		if iss.Err() != nil {
+			return nil, nil, fmt.Errorf("e.CompileSource(%q) failed: %w", src.Content(), iss.Err())
+		}
+		return ast, nil, nil
+	case CELPolicy, TextYAML:
+		src := policy.ByteSource(data, f.Path)
+		parser, err := compiler.CreatePolicyParser()
+		if err != nil {
+			return nil, nil, err
+		}
+		p, iss := parser.Parse(src)
+		if iss.Err() != nil {
+			return nil, nil, fmt.Errorf("parser.Parse(%q) failed: %w", src.Content(), iss.Err())
+		}
+		policyMetadata := clonePolicyMetadata(p)
+		for _, opt := range compiler.PolicyMetadataEnvOptions() {
+			if e, err = e.Extend(opt(policyMetadata)); err != nil {
+				return nil, nil, fmt.Errorf("e.Extend() with metadata option failed: %w", err)
+			}
+		}
+		ast, iss := policy.Compile(e, p, compiler.PolicyCompilerOptions()...)
+		if iss.Err() != nil {
+			return nil, nil, fmt.Errorf("policy.Compile(%q) failed: %w", src.Content(), iss.Err())
+		}
+		return ast, policyMetadata, nil
+	default:
+		return nil, nil, fmt.Errorf("unsupported file format: %v", format)
+	}
+}
+
+func clonePolicyMetadata(p *policy.Policy) map[string]any {
+	metadataKeys := p.MetadataKeys()
+	metadata := make(map[string]any, len(metadataKeys))
+	for _, key := range metadataKeys {
+		value, _ := p.Metadata(key)
+		metadata[key] = value
+	}
+	return metadata
+}
+
+// RawExpression is an InputExpression which loads a CEL expression from a string.
+type RawExpression struct {
+	Value string
+}
+
+// CreateAST creates a CEL AST from a raw CEL expression using the provided compiler.
+func (r *RawExpression) CreateAST(compiler Compiler) (*cel.Ast, map[string]any, error) {
+	e, err := compiler.CreateEnv()
+	if err != nil {
+		return nil, nil, err
+	}
+	ast, iss := e.Compile(r.Value)
+	if iss.Err() != nil {
+		return nil, nil, fmt.Errorf("e.Compile(%q) failed: %w", r.Value, iss.Err())
+	}
+	return ast, nil, nil
+}
diff --git a/tools/compiler/compiler_test.go b/tools/compiler/compiler_test.go
new file mode 100644
index 000000000..d0c9ad0be
--- /dev/null
+++ b/tools/compiler/compiler_test.go
@@ -0,0 +1,492 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package compiler
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common/decls"
+	"github.com/google/cel-go/common/env"
+	"github.com/google/cel-go/common/types"
+	"github.com/google/cel-go/ext"
+	"github.com/google/cel-go/policy"
+	"gopkg.in/yaml.v3"
+
+	celpb "cel.dev/expr"
+	configpb "cel.dev/expr/conformance"
+	structpb "google.golang.org/protobuf/types/known/structpb"
+)
+
+func TestEnvironmentFileCompareTextprotoAndYAML(t *testing.T) {
+	t.Run("compare textproto and yaml environment files", func(t *testing.T) {
+		pbEnv := testEnvProto()
+		protoConfig, err := configFromEnvProto(t, pbEnv)
+		if err != nil {
+			t.Fatalf("configFromEnvProto(%v) failed: %v", pbEnv, err)
+		}
+		config, err := parseEnv(t, "yaml_config", "testdata/config.yaml")
+		if err != nil {
+			t.Fatalf("parseEnv(%s) failed: %v", "testdata/config.yaml", err)
+		}
+		if protoConfig.Container != config.Container {
+			t.Fatalf("Container got %q, wanted %q", protoConfig.Container, config.Container)
+		}
+		if !reflect.DeepEqual(protoConfig.Imports, config.Imports) {
+			t.Fatalf("Imports got %v, wanted %v", protoConfig.Imports, config.Imports)
+		}
+		if !reflect.DeepEqual(protoConfig.StdLib, config.StdLib) {
+			t.Fatalf("StdLib got %v, wanted %v", protoConfig.StdLib, config.StdLib)
+		}
+		if len(protoConfig.Extensions) != len(config.Extensions) {
+			t.Fatalf("Extensions count got %d, wanted %d", len(protoConfig.Extensions), len(config.Extensions))
+		}
+		for _, protoConfigExt := range protoConfig.Extensions {
+			found := false
+			for _, configExt := range config.Extensions {
+				if reflect.DeepEqual(protoConfigExt, configExt) {
+					found = true
+					break
+				}
+			}
+			if !found {
+				t.Fatalf("Extensions got %v, wanted %v", protoConfig.Extensions, config.Extensions)
+			}
+		}
+		if !reflect.DeepEqual(protoConfig.ContextVariable, config.ContextVariable) {
+			t.Fatalf("ContextVariable got %v, wanted %v", protoConfig.ContextVariable, config.ContextVariable)
+		}
+		if len(protoConfig.Variables) != len(config.Variables) {
+			t.Fatalf("Variables count got %d, wanted %d", len(protoConfig.Variables), len(config.Variables))
+		} else {
+			for i, v := range protoConfig.Variables {
+				for j, p := range v.TypeDesc.Params {
+					if p.TypeName == "google.protobuf.Any" &&
+						config.Variables[i].TypeDesc.Params[j].TypeName == "dyn" {
+						p.TypeName = "dyn"
+					}
+				}
+				if !reflect.DeepEqual(v, config.Variables[i]) {
+					t.Fatalf("Variables[%d] not equal, got %v, wanted %v", i, v, config.Variables[i])
+				}
+			}
+		}
+		if len(protoConfig.Functions) != len(config.Functions) {
+			t.Fatalf("Functions count got %d, wanted %d", len(protoConfig.Functions), len(config.Functions))
+		} else {
+			for i, f := range protoConfig.Functions {
+				if !reflect.DeepEqual(f, config.Functions[i]) {
+					t.Fatalf("Functions[%d] not equal, got %v, wanted %v", i, f, config.Functions[i])
+				}
+			}
+		}
+		if len(protoConfig.Features) != len(config.Features) {
+			t.Fatalf("Features count got %d, wanted %d", len(protoConfig.Features), len(config.Features))
+		} else {
+			for i, f := range protoConfig.Features {
+				if !reflect.DeepEqual(f, config.Features[i]) {
+					t.Fatalf("Features[%d] not equal, got %v, wanted %v", i, f, config.Features[i])
+				}
+			}
+		}
+		if len(protoConfig.Validators) != len(config.Validators) {
+			t.Fatalf("Validators count got %d, wanted %d", len(protoConfig.Validators), len(config.Validators))
+		} else {
+			for i, v := range protoConfig.Validators {
+				if !reflect.DeepEqual(v, config.Validators[i]) {
+					t.Fatalf("Validators[%d] not equal, got %v, wanted %v", i, v, config.Validators[i])
+				}
+			}
+		}
+	})
+}
+
+func testEnvProto() *configpb.Environment {
+	return &configpb.Environment{
+		Name:        "test-environment",
+		Description: "Test environment",
+		Container:   "google.expr",
+		Imports: []*configpb.Environment_Import{
+			{Name: "google.expr.proto3.test.TestAllTypes"},
+		},
+		Stdlib: &configpb.LibrarySubset{
+			IncludeMacros: []string{"has", "exists"},
+			IncludeFunctions: []*celpb.Decl{
+				{
+					Name: "_==_",
+					DeclKind: &celpb.Decl_Function{
+						Function: &celpb.Decl_FunctionDecl{
+							Overloads: []*celpb.Decl_FunctionDecl_Overload{
+								{
+									OverloadId: "equals",
+									Params: []*celpb.Type{
+										{
+											TypeKind: &celpb.Type_Primitive{
+												Primitive: celpb.Type_STRING,
+											},
+										},
+										{
+											TypeKind: &celpb.Type_Primitive{
+												Primitive: celpb.Type_STRING,
+											},
+										},
+									},
+									ResultType: &celpb.Type{
+										TypeKind: &celpb.Type_Primitive{
+											Primitive: celpb.Type_BOOL,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+				{
+					Name: "_||_",
+					DeclKind: &celpb.Decl_Function{
+						Function: &celpb.Decl_FunctionDecl{
+							Overloads: []*celpb.Decl_FunctionDecl_Overload{
+								{
+									OverloadId: "logical_or",
+									Params: []*celpb.Type{
+										{
+											TypeKind: &celpb.Type_Primitive{
+												Primitive: celpb.Type_BOOL,
+											},
+										},
+										{
+											TypeKind: &celpb.Type_Primitive{
+												Primitive: celpb.Type_BOOL,
+											},
+										},
+									},
+									ResultType: &celpb.Type{
+										TypeKind: &celpb.Type_Primitive{
+											Primitive: celpb.Type_BOOL,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		Extensions: []*configpb.Extension{
+			{
+				Name:    "optional",
+				Version: "latest",
+			},
+			{
+				Name:    "lists",
+				Version: "latest",
+			},
+			{
+				Name:    "sets",
+				Version: "latest",
+			},
+		},
+		Declarations: []*celpb.Decl{
+			{
+				Name: "destination.ip",
+				DeclKind: &celpb.Decl_Ident{
+					Ident: &celpb.Decl_IdentDecl{
+						Type: &celpb.Type{
+							TypeKind: &celpb.Type_Primitive{
+								Primitive: celpb.Type_STRING,
+							},
+						},
+					},
+				},
+			},
+			{
+				Name: "origin.ip",
+				DeclKind: &celpb.Decl_Ident{
+					Ident: &celpb.Decl_IdentDecl{
+						Type: &celpb.Type{
+							TypeKind: &celpb.Type_Primitive{
+								Primitive: celpb.Type_STRING,
+							},
+						},
+					},
+				},
+			},
+			{
+				Name: "spec.restricted_destinations",
+				DeclKind: &celpb.Decl_Ident{
+					Ident: &celpb.Decl_IdentDecl{
+						Type: &celpb.Type{
+							TypeKind: &celpb.Type_ListType_{
+								ListType: &celpb.Type_ListType{
+									ElemType: &celpb.Type{
+										TypeKind: &celpb.Type_Primitive{
+											Primitive: celpb.Type_STRING,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			{
+				Name: "spec.origin",
+				DeclKind: &celpb.Decl_Ident{
+					Ident: &celpb.Decl_IdentDecl{
+						Type: &celpb.Type{
+							TypeKind: &celpb.Type_Primitive{
+								Primitive: celpb.Type_STRING,
+							},
+						},
+					},
+				},
+			},
+			{
+				Name: "request",
+				DeclKind: &celpb.Decl_Ident{
+					Ident: &celpb.Decl_IdentDecl{
+						Type: &celpb.Type{
+							TypeKind: &celpb.Type_MapType_{
+								MapType: &celpb.Type_MapType{
+									KeyType: &celpb.Type{
+										TypeKind: &celpb.Type_Primitive{
+											Primitive: celpb.Type_STRING,
+										},
+									},
+									ValueType: &celpb.Type{
+										TypeKind: &celpb.Type_WellKnown{
+											WellKnown: celpb.Type_ANY,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			{
+				Name: "resource",
+				DeclKind: &celpb.Decl_Ident{
+					Ident: &celpb.Decl_IdentDecl{
+						Type: &celpb.Type{
+							TypeKind: &celpb.Type_MapType_{
+								MapType: &celpb.Type_MapType{
+									KeyType: &celpb.Type{
+										TypeKind: &celpb.Type_Primitive{
+											Primitive: celpb.Type_STRING,
+										},
+									},
+									ValueType: &celpb.Type{
+										TypeKind: &celpb.Type_WellKnown{
+											WellKnown: celpb.Type_ANY,
+										},
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+			{
+				Name: "locationCode",
+				DeclKind: &celpb.Decl_Function{
+					Function: &celpb.Decl_FunctionDecl{
+						Overloads: []*celpb.Decl_FunctionDecl_Overload{
+							{
+								OverloadId: "locationCode_string",
+								Params: []*celpb.Type{
+									{
+										TypeKind: &celpb.Type_Primitive{
+											Primitive: celpb.Type_STRING,
+										},
+									},
+								},
+								ResultType: &celpb.Type{
+									TypeKind: &celpb.Type_Primitive{
+										Primitive: celpb.Type_STRING,
+									},
+								},
+							},
+						},
+					},
+				},
+			},
+		},
+		Validators: []*configpb.Validator{
+			{Name: "cel.validator.duration"},
+			{
+				Name: "cel.validator.nesting_comprehension_limit",
+				Config: map[string]*structpb.Value{
+					"limits": structpb.NewNumberValue(2),
+				},
+			},
+		},
+		Features: []*configpb.Feature{
+			{
+				Name:    "cel.feature.macro_call_tracking",
+				Enabled: true,
+			},
+		},
+	}
+}
+
+func configFromEnvProto(t *testing.T, pbEnv *configpb.Environment) (*env.Config, error) {
+	t.Helper()
+	envConfig, fileDescriptorSet, err := envProtoToConfig(pbEnv)
+	if err != nil {
+		return nil, err
+	}
+	var envOpts []cel.EnvOption
+	if fileDescriptorSet != nil {
+		envOpts = append(envOpts, cel.TypeDescs(fileDescriptorSet))
+	}
+	envOpts = append(envOpts, cel.FromConfig(envConfig, ext.ExtensionOptionFactory))
+	return envOptionToConfig(t, envConfig.Name, envOpts...)
+}
+
+func parseEnv(t *testing.T, name, path string) (*env.Config, error) {
+	t.Helper()
+	opts := EnvironmentFile(path)
+	return envOptionToConfig(t, name, opts)
+}
+
+func envOptionToConfig(t *testing.T, name string, opts ...cel.EnvOption) (*env.Config, error) {
+	t.Helper()
+	e, err := cel.NewCustomEnv(opts...)
+	if err != nil {
+		return nil, err
+	}
+	conf, err := e.ToConfig(name)
+	if err != nil {
+		return nil, err
+	}
+	return conf, nil
+}
+
+func TestFileExpressionCustomPolicyParser(t *testing.T) {
+	t.Run("test file expression custom policy parser", func(t *testing.T) {
+		envOpt := EnvironmentFile("../../policy/testdata/k8s/config.yaml")
+		parserOpt := policy.ParserOption(func(p *policy.Parser) (*policy.Parser, error) {
+			p.TagVisitor = policy.K8sTestTagHandler()
+			return p, nil
+		})
+		compilerOpts := []any{envOpt, parserOpt}
+		compiler, err := NewCompiler(compilerOpts...)
+		if err != nil {
+			t.Fatalf("NewCompiler() failed: %v", err)
+		}
+		policyFile := &FileExpression{
+			Path: "../../policy/testdata/k8s/policy.yaml",
+		}
+		k8sAst, _, err := policyFile.CreateAST(compiler)
+		if err != nil {
+			t.Fatalf("CreateAST() failed: %v", err)
+		}
+		if k8sAst == nil {
+			t.Fatalf("CreateAST() returned nil ast")
+		}
+	})
+}
+
+func TestFileExpressionPolicyMetadataOptions(t *testing.T) {
+	t.Run("test file expression policy metadata options", func(t *testing.T) {
+		envOpt := EnvironmentFile("testdata/custom_policy_config.yaml")
+		parserOpt := policy.ParserOption(func(p *policy.Parser) (*policy.Parser, error) {
+			p.TagVisitor = customTagHandler{TagVisitor: policy.DefaultTagVisitor()}
+			return p, nil
+		})
+		policyMetadataOpt := PolicyMetadataEnvOption(ParsePolicyVariables)
+		compilerOpts := []any{envOpt, parserOpt, policyMetadataOpt}
+		compiler, err := NewCompiler(compilerOpts...)
+		if err != nil {
+			t.Fatalf("NewCompiler() failed: %v", err)
+		}
+		policyFile := &FileExpression{
+			Path: "testdata/custom_policy.celpolicy",
+		}
+		ast, _, err := policyFile.CreateAST(compiler)
+		if err != nil {
+			t.Fatalf("CreateAST() failed: %v", err)
+		}
+		if ast == nil {
+			t.Fatalf("CreateAST() returned nil ast")
+		}
+	})
+}
+
+func ParsePolicyVariables(metadata map[string]any) cel.EnvOption {
+	variables := []*decls.VariableDecl{}
+	for n, t := range metadata {
+		variables = append(variables, decls.NewVariable(n, parseCustomPolicyVariableType(t.(string))))
+	}
+	return cel.VariableDecls(variables...)
+}
+
+func parseCustomPolicyVariableType(t string) *types.Type {
+	switch t {
+	case "int":
+		return types.IntType
+	case "string":
+		return types.StringType
+	default:
+		return types.UnknownType
+	}
+}
+
+type variableType struct {
+	VariableName string `yaml:"variable_name"`
+	VariableType string `yaml:"variable_type"`
+}
+
+type customTagHandler struct {
+	policy.TagVisitor
+}
+
+func (customTagHandler) PolicyTag(ctx policy.ParserContext, id int64, tagName string, node *yaml.Node, p *policy.Policy) {
+	switch tagName {
+	case "variable_types":
+		varList := []*variableType{}
+		if err := node.Decode(&varList); err != nil {
+			ctx.ReportErrorAtID(id, "invalid yaml variable_types node: %v, error: %w", node, err)
+			return
+		}
+		for _, v := range varList {
+			p.SetMetadata(v.VariableName, v.VariableType)
+		}
+	default:
+		ctx.ReportErrorAtID(id, "unsupported policy tag: %s", tagName)
+	}
+}
+
+func TestRawExpressionCreateAst(t *testing.T) {
+	t.Run("test raw expression create ast", func(t *testing.T) {
+		envOpt := EnvironmentFile("testdata/config.yaml")
+		compiler, err := NewCompiler(envOpt)
+		if err != nil {
+			t.Fatalf("NewCompiler() failed: %v", err)
+		}
+		rawExpr := &RawExpression{
+			Value: "locationCode(destination.ip)==locationCode(origin.ip)",
+		}
+		ast, _, err := rawExpr.CreateAST(compiler)
+		if err != nil {
+			t.Fatalf("CreateAST() failed: %v", err)
+		}
+		if ast == nil {
+			t.Fatalf("CreateAST() returned nil ast")
+		}
+	})
+}
diff --git a/tools/compiler/testdata/config.yaml b/tools/compiler/testdata/config.yaml
new file mode 100644
index 000000000..a63a859ad
--- /dev/null
+++ b/tools/compiler/testdata/config.yaml
@@ -0,0 +1,84 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "test-environment"
+description: "Test environment"
+container: "google.expr"
+imports:
+- name: "google.expr.proto3.test.TestAllTypes"
+stdlib:
+  include_macros:
+  - has
+  - exists
+  include_functions:
+  - name: "_==_"
+    overloads:
+    - id: equals
+      args:
+      - type_name: "string"
+      - type_name: "string"
+      return:
+        type_name: "bool"
+  - name: "_||_"
+    overloads:
+    - id: logical_or
+      args:
+      - type_name: "bool"
+      - type_name: "bool"
+      return:
+        type_name: "bool"
+extensions:
+- name: "optional"
+  version: "latest"
+- name: "lists"
+  version: "latest"
+- name: "sets"
+  version: "latest"
+variables:
+- name: "destination.ip"
+  type_name: "string"
+- name: "origin.ip"
+  type_name: "string"
+- name: "spec.restricted_destinations"
+  type_name: "list"
+  params:
+  - type_name: "string"
+- name: "spec.origin"
+  type_name: "string"
+- name: "request"
+  type_name: "map"
+  params:
+  - type_name: "string"
+  - type_name: "dyn"
+- name: "resource"
+  type_name: "map"
+  params:
+  - type_name: "string"
+  - type_name: "dyn"
+functions:
+- name: "locationCode"
+  overloads:
+  - id: "locationCode_string"
+    args:
+    - type_name: "string"
+    return:
+      type_name: "string"
+validators:
+- name: cel.validator.duration
+- name: cel.validator.nesting_comprehension_limit
+  config:
+    limit: 2
+features:
+- name: cel.feature.macro_call_tracking
+  enabled: true
diff --git a/tools/compiler/testdata/custom_policy.celpolicy b/tools/compiler/testdata/custom_policy.celpolicy
new file mode 100644
index 000000000..663fcf0a7
--- /dev/null
+++ b/tools/compiler/testdata/custom_policy.celpolicy
@@ -0,0 +1,25 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "custom_policy"
+variable_types:
+  - variable_name: "variable1"
+    variable_type: "int"
+  - variable_name: "variable2"
+    variable_type: "string"
+rule:
+  match:
+    - condition: |
+        variable1 == 1 || variable2 == "known"
+      output: "true"
diff --git a/tools/compiler/testdata/custom_policy_config.yaml b/tools/compiler/testdata/custom_policy_config.yaml
new file mode 100644
index 000000000..460fab4b4
--- /dev/null
+++ b/tools/compiler/testdata/custom_policy_config.yaml
@@ -0,0 +1,18 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "custom_policy_config"
+extensions:
+- name: "optional"
+  version: "latest"
diff --git a/tools/go.mod b/tools/go.mod
new file mode 100644
index 000000000..392efac8f
--- /dev/null
+++ b/tools/go.mod
@@ -0,0 +1,22 @@
+module github.com/google/cel-go/tools
+
+go 1.23.0
+
+require (
+	cel.dev/expr v0.22.1
+	github.com/google/cel-go v0.22.0
+	github.com/google/cel-go/policy v0.0.0-20250311174852-f5ea07b389a1
+	google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf
+	google.golang.org/protobuf v1.36.5
+	gopkg.in/yaml.v3 v3.0.1
+)
+
+require (
+	github.com/antlr4-go/antlr/v4 v4.13.1 // indirect
+	github.com/stoewer/go-strcase v1.3.0 // indirect
+	golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 // indirect
+	golang.org/x/text v0.22.0 // indirect
+	google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf // indirect
+)
+
+replace github.com/google/cel-go => ../.
diff --git a/tools/go.sum b/tools/go.sum
new file mode 100644
index 000000000..b34becfc8
--- /dev/null
+++ b/tools/go.sum
@@ -0,0 +1,37 @@
+cel.dev/expr v0.22.1 h1:xoFEsNh972Yzey8N9TCPx2nDvMN7TMhQEzxLuj/iRrI=
+cel.dev/expr v0.22.1/go.mod h1:MrpN08Q+lEBs+bGYdLxxHkZoUSsCp0nSKTs0nTymJgw=
+github.com/antlr4-go/antlr/v4 v4.13.1 h1:SqQKkuVZ+zWkMMNkjy5FZe5mr5WURWnlpmOuzYWrPrQ=
+github.com/antlr4-go/antlr/v4 v4.13.1/go.mod h1:GKmUxMtwp6ZgGwZSva4eWPC5mS6vUAmOABFgjdkM7Nw=
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/google/cel-go/policy v0.0.0-20250311174852-f5ea07b389a1 h1:jT/04RYwo++S9tvHggXWuAqvnc2Pi0BTHYsZYVOoMOs=
+github.com/google/cel-go/policy v0.0.0-20250311174852-f5ea07b389a1/go.mod h1:dgvqy3CzFx17CBMkL0s1hd0r1+rEQOo85tDpr0g6Dp4=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/stoewer/go-strcase v1.3.0 h1:g0eASXYtp+yvN9fK8sH94oCIk0fau9uV1/ZdJ0AVEzs=
+github.com/stoewer/go-strcase v1.3.0/go.mod h1:fAH5hQ5pehh+j3nZfvwdk2RgEgQjAoM8wodgtPmh1xo=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
+github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/exp v0.0.0-20240823005443-9b4947da3948 h1:kx6Ds3MlpiUHKj7syVnbp57++8WpuKPcR5yjLBjvLEA=
+golang.org/x/exp v0.0.0-20240823005443-9b4947da3948/go.mod h1:akd2r19cwCdwSwWeIdzYQGa/EZZyqcOdwWiwj5L5eKQ=
+golang.org/x/text v0.22.0 h1:bofq7m3/HAFvbF51jz3Q9wLg3jkvSPuiZu/pD1XwgtM=
+golang.org/x/text v0.22.0/go.mod h1:YRoo4H8PVmsu+E3Ou7cqLVH8oXWIHVoX0jqUWALQhfY=
+google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf h1:BdIVRm+fyDUn8lrZLPSlBCfM/YKDwUBYgDoLv9+DYo0=
+google.golang.org/genproto/googleapis/api v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:jbe3Bkdp+Dh2IrslsFCklNhweNTBgSYanP1UXhJDhKg=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf h1:dHDlF3CWxQkefK9IJx+O8ldY0gLygvrlYRBNbPqDWuY=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20250311190419-81fb87f6b8bf/go.mod h1:LuRYeWDFV6WOn90g357N17oMCaxpgCnbi/44qJvDn2I=
+google.golang.org/protobuf v1.36.5 h1:tPhr+woSbjfYvY6/GPufUoYizxw1cF/yFoxJ2fmpwlM=
+google.golang.org/protobuf v1.36.5/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/vendor/cel.dev/expr/.bazelversion b/vendor/cel.dev/expr/.bazelversion
index 26bc914a3..13c50892b 100644
--- a/vendor/cel.dev/expr/.bazelversion
+++ b/vendor/cel.dev/expr/.bazelversion
@@ -1,2 +1,2 @@
-7.0.1
+7.3.2
 # Keep this pinned version in parity with cel-go
diff --git a/vendor/cel.dev/expr/MODULE.bazel b/vendor/cel.dev/expr/MODULE.bazel
index 9794266f5..c0a631316 100644
--- a/vendor/cel.dev/expr/MODULE.bazel
+++ b/vendor/cel.dev/expr/MODULE.bazel
@@ -13,12 +13,24 @@ bazel_dep(
 )
 bazel_dep(
     name = "googleapis",
-    version = "0.0.0-20240819-fe8ba054a",
+    version = "0.0.0-20241220-5e258e33.bcr.1",
     repo_name = "com_google_googleapis",
 )
+bazel_dep(
+    name = "googleapis-cc",
+    version = "1.0.0",
+)
+bazel_dep(
+    name = "googleapis-java",
+    version = "1.0.0",
+)
+bazel_dep(
+    name = "googleapis-go",
+    version = "1.0.0",
+)
 bazel_dep(
     name = "protobuf",
-    version = "26.0",
+    version = "27.0",
     repo_name = "com_google_protobuf",
 )
 bazel_dep(
@@ -27,7 +39,7 @@ bazel_dep(
 )
 bazel_dep(
     name = "rules_go",
-    version = "0.49.0",
+    version = "0.50.1",
     repo_name = "io_bazel_rules_go",
 )
 bazel_dep(
@@ -50,14 +62,6 @@ python.toolchain(
     python_version = "3.11",
 )
 
-switched_rules = use_extension("@com_google_googleapis//:extensions.bzl", "switched_rules")
-switched_rules.use_languages(
-    cc = True,
-    go = True,
-    java = True,
-)
-use_repo(switched_rules, "com_google_googleapis_imports")
-
 go_sdk = use_extension("@io_bazel_rules_go//go:extensions.bzl", "go_sdk")
 go_sdk.download(version = "1.21.1")
 
diff --git a/vendor/cel.dev/expr/cloudbuild.yaml b/vendor/cel.dev/expr/cloudbuild.yaml
index c40881f12..e3e533a04 100644
--- a/vendor/cel.dev/expr/cloudbuild.yaml
+++ b/vendor/cel.dev/expr/cloudbuild.yaml
@@ -1,5 +1,5 @@
 steps:
-- name: 'gcr.io/cloud-builders/bazel:7.0.1'
+- name: 'gcr.io/cloud-builders/bazel:7.3.2'
   entrypoint: bazel
   args: ['build', '...']
   id: bazel-build
diff --git a/vendor/modules.txt b/vendor/modules.txt
index 620a2e524..dfdf1bd13 100644
--- a/vendor/modules.txt
+++ b/vendor/modules.txt
@@ -1,4 +1,4 @@
-# cel.dev/expr v0.21.2
+# cel.dev/expr v0.22.1
 ## explicit; go 1.21.1
 cel.dev/expr
 # github.com/antlr4-go/antlr/v4 v4.13.0


Reviews:
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T20:54:44Z
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-20T20:56:27Z
user: login: aakash070
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-21T06:34:32Z
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T22:16:13Z
user: login: TristonianJones
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-24T22:17:11Z
user: login: TristonianJones
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-25T22:30:54Z

Review comments:
diff_hunk: @@ -0,0 +1,25 @@
+# Copyright 2025 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#    https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+name: "custom_policy"
+rule:
+  match:
+    - condition: |
+        variable1 == 1 || variable2 == "known"
+      output: "true"
+variable_types:
+  - variable_name: "variable1"
+    variable_type: "int"
+  - variable_name: "variable2"
+    variable_type: "string"
path: tools/compiler/testdata/custom_policy.celpolicy
user: login: TristonianJones
body: Could you ensure that `variable_types` appears before the `rule` declaration?
created_at: 2025-03-20T20:54:44Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,138 @@
+# proto-file: cel.dev/expr/conformance/env_config.proto
path: tools/compiler/testdata/config.textproto
user: login: TristonianJones
body: At a file level, we should really just validate the round-tripping logic yaml -> proto -> yaml and validate that the roundtrip is correct.  If desired, you could convert yaml -> textproto in memory and run tests against the converted text proto. Let's not check in the textproto file itself though.
created_at: 2025-03-20T20:56:27Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,224 @@
+package compiler
path: tools/compiler/compiler_test.go
user: login: TristonianJones
body: nit: missing copyright
created_at: 2025-03-20T20:57:35Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,515 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package compiler exposes a standard way to set up a compiler which can be used for CEL
+// expressions and policies.
+package compiler
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"gopkg.in/yaml.v3"
+
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common"
+	"github.com/google/cel-go/common/env"
+	"github.com/google/cel-go/common/types"
+	"github.com/google/cel-go/ext"
+	"github.com/google/cel-go/policy"
+	"google.golang.org/protobuf/encoding/prototext"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+
+	celpb "cel.dev/expr"
+	configpb "cel.dev/expr/conformance"
+	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
+	descpb "google.golang.org/protobuf/types/descriptorpb"
+)
+
+// FileFormat represents the format of the file being loaded.
+type FileFormat int
+
+const (
+	// Unspecified is used when the file format is not determined.
+	Unspecified FileFormat = iota + 1
+	// BinaryProto is used for a binary proto file.
+	BinaryProto
+	// TextProto is used for a text proto file.
+	TextProto
+	// TextYAML is used for a YAML file.
+	TextYAML
+	// CELString is used for a CEL string expression defined in a file with .cel extension.
+	CELString
+	// CELPolicy is used for a CEL policy file with .celpolicy extension.
+	CELPolicy
+)
+
+// PolicyMetadataEnvOption represents a function which accepts a policy metadata map and returns an
+// environment option used to extend the CEL environment.
+//
+// The policy metadata map is generally produced as a byproduct of the parsing the policy and it can
+// be optionally customised by providing a custom policy parser.
+type PolicyMetadataEnvOption func(map[string]any) cel.EnvOption
+
+// Compiler interface is used to set up a compiler with the following capabilities:
+// - create a CEL environment
+// - create a policy parser
+// - fetch policy compiler options
+// - fetch policy environment options
+//
+// Note: This compiler is not the same as the CEL expression compiler, rather it provides an
+// abstraction layer which can create the different components needed for parsing and compiling CEL
+// expressions and policies.
+type Compiler interface {
+	// CreateEnv creates a CEL environment with the configured environment options.
+	CreateEnv() (*cel.Env, error)
+	// CreatePolicyParser creates a policy parser using the optionally configured parser options.
+	CreatePolicyParser() (*policy.Parser, error)
+	// PolicyCompilerOptions returns the policy compiler options.
+	PolicyCompilerOptions() []policy.CompilerOption
+	// PolicyMetadataEnvOptions returns the policy metadata environment options.
+	PolicyMetadataEnvOptions() []PolicyMetadataEnvOption
+}
+
+type compiler struct {
+	envOptions               []cel.EnvOption
+	policyParserOptions      []policy.ParserOption
+	policyCompilerOptions    []policy.CompilerOption
+	policyMetadataEnvOptions []PolicyMetadataEnvOption
+	env                      *cel.Env
+}
+
+// NewCompiler creates a new compiler with a set of functional options.
+func NewCompiler(opts ...any) (Compiler, error) {
+	c := &compiler{
+		envOptions:               []cel.EnvOption{},
+		policyParserOptions:      []policy.ParserOption{},
+		policyCompilerOptions:    []policy.CompilerOption{},
+		policyMetadataEnvOptions: []PolicyMetadataEnvOption{},
+	}
+	for _, opt := range opts {
+		switch opt := opt.(type) {
+		case cel.EnvOption:
+			c.envOptions = append(c.envOptions, opt)
+		case policy.ParserOption:
+			c.policyParserOptions = append(c.policyParserOptions, opt)
+		case policy.CompilerOption:
+			c.policyCompilerOptions = append(c.policyCompilerOptions, opt)
+		case PolicyMetadataEnvOption:
+			c.policyMetadataEnvOptions = append(c.policyMetadataEnvOptions, opt)
+		default:
+			return nil, fmt.Errorf("unsupported compiler option: %v", opt)
+		}
+	}
+	return c, nil
+}
+
+func (c *compiler) CreateEnv() (*cel.Env, error) {
+	if c.env != nil {
+		return c.env, nil
+	}
+	env, err := cel.NewCustomEnv(c.envOptions...)
+	if err != nil {
+		return nil, err
+	}
+	c.env = env
+	return c.env, nil
+}
+
+func (c *compiler) CreatePolicyParser() (*policy.Parser, error) {
+	return policy.NewParser(c.policyParserOptions...)
+}
+
+func (c *compiler) PolicyCompilerOptions() []policy.CompilerOption {
path: tools/compiler/compiler.go
user: login: TristonianJones
body: nit: please comment all exposed functions
created_at: 2025-03-20T20:58:07Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,138 @@
+# proto-file: cel.dev/expr/conformance/env_config.proto
path: tools/compiler/testdata/config.textproto
user: login: aakash070
body: Since we're not providing a round tripping logic yaml->proto->yaml as a capability, i am not sure if adding the same for tests would be beneficial as the transformation logic could get biased for the test scenario.
I have replaced the textproto file with an in-memory proto derived object instead, it should serve the same purpose without having to check in the file.
created_at: 2025-03-21T06:34:32Z
author_association: COLLABORATOR
diff_hunk: @@ -0,0 +1,515 @@
+// Copyright 2025 Google LLC
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+//    https://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+// Package compiler exposes a standard way to set up a compiler which can be used for CEL
+// expressions and policies.
+package compiler
+
+import (
+	"fmt"
+	"os"
+	"path/filepath"
+
+	"gopkg.in/yaml.v3"
+
+	"github.com/google/cel-go/cel"
+	"github.com/google/cel-go/common"
+	"github.com/google/cel-go/common/env"
+	"github.com/google/cel-go/common/types"
+	"github.com/google/cel-go/ext"
+	"github.com/google/cel-go/policy"
+	"google.golang.org/protobuf/encoding/prototext"
+	"google.golang.org/protobuf/proto"
+	"google.golang.org/protobuf/reflect/protoreflect"
+
+	celpb "cel.dev/expr"
+	configpb "cel.dev/expr/conformance"
+	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
+	descpb "google.golang.org/protobuf/types/descriptorpb"
+)
+
+// FileFormat represents the format of the file being loaded.
+type FileFormat int
+
+const (
+	// Unspecified is used when the file format is not determined.
+	Unspecified FileFormat = iota + 1
+	// BinaryProto is used for a binary proto file.
+	BinaryProto
+	// TextProto is used for a text proto file.
+	TextProto
+	// TextYAML is used for a YAML file.
+	TextYAML
+	// CELString is used for a CEL string expression defined in a file with .cel extension.
+	CELString
+	// CELPolicy is used for a CEL policy file with .celpolicy extension.
+	CELPolicy
+)
+
+// PolicyMetadataEnvOption represents a function which accepts a policy metadata map and returns an
+// environment option used to extend the CEL environment.
+//
+// The policy metadata map is generally produced as a byproduct of the parsing the policy and it can
+// be optionally customised by providing a custom policy parser.
+type PolicyMetadataEnvOption func(map[string]any) cel.EnvOption
+
+// Compiler interface is used to set up a compiler with the following capabilities:
+// - create a CEL environment
+// - create a policy parser
+// - fetch policy compiler options
+// - fetch policy environment options
+//
+// Note: This compiler is not the same as the CEL expression compiler, rather it provides an
+// abstraction layer which can create the different components needed for parsing and compiling CEL
+// expressions and policies.
+type Compiler interface {
+	// CreateEnv creates a CEL environment with the configured environment options.
+	CreateEnv() (*cel.Env, error)
+	// CreatePolicyParser creates a policy parser using the optionally configured parser options.
+	CreatePolicyParser() (*policy.Parser, error)
+	// PolicyCompilerOptions returns the policy compiler options.
+	PolicyCompilerOptions() []policy.CompilerOption
+	// PolicyMetadataEnvOptions returns the policy metadata environment options.
+	PolicyMetadataEnvOptions() []PolicyMetadataEnvOption
+}
+
+type compiler struct {
+	envOptions               []cel.EnvOption
+	policyParserOptions      []policy.ParserOption
+	policyCompilerOptions    []policy.CompilerOption
+	policyMetadataEnvOptions []PolicyMetadataEnvOption
+	env                      *cel.Env
+}
+
+// NewCompiler creates a new compiler with a set of functional options.
+func NewCompiler(opts ...any) (Compiler, error) {
+	c := &compiler{
+		envOptions:               []cel.EnvOption{},
+		policyParserOptions:      []policy.ParserOption{},
+		policyCompilerOptions:    []policy.CompilerOption{},
+		policyMetadataEnvOptions: []PolicyMetadataEnvOption{},
+	}
+	for _, opt := range opts {
+		switch opt := opt.(type) {
+		case cel.EnvOption:
+			c.envOptions = append(c.envOptions, opt)
+		case policy.ParserOption:
+			c.policyParserOptions = append(c.policyParserOptions, opt)
+		case policy.CompilerOption:
+			c.policyCompilerOptions = append(c.policyCompilerOptions, opt)
+		case PolicyMetadataEnvOption:
+			c.policyMetadataEnvOptions = append(c.policyMetadataEnvOptions, opt)
+		default:
+			return nil, fmt.Errorf("unsupported compiler option: %v", opt)
+		}
+	}
+	return c, nil
+}
+
+func (c *compiler) CreateEnv() (*cel.Env, error) {
+	if c.env != nil {
+		return c.env, nil
+	}
+	env, err := cel.NewCustomEnv(c.envOptions...)
+	if err != nil {
+		return nil, err
+	}
+	c.env = env
+	return c.env, nil
+}
+
+func (c *compiler) CreatePolicyParser() (*policy.Parser, error) {
+	return policy.NewParser(c.policyParserOptions...)
+}
+
+func (c *compiler) PolicyCompilerOptions() []policy.CompilerOption {
+	return c.policyCompilerOptions
+}
+
+func (c *compiler) PolicyMetadataEnvOptions() []PolicyMetadataEnvOption {
+	return c.policyMetadataEnvOptions
+}
+
+func loadFile(path string) ([]byte, error) {
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, fmt.Errorf("failed to read file %q: %v", path, err)
+	}
+	return data, err
+}
+
+func loadProtoFile(path string, format FileFormat, out protoreflect.ProtoMessage) error {
+	unmarshaller := proto.Unmarshal
+	if format == TextProto {
+		unmarshaller = prototext.Unmarshal
+	}
+	data, err := loadFile(path)
+	if err != nil {
+		return err
+	}
+	return unmarshaller(data, out)
+}
+
+func inferFileFormat(path string) FileFormat {
+	extension := filepath.Ext(path)
+	switch extension {
+	case ".textproto":
+		return TextProto
+	case ".yaml":
+		return TextYAML
+	case ".binarypb", ".fds":
+		return BinaryProto
+	case ".cel":
+		return CELString
+	case ".celpolicy":
+		return CELPolicy
+	default:
+		return Unspecified
+	}
+}
+
+// EnvironmentFile returns an EnvOption which loads a serialized CEL environment from a file.
+// The file must be in one of the following formats:
+// - textproto
+// - yaml
+// - binarypb
+func EnvironmentFile(path string) cel.EnvOption {
+	return func(e *cel.Env) (*cel.Env, error) {
+		format := inferFileFormat(path)
+		if format != TextProto && format != TextYAML && format != BinaryProto {
+			return nil, fmt.Errorf("file extension must be one of .textproto, .yaml, .binarypb: found %v", format)
+		}
+		var envConfig *env.Config
+		var fileDescriptorSet *descpb.FileDescriptorSet
+		switch format {
+		case TextProto, BinaryProto:
+			pbEnv := &configpb.Environment{}
+			if err := loadProtoFile(path, format, pbEnv); err != nil {
+				return nil, err
+			}
+			envConfig = env.NewConfig(pbEnv.GetName())
+			envConfig.Description = pbEnv.GetDescription()
+			envConfig.SetContainer(pbEnv.GetContainer())
+			for _, imp := range pbEnv.GetImports() {
+				envConfig.AddImports(env.NewImport(imp.GetName()))
+			}
+			stdLib, err := envToStdLib(pbEnv)
+			if err != nil {
+				return nil, err
+			}
+			envConfig.SetStdLib(stdLib)
+			extensions := make([]*env.Extension, 0, len(pbEnv.GetExtensions()))
+			for _, extension := range pbEnv.GetExtensions() {
+				extensions = append(extensions, &env.Extension{Name: extension.GetName(), Version: extension.GetVersion()})
+			}
+			envConfig.AddExtensions(extensions...)
+			if contextVariable := pbEnv.GetContextVariable(); contextVariable != nil {
+				envConfig.SetContextVariable(env.NewContextVariable(contextVariable.GetTypeName()))
+			}
+			functions, variables, err := protoDeclToFunctionsAndVariables(pbEnv.GetDeclarations())
+			if err != nil {
+				return nil, err
+			}
+			envConfig.AddFunctions(functions...)
+			envConfig.AddVariables(variables...)
+			validators, err := envToValidators(pbEnv)
+			if err != nil {
+				return nil, err
+			}
+			envConfig.AddValidators(validators...)
+			features, err := envToFeatures(pbEnv)
+			if err != nil {
+				return nil, err
+			}
+			envConfig.AddFeatures(features...)
+			fileDescriptorSet = pbEnv.GetMessageTypeExtension()
+		case TextYAML:
+			envConfig = &env.Config{}
+			data, err := loadFile(path)
+			if err != nil {
+				return nil, err
+			}
+			err = yaml.Unmarshal(data, envConfig)
+			if err != nil {
+				return nil, fmt.Errorf("yaml.Unmarshal failed to map CEL environment: %w", err)
+			}
+		default:
+			return nil, fmt.Errorf("unsupported format: %v, file: %s", format, path)
+		}
+		var envOpts []cel.EnvOption
+		if fileDescriptorSet != nil {
+			envOpts = append(envOpts, cel.TypeDescs(fileDescriptorSet))
+		}
+		envOpts = append(envOpts, cel.FromConfig(envConfig, ext.ExtensionOptionFactory))
+		var err error
+		e, err = e.Extend(envOpts...)
+		if err != nil {
+			return nil, fmt.Errorf("e.Extend() with env options %v failed: %w", envOpts, err)
+		}
+		return e, nil
+	}
+}
+
+func envToFeatures(pbEnv *configpb.Environment) ([]*env.Feature, error) {
+	features := make([]*env.Feature, 0, len(pbEnv.GetFeatures())+1)
+	for _, feature := range pbEnv.GetFeatures() {
+		features = append(features, env.NewFeature(feature.GetName(), feature.GetEnabled()))
+	}
+	if pbEnv.GetEnableMacroCallTracking() {
+		features = append(features, env.NewFeature("cel.feature.macro_call_tracking", true))
+	}
+	return features, nil
+}
+
+func envToValidators(pbEnv *configpb.Environment) ([]*env.Validator, error) {
+	validators := make([]*env.Validator, 0, len(pbEnv.GetValidators()))
+	for _, pbValidator := range pbEnv.GetValidators() {
+		validator := env.NewValidator(pbValidator.GetName())
+		config := map[string]any{}
+		for k, v := range pbValidator.GetConfig() {
+			val := types.DefaultTypeAdapter.NativeToValue(v)
+			config[k] = val
+		}
+		validator.SetConfig(config)
+		validators = append(validators, validator)
+	}
+	return validators, nil
+}
+
+func protoDeclToFunctionsAndVariables(declarations []*celpb.Decl) ([]*env.Function, []*env.Variable, error) {
+	functions := make([]*env.Function, 0, len(declarations))
+	variables := make([]*env.Variable, 0, len(declarations))
+	for _, decl := range declarations {
+		switch decl.GetDeclKind().(type) {
+		case *celpb.Decl_Function:
+			fn, err := protoDeclToFunction(decl)
+			if err != nil {
+				return nil, nil, fmt.Errorf("protoDeclToFunction(%s) failed to create function: %w", decl.GetName(), err)
+			}
+			functions = append(functions, fn)
+		case *celpb.Decl_Ident:
+			t, err := types.ProtoAsType(decl.GetIdent().GetType())
+			if err != nil {
+				return nil, nil, fmt.Errorf("types.ProtoAsType(%s) failed to create type: %w", decl.GetIdent().GetType(), err)
+			}
+			variables = append(variables, env.NewVariable(decl.GetName(), env.SerializeTypeDesc(t)))
+		}
+	}
+	return functions, variables, nil
+}
+
+func envToStdLib(pbEnv *configpb.Environment) (*env.LibrarySubset, error) {
+	stdLib := env.NewLibrarySubset()
+	if pbEnv.GetDisableStandardCelDeclarations() {
+		stdLib.SetDisabled(true)
+	}
+	if pbEnvStdLib := pbEnv.GetStdlib(); pbEnvStdLib != nil {
+		if !stdLib.Disabled {
+			stdLib.SetDisabled(pbEnvStdLib.GetDisabled())
+		}
+		stdLib.SetDisableMacros(pbEnvStdLib.GetDisableMacros())
+		stdLib.AddIncludedMacros(pbEnvStdLib.GetIncludeMacros()...)
+		stdLib.AddExcludedMacros(pbEnvStdLib.GetExcludeMacros()...)
+		if pbEnvStdLib.GetIncludeFunctions() != nil {
+			for _, includeFn := range pbEnvStdLib.GetIncludeFunctions() {
+				if includeFn.GetFunction() != nil {
+					fn, err := protoDeclToFunction(includeFn)
+					if err != nil {
+						return nil, err
+					}
+					stdLib.AddIncludedFunctions(fn)
+				} else {
+					return nil, fmt.Errorf("IncludeFunctions must specify a function decl")
+				}
+			}
+		}
+		if pbEnvStdLib.GetExcludeFunctions() != nil {
+			for _, excludeFn := range pbEnvStdLib.GetExcludeFunctions() {
+				if excludeFn.GetFunction() != nil {
+					fn, err := protoDeclToFunction(excludeFn)
+					if err != nil {
+						return nil, err
+					}
+					stdLib.AddExcludedFunctions(fn)
+				} else {
+					return nil, fmt.Errorf("ExcludeFunctions must specify a function decl")
+				}
+			}
+		}
+	}
+	return stdLib, nil
+}
+
+func protoDeclToFunction(decl *celpb.Decl) (*env.Function, error) {
+	declFn := decl.GetFunction()
+	if declFn == nil {
+		return nil, nil
+	}
+	overloads := make([]*env.Overload, 0, len(declFn.GetOverloads()))
+	for _, o := range declFn.GetOverloads() {
+		args := make([]*env.TypeDesc, 0, len(o.GetParams()))
+		for _, p := range o.GetParams() {
+			t, err := types.ProtoAsType(p)
+			if err != nil {
+				return nil, err
+			}
+			args = append(args, env.SerializeTypeDesc(t))
+		}
+		res, err := types.ProtoAsType(o.GetResultType())
+		if err != nil {
+			return nil, err
+		}
+		ret := env.SerializeTypeDesc(res)
+		if o.IsInstanceFunction {
+			overloads = append(overloads, env.NewMemberOverload(o.GetOverloadId(), args[0], args[1:], ret))
+		} else {
+			overloads = append(overloads, env.NewOverload(o.GetOverloadId(), args, ret))
+		}
+	}
+	return env.NewFunction(decl.GetName(), overloads...), nil
+}
+
+// TypeDescriptorSetFile returns an EnvOption which loads type descriptors from a file.
+// The file must be in binary format.
+func TypeDescriptorSetFile(path string) cel.EnvOption {
+	return func(e *cel.Env) (*cel.Env, error) {
+		format := inferFileFormat(path)
+		if format != BinaryProto {
+			return nil, fmt.Errorf("type descriptor must be in binary format")
+		}
+		fds := &descpb.FileDescriptorSet{}
+		if err := loadProtoFile(path, BinaryProto, fds); err != nil {
+			return nil, err
+		}
+		var err error
+		e, err = e.Extend(cel.TypeDescs(fds))
+		if err != nil {
+			return nil, fmt.Errorf("e.Extend() with type descriptor set %v failed: %w", fds, err)
+		}
+		return e, nil
+	}
+}
+
+// InputExpression is an interface for an expression which can be compiled into a CEL AST and return
+// an optional policy metadata map.
+type InputExpression interface {
+	// CreateAst creates a CEL AST from the input expression using the provided compiler.
+	CreateAst(Compiler) (*cel.Ast, map[string]any, error)
+}
+
+// CompiledExpression is an InputExpression which loads a CheckedExpr from a file.
+type CompiledExpression struct {
+	Path string
+}
+
+// CreateAst creates a CEL AST from a checked expression file.
+// The file must be in one of the following formats:
+// - .binarypb
+// - .textproto
+func (c *CompiledExpression) CreateAst(_ Compiler) (*cel.Ast, map[string]any, error) {
+	var expr exprpb.CheckedExpr
+	format := inferFileFormat(c.Path)
+	if format != BinaryProto && format != TextProto {
+		return nil, nil, fmt.Errorf("file extension must be .binarypb or .textproto: found %v", format)
+	}
+	if err := loadProtoFile(c.Path, format, &expr); err != nil {
+		return nil, nil, err
+	}
+	return cel.CheckedExprToAst(&expr), nil, nil
+}
+
+// FileExpression is an InputExpression which loads a CEL expression or policy from a file.
+type FileExpression struct {
+	Path string
+}
+
+// CreateAst creates a CEL AST from a file using the provided compiler:
+// - All policy metadata options as executed using the policy metadata map to extend the
+// environment.
+// - All policy compiler options are passed on to compile the parsed policy.
+//
+// The file must be in one of the following formats:
+// - .cel: CEL string expression
+// - .celpolicy: CEL policy
+func (f *FileExpression) CreateAst(compiler Compiler) (*cel.Ast, map[string]any, error) {
path: tools/compiler/compiler.go
user: login: TristonianJones
body: nit: casing on `Ast` should be `AST` (sorry for the bad precedent from CEL early on with `cel.Ast`)
created_at: 2025-03-24T22:17:11Z
author_association: COLLABORATOR

PR comments:
user: login: TristonianJones
created_at: 2025-03-20T20:42:59Z
body: /gcbrun
author_association: COLLABORATOR
user: login: TristonianJones
created_at: 2025-03-24T16:06:45Z
body: /gcbrun
author_association: COLLABORATOR
user: login: TristonianJones
created_at: 2025-03-24T22:15:23Z
body: @aakash070 looks like the build deps for policy need to be opened up for compiler to depend on them.
author_association: COLLABORATOR
user: login: aakash070
created_at: 2025-03-25T10:23:33Z
body: /gcbrun
author_association: COLLABORATOR