Add proto schemas for the IR plan and bundle manifest (#8775)

This adds two new proto schemas:

* v1/bundle/manifest.proto
* v1/ir/plan.proto

---------

Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
This commit is contained in:
Sebastian Spaink
2026-06-22 10:02:15 -05:00
committed by GitHub
parent 1a68282b0c
commit 0e6fe9caa2
17 changed files with 2973 additions and 3 deletions
+46 -2
View File
@@ -23,6 +23,7 @@ jobs:
docs: ${{ steps.changes.outputs.docs }} docs: ${{ steps.changes.outputs.docs }}
rego: ${{ steps.changes.outputs.rego }} rego: ${{ steps.changes.outputs.rego }}
yaml: ${{ steps.changes.outputs.yaml }} yaml: ${{ steps.changes.outputs.yaml }}
proto: ${{ steps.changes.outputs.proto }}
steps: steps:
- name: Check out repository code - name: Check out repository code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -46,6 +47,7 @@ jobs:
echo "docs=true" >> $GITHUB_OUTPUT echo "docs=true" >> $GITHUB_OUTPUT
echo "rego=true" >> $GITHUB_OUTPUT echo "rego=true" >> $GITHUB_OUTPUT
echo "yaml=true" >> $GITHUB_OUTPUT echo "yaml=true" >> $GITHUB_OUTPUT
echo "proto=true" >> $GITHUB_OUTPUT
# Get changed files: use git diff for merge_group, PR API for pull_request # Get changed files: use git diff for merge_group, PR API for pull_request
if [ -n "${{ github.event.merge_group.base_sha }}" ]; then if [ -n "${{ github.event.merge_group.base_sha }}" ]; then
@@ -54,13 +56,13 @@ jobs:
elif ! curl -s -o changed_files.json -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \ elif ! curl -s -o changed_files.json -H "Authorization: Bearer ${{ secrets.GITHUB_TOKEN }}" \
"https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"; then "https://api.github.com/repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/files"; then
echo "Error: Failed to fetch changed files from GitHub API" echo "Error: Failed to fetch changed files from GitHub API"
echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true)" echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true, proto=true)"
exit 0 exit 0
fi fi
if [ ! -s changed_files.json ]; then if [ ! -s changed_files.json ]; then
echo "Warning: No changed files found" echo "Warning: No changed files found"
echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true)" echo "Defaulting to running all checks (go=true, wasm=true, docs=true, rego=true, yaml=true, proto=true)"
exit 0 exit 0
fi fi
@@ -78,12 +80,14 @@ jobs:
docs_result=$(jq -r '.changes.docs // false' opa_result.json) docs_result=$(jq -r '.changes.docs // false' opa_result.json)
rego_result=$(jq -r '.changes.rego // false' opa_result.json) rego_result=$(jq -r '.changes.rego // false' opa_result.json)
yaml_result=$(jq -r '.changes.yaml // false' opa_result.json) yaml_result=$(jq -r '.changes.yaml // false' opa_result.json)
proto_result=$(jq -r '.changes.proto // false' opa_result.json)
echo "go=${go_result}" >> $GITHUB_OUTPUT echo "go=${go_result}" >> $GITHUB_OUTPUT
echo "wasm=${wasm_result}" >> $GITHUB_OUTPUT echo "wasm=${wasm_result}" >> $GITHUB_OUTPUT
echo "docs=${docs_result}" >> $GITHUB_OUTPUT echo "docs=${docs_result}" >> $GITHUB_OUTPUT
echo "rego=${rego_result}" >> $GITHUB_OUTPUT echo "rego=${rego_result}" >> $GITHUB_OUTPUT
echo "yaml=${yaml_result}" >> $GITHUB_OUTPUT echo "yaml=${yaml_result}" >> $GITHUB_OUTPUT
echo "proto=${proto_result}" >> $GITHUB_OUTPUT
echo "Final outputs:" echo "Final outputs:"
echo " go=${go_result}" echo " go=${go_result}"
@@ -91,6 +95,7 @@ jobs:
echo " docs=${docs_result}" echo " docs=${docs_result}"
echo " rego=${rego_result}" echo " rego=${rego_result}"
echo " yaml=${yaml_result}" echo " yaml=${yaml_result}"
echo " proto=${proto_result}"
# All jobs essentially re-create the `ci-release-test` make target, but are split # All jobs essentially re-create the `ci-release-test` make target, but are split
# up for parallel runners for faster PR feedback and a nicer UX. # up for parallel runners for faster PR feedback and a nicer UX.
@@ -264,6 +269,44 @@ jobs:
env: env:
YAML_LINT_FORMAT: github YAML_LINT_FORMAT: github
proto-check:
name: Proto Lint and Breaking
runs-on: ubuntu-24.04
needs: check-changes
if: ${{ needs.check-changes.outputs.proto == 'true' }}
steps:
- name: Check out code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install buf
uses: bufbuild/buf-setup-action@a47c93e0b1648d5651a065437926377d060baa99 # v1.50.0
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
- name: buf lint
run: buf lint
- name: buf breaking against base
# Bootstrap PR has no buf.yaml on the comparison ref — skip then.
env:
BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.merge_group.base_sha }}
run: |
set -euo pipefail
if [ -n "${BASE_SHA:-}" ]; then
git fetch --no-tags --depth=1 origin "$BASE_SHA"
ref="$BASE_SHA"
else
git fetch --no-tags --depth=1 origin main
ref="$(git rev-parse FETCH_HEAD)"
fi
if git show "${ref}:buf.yaml" >/dev/null 2>&1; then
buf breaking --against ".git#ref=${ref}"
else
echo "Comparison ref ${ref} has no buf.yaml — bootstrap PR, skipping breaking-change check."
fi
gh-actions-lint: gh-actions-lint:
name: Github Actions Lint name: Github Actions Lint
runs-on: ubuntu-24.04 runs-on: ubuntu-24.04
@@ -672,6 +715,7 @@ jobs:
go-lint, go-lint,
yaml-lint, yaml-lint,
gh-actions-lint, gh-actions-lint,
proto-check,
wasm, wasm,
check-generated, check-generated,
race-detector, race-detector,
+14
View File
@@ -0,0 +1,14 @@
# buf configuration for OPA's hand-authored .proto schemas.
version: v2
modules:
- path: v1/ir
- path: v1/bundle
lint:
use:
- MINIMAL
except:
- PACKAGE_DIRECTORY_MATCH
breaking:
use:
- FILE
+18
View File
@@ -63,6 +63,12 @@ go_root_files := [
"main.go", "main.go",
] ]
# Paths covered by buf.yaml.
proto_change_prefixes := [
"v1/bundle/",
"v1/ir/",
]
changes.docs if { changes.docs if {
some changed_file in input some changed_file in input
startswith(changed_file.filename, "docs/") startswith(changed_file.filename, "docs/")
@@ -81,6 +87,9 @@ changes.go if {
} else if { } else if {
some changed_file in input some changed_file in input
changed_file.filename in go_root_files changed_file.filename in go_root_files
} else if {
# .proto changes also run go-test (consistency tests live there).
changes.proto
} }
changes.wasm if { changes.wasm if {
@@ -104,6 +113,15 @@ changes.yaml if {
strings.any_suffix_match(changed_file.filename, yaml_change_suffixes) strings.any_suffix_match(changed_file.filename, yaml_change_suffixes)
} }
changes.proto if {
some changed_file in input
strings.any_prefix_match(changed_file.filename, proto_change_prefixes)
endswith(changed_file.filename, ".proto")
} else if {
some changed_file in input
changed_file.filename == "buf.yaml"
}
changes.bench contains "./v1/ast" if { changes.bench contains "./v1/ast" if {
some changed_file in input some changed_file in input
startswith(changed_file.filename, "v1/ast/") startswith(changed_file.filename, "v1/ast/")
+29
View File
@@ -55,6 +55,10 @@ example_bench_rego_changelist := [{"filename": "v1/rego/rego.go"}]
example_bench_no_match_changelist := [{"filename": "cmd/build.go"}] example_bench_no_match_changelist := [{"filename": "cmd/build.go"}]
example_proto_changelist := [{"filename": "v1/ir/plan.proto"}]
example_buf_yaml_changelist := [{"filename": "buf.yaml"}]
test_run_docs_check_expect if { test_run_docs_check_expect if {
pr_check.changes.docs with input as example_docs_changelist pr_check.changes.docs with input as example_docs_changelist
} }
@@ -119,3 +123,28 @@ test_bench_no_match if {
pr_check.changes.go with input as example_bench_no_match_changelist pr_check.changes.go with input as example_bench_no_match_changelist
pr_check.changes.bench == set() with input as example_bench_no_match_changelist pr_check.changes.bench == set() with input as example_bench_no_match_changelist
} }
test_run_proto_check_expect if {
pr_check.changes.proto with input as example_proto_changelist
}
test_run_proto_check_on_buf_yaml if {
pr_check.changes.proto with input as example_buf_yaml_changelist
}
test_run_no_proto_check_for_unrelated if {
not pr_check.changes.proto with input as example_go_changelist
}
# A .proto outside the configured buf.yaml modules must NOT trigger the
# proto-check job — buf would silently ignore it and we'd ship false-green.
test_run_no_proto_check_for_stray_proto if {
stray := [{"filename": "docs/examples/foo.proto"}]
not pr_check.changes.proto with input as stray
}
# A .proto-only PR must still trigger the go-test job, since the
# Go-vs-proto consistency tests live in v1/bundle / v1/ir.
test_proto_only_pr_triggers_go_test if {
pr_check.changes.go with input as example_proto_changelist
}
+12
View File
@@ -27,6 +27,18 @@ When contributing please consider the following pointers:
lightweight, and easily embedded. Vendoring may make features _easier_ to lightweight, and easily embedded. Vendoring may make features _easier_ to
implement however they come with their own cost for both OPA developers and implement however they come with their own cost for both OPA developers and
OPA users (e.g., vendoring conflicts, security, debugging, etc.) OPA users (e.g., vendoring conflicts, security, debugging, etc.)
- **Wire-format schemas:** The IR plan and bundle manifest each have two
published schemas: a JSON Schema generated from the Go types (drift-tested),
and a hand-authored `.proto` (Edition 2023) checked for consistency with the
Go types. When adding, renaming, or removing fields on `ir.Policy`,
`ir.Stmt` / `ir.Val` kinds, or `bundle.Manifest`, update the matching
`.proto` (`v1/ir/plan.proto` or `v1/bundle/manifest.proto`) in the same
change set. Field numbers and `oneof` case numbers in the `.proto` are
a permanent wire-format commitment — never reuse a number for a
different field, and mark removed fields with `reserved`. The consistency tests
(`e2e/proto/plan_test.go`, `e2e/proto/manifest_test.go`) catch
drift; `buf breaking` (in CI) catches wire-incompatible
changes.
- **AI Tooling**: You can use generative AI tooling to assist your work on OPA, - **AI Tooling**: You can use generative AI tooling to assist your work on OPA,
but please review the project's [AI Guidelines](#ai-guidelines) below before doing so to but please review the project's [AI Guidelines](#ai-guidelines) below before doing so to
help maintainers help you. help maintainers help you.
+2 -1
View File
@@ -6,6 +6,7 @@ go 1.25.7
replace github.com/open-policy-agent/opa => ../ replace github.com/open-policy-agent/opa => ../
require ( require (
github.com/bufbuild/protocompile v0.14.1
github.com/go-sql-driver/mysql v1.10.0 github.com/go-sql-driver/mysql v1.10.0
github.com/google/go-cmp v0.7.0 github.com/google/go-cmp v0.7.0
github.com/lib/pq v1.12.3 github.com/lib/pq v1.12.3
@@ -13,6 +14,7 @@ require (
github.com/open-policy-agent/opa v1.8.0 github.com/open-policy-agent/opa v1.8.0
github.com/rogpeppe/go-internal v1.15.0 github.com/rogpeppe/go-internal v1.15.0
github.com/testcontainers/testcontainers-go v0.42.0 github.com/testcontainers/testcontainers-go v0.42.0
google.golang.org/protobuf v1.36.11
modernc.org/sqlite v1.51.0 modernc.org/sqlite v1.51.0
) )
@@ -142,7 +144,6 @@ require (
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa // indirect
google.golang.org/grpc v1.81.1 // indirect google.golang.org/grpc v1.81.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gopkg.in/ini.v1 v1.67.2 // indirect gopkg.in/ini.v1 v1.67.2 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
+2
View File
@@ -30,6 +30,8 @@ github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig
github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/FBatYVw=
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ= github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0= github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
+288
View File
@@ -0,0 +1,288 @@
// Copyright 2026 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package proto
import (
"context"
"net/url"
"reflect"
"testing"
"github.com/bufbuild/protocompile"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/open-policy-agent/opa/e2e/proto/protoroundtrip"
"github.com/open-policy-agent/opa/e2e/proto/protoschemacheck"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/ast/location"
"github.com/open-policy-agent/opa/v1/bundle"
)
func TestManifestProtoConsistency(t *testing.T) {
protoschemacheck.Run(t, protoschemacheck.Spec{
ProtoPath: "manifest.proto",
ImportPaths: []string{"../../v1/bundle"},
Messages: []protoschemacheck.MessageSpec{
{
Name: "Manifest",
GoType: reflect.TypeOf(bundle.Manifest{}),
},
{
Name: "WasmResolver",
GoType: reflect.TypeOf(bundle.WasmResolver{}),
},
{
Name: "Annotations",
GoType: reflect.TypeOf(ast.Annotations{}),
// `comments` and `node` are unexported on the Go side
// and skipped by protoschemacheck's json-tag walk.
},
{
Name: "SchemaAnnotation",
GoType: reflect.TypeOf(ast.SchemaAnnotation{}),
// ast.Ref is []*Term in Go; modeled as canonical-form
// string in proto. *any Definition modeled as
// google.protobuf.Value. Neither admits a reflect-level
// type match against a proto string field.
OpaqueProtoFields: []string{"path", "schema"},
},
{
Name: "CompileAnnotation",
GoType: reflect.TypeOf(ast.CompileAnnotation{}),
// Same Ref → string mapping as SchemaAnnotation.
OpaqueProtoFields: []string{"unknowns", "mask_rule"},
},
{
Name: "AuthorAnnotation",
GoType: reflect.TypeOf(ast.AuthorAnnotation{}),
},
{
Name: "RelatedResourceAnnotation",
GoType: reflect.TypeOf(ast.RelatedResourceAnnotation{}),
// url.URL → string via its String() method.
OpaqueProtoFields: []string{"ref"},
},
{
Name: "Location",
GoType: reflect.TypeOf(location.Location{}),
// `Text`, `Offset`, `Tabs` are tagged `json:"-"` and
// skipped by the json-tag walk.
},
},
})
}
func TestManifestProtoRoundTrip(t *testing.T) {
codec := loadManifestCodec(t)
regoV1 := 1
tests := []struct {
note string
manifest bundle.Manifest
}{
{
note: "empty manifest",
manifest: bundle.Manifest{},
},
{
note: "revision and roots",
manifest: func() bundle.Manifest {
m := bundle.Manifest{Revision: "abc123"}
m.Init()
m.AddRoot("roles")
m.AddRoot("http/example/authz")
return m
}(),
},
{
note: "rego version and per-file overrides",
manifest: bundle.Manifest{
Revision: "abc123",
RegoVersion: &regoV1,
FileRegoVersions: map[string]int{
"/foo/*.rego": 0,
"/policy1.rego": 0,
},
},
},
{
note: "wasm resolvers",
manifest: bundle.Manifest{
Revision: "abc123",
WasmResolvers: []bundle.WasmResolver{
{Entrypoint: "http/example/authz/allow", Module: "/policy.wasm"},
},
},
},
{
note: "metadata",
manifest: bundle.Manifest{
Revision: "abc123",
Metadata: map[string]any{
"build_id": "ci-1234",
"tags": []any{"prod", "us-east-1"},
"nested": map[string]any{"k": 1.5, "ok": true},
},
},
},
{
note: "wasm resolver with annotations",
manifest: bundle.Manifest{
Revision: "abc123",
WasmResolvers: []bundle.WasmResolver{{
Entrypoint: "http/example/authz/allow",
Module: "/policy.wasm",
Annotations: []*ast.Annotations{{
Scope: "rule",
Title: "Allow rule",
Description: "Permits authorized requests",
Entrypoint: true,
Organizations: []string{"acme"},
Authors: []*ast.AuthorAnnotation{
{Name: "Alice", Email: "alice@example.com"},
},
Custom: map[string]any{"sla": "p99-100ms"},
Labels: map[string]any{"team": "platform"},
}},
}},
},
},
{
note: "annotations with refs, schemas, compile, location",
manifest: bundle.Manifest{
Revision: "abc123",
WasmResolvers: []bundle.WasmResolver{{
Entrypoint: "http/example/authz/allow",
Module: "/policy.wasm",
Annotations: []*ast.Annotations{{
Scope: "package",
RelatedResources: []*ast.RelatedResourceAnnotation{
{Ref: mustParseURL("https://example.com/docs"), Description: "design doc"},
},
Schemas: []*ast.SchemaAnnotation{
{
Path: ast.MustParseRef("input.user"),
Schema: ast.MustParseRef("schema.user"),
Definition: ptrAny(map[string]any{"type": "object"}),
},
},
Compile: &ast.CompileAnnotation{
Unknowns: []ast.Ref{ast.MustParseRef("input.x"), ast.MustParseRef("input.y")},
MaskRule: ast.MustParseRef("data.policy.mask"),
},
Location: &location.Location{File: "policy.rego", Row: 3, Col: 1},
}},
}},
},
},
}
opts := []cmp.Option{
cmpopts.EquateEmpty(),
// ast.Annotations carries unexported `comments` and `node` that
// aren't part of the wire shape; ignore them in the diff.
cmpopts.IgnoreUnexported(ast.Annotations{}),
}
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
bs, err := codec.Encode(&tc.manifest)
if err != nil {
t.Fatalf("encode: %v", err)
}
var got bundle.Manifest
if err := codec.Decode(bs, &got); err != nil {
t.Fatalf("decode: %v", err)
}
// EquateEmpty handles nil-vs-empty slice/map, but not the
// *[]string pointer case on Manifest.Roots.
normalizeRoots(&tc.manifest)
normalizeRoots(&got)
if diff := cmp.Diff(tc.manifest, got, opts...); diff != "" {
t.Fatalf("round-trip mismatch (-want +got):\n%s", diff)
}
})
}
}
func normalizeRoots(m *bundle.Manifest) {
if m.Roots != nil && len(*m.Roots) == 0 {
m.Roots = nil
}
}
func mustParseURL(s string) url.URL {
u, err := url.Parse(s)
if err != nil {
panic(err)
}
return *u
}
func ptrAny(v any) *any {
return &v
}
func loadManifestCodec(t *testing.T) *protoroundtrip.Codec {
t.Helper()
c := protocompile.Compiler{
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: []string{"../../v1/bundle"},
}),
}
files, err := c.Compile(context.Background(), "manifest.proto")
if err != nil {
t.Fatalf("compile manifest.proto: %v", err)
}
codec := protoroundtrip.NewCodec(files[0])
codec.RegisterRoot(reflect.TypeOf(bundle.Manifest{}), "Manifest")
codec.RegisterRoot(reflect.TypeOf(bundle.WasmResolver{}), "WasmResolver")
codec.RegisterRoot(reflect.TypeOf(ast.Annotations{}), "Annotations")
codec.RegisterRoot(reflect.TypeOf(ast.SchemaAnnotation{}), "SchemaAnnotation")
codec.RegisterRoot(reflect.TypeOf(ast.CompileAnnotation{}), "CompileAnnotation")
codec.RegisterRoot(reflect.TypeOf(ast.AuthorAnnotation{}), "AuthorAnnotation")
codec.RegisterRoot(reflect.TypeOf(ast.RelatedResourceAnnotation{}), "RelatedResourceAnnotation")
codec.RegisterRoot(reflect.TypeOf(location.Location{}), "Location")
// ast.Ref ↔ canonical dotted form. Empty refs round-trip through "".
codec.RegisterScalarConverter(reflect.TypeOf(ast.Ref{}), protoroundtrip.ScalarConverter{
Encode: func(rv reflect.Value) (string, error) {
ref := rv.Interface().(ast.Ref)
if len(ref) == 0 {
return "", nil
}
return ref.String(), nil
},
Decode: func(s string) (reflect.Value, error) {
if s == "" {
return reflect.ValueOf(ast.Ref(nil)), nil
}
ref, err := ast.ParseRef(s)
if err != nil {
return reflect.Value{}, err
}
return reflect.ValueOf(ref), nil
},
})
// url.URL ↔ its String() form. url.Parse("") returns the zero URL
// without error, so empty strings round-trip naturally.
codec.RegisterScalarConverter(reflect.TypeOf(url.URL{}), protoroundtrip.ScalarConverter{
Encode: func(rv reflect.Value) (string, error) {
u := rv.Interface().(url.URL)
return u.String(), nil
},
Decode: func(s string) (reflect.Value, error) {
u, err := url.Parse(s)
if err != nil {
return reflect.Value{}, err
}
return reflect.ValueOf(*u), nil
},
})
return codec
}
+408
View File
@@ -0,0 +1,408 @@
// Copyright 2026 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package proto
import (
"context"
"errors"
"fmt"
"reflect"
"strings"
"testing"
"unicode"
"github.com/bufbuild/protocompile"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
"github.com/open-policy-agent/opa/e2e/proto/protoroundtrip"
"github.com/open-policy-agent/opa/e2e/proto/protoschemacheck"
"github.com/open-policy-agent/opa/internal/planner"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/ir"
"github.com/open-policy-agent/opa/v1/test/cases"
)
func TestPlanProtoConsistency(t *testing.T) {
locationT := reflect.TypeOf(ir.Location{})
stmtMsg := func(name string, goType reflect.Type, opts ...func(*protoschemacheck.MessageSpec)) protoschemacheck.MessageSpec {
m := protoschemacheck.MessageSpec{
Name: name,
GoType: goType,
SkipEmbeddedTypes: []reflect.Type{locationT},
}
for _, opt := range opts {
opt(&m)
}
return m
}
withFieldOverride := func(overrides map[string]string) func(*protoschemacheck.MessageSpec) {
return func(m *protoschemacheck.MessageSpec) {
m.FieldNameOverride = overrides
}
}
stmtBodies := []protoschemacheck.MessageSpec{
stmtMsg("ArrayAppendStmt", reflect.TypeOf(ir.ArrayAppendStmt{})),
stmtMsg("AssignIntStmt", reflect.TypeOf(ir.AssignIntStmt{})),
stmtMsg("AssignVarOnceStmt", reflect.TypeOf(ir.AssignVarOnceStmt{})),
stmtMsg("AssignVarStmt", reflect.TypeOf(ir.AssignVarStmt{})),
stmtMsg("BlockStmt", reflect.TypeOf(ir.BlockStmt{})),
stmtMsg("BreakStmt", reflect.TypeOf(ir.BreakStmt{})),
stmtMsg("CallDynamicStmt", reflect.TypeOf(ir.CallDynamicStmt{})),
// CallStmt.Func → proto "function" (avoids reserved-keyword
// collision; mirrors Func.return → result).
stmtMsg("CallStmt", reflect.TypeOf(ir.CallStmt{}),
withFieldOverride(map[string]string{"func": "function"})),
stmtMsg("DotStmt", reflect.TypeOf(ir.DotStmt{})),
stmtMsg("EqualStmt", reflect.TypeOf(ir.EqualStmt{})),
stmtMsg("IsArrayStmt", reflect.TypeOf(ir.IsArrayStmt{})),
stmtMsg("IsDefinedStmt", reflect.TypeOf(ir.IsDefinedStmt{})),
stmtMsg("IsObjectStmt", reflect.TypeOf(ir.IsObjectStmt{})),
stmtMsg("IsSetStmt", reflect.TypeOf(ir.IsSetStmt{})),
stmtMsg("IsUndefinedStmt", reflect.TypeOf(ir.IsUndefinedStmt{})),
stmtMsg("LenStmt", reflect.TypeOf(ir.LenStmt{})),
stmtMsg("MakeArrayStmt", reflect.TypeOf(ir.MakeArrayStmt{})),
stmtMsg("MakeNullStmt", reflect.TypeOf(ir.MakeNullStmt{})),
stmtMsg("MakeNumberIntStmt", reflect.TypeOf(ir.MakeNumberIntStmt{})),
// MakeNumberRefStmt's Go field has no json tag, so its derived
// JSON name is "Index"; the proto's canonical field is "index".
// The runtime's MarshalJSON also emits a deprecated "Index" alias
// — JSON-only contract, not modeled in the proto.
stmtMsg("MakeNumberRefStmt", reflect.TypeOf(ir.MakeNumberRefStmt{}),
withFieldOverride(map[string]string{"Index": "index"})),
stmtMsg("MakeObjectStmt", reflect.TypeOf(ir.MakeObjectStmt{})),
stmtMsg("MakeSetStmt", reflect.TypeOf(ir.MakeSetStmt{})),
stmtMsg("NopStmt", reflect.TypeOf(ir.NopStmt{})),
stmtMsg("NotEqualStmt", reflect.TypeOf(ir.NotEqualStmt{})),
stmtMsg("NotStmt", reflect.TypeOf(ir.NotStmt{})),
stmtMsg("ObjectInsertOnceStmt", reflect.TypeOf(ir.ObjectInsertOnceStmt{})),
stmtMsg("ObjectInsertStmt", reflect.TypeOf(ir.ObjectInsertStmt{})),
stmtMsg("ObjectMergeStmt", reflect.TypeOf(ir.ObjectMergeStmt{})),
stmtMsg("ResetLocalStmt", reflect.TypeOf(ir.ResetLocalStmt{})),
stmtMsg("ResultSetAddStmt", reflect.TypeOf(ir.ResultSetAddStmt{})),
stmtMsg("ReturnLocalStmt", reflect.TypeOf(ir.ReturnLocalStmt{})),
stmtMsg("ScanStmt", reflect.TypeOf(ir.ScanStmt{})),
stmtMsg("SetAddStmt", reflect.TypeOf(ir.SetAddStmt{})),
stmtMsg("WithStmt", reflect.TypeOf(ir.WithStmt{})),
}
structural := []protoschemacheck.MessageSpec{
{Name: "Policy", GoType: reflect.TypeOf(ir.Policy{})},
{Name: "Static", GoType: reflect.TypeOf(ir.Static{})},
{Name: "Plans", GoType: reflect.TypeOf(ir.Plans{})},
{Name: "Funcs", GoType: reflect.TypeOf(ir.Funcs{})},
// BuiltinFunc.Decl has no proto counterpart by design — see
// the comment on `BuiltinFunc` in plan.proto.
{
Name: "BuiltinFunc",
GoType: reflect.TypeOf(ir.BuiltinFunc{}),
SkipGoFields: []string{"Decl"},
},
{Name: "Plan", GoType: reflect.TypeOf(ir.Plan{})},
// Func.Return → proto "result" (avoids reserved-keyword collision).
{
Name: "Func",
GoType: reflect.TypeOf(ir.Func{}),
FieldNameOverride: map[string]string{"return": "result"},
},
{Name: "Block", GoType: reflect.TypeOf(ir.Block{})},
{Name: "StringConst", GoType: reflect.TypeOf(ir.StringConst{})},
{Name: "Operand", GoType: reflect.TypeOf(ir.Operand{})},
// Stmt envelope: file/col/row come from ir.Location; oneof
// validated via the OneofSpec below.
{Name: "Stmt", GoType: locationT},
}
messages := make([]protoschemacheck.MessageSpec, 0, len(structural)+len(stmtBodies))
messages = append(messages, structural...)
messages = append(messages, stmtBodies...)
stmtDiscToCase := map[string]string{}
stmtDiscToGoType := map[string]reflect.Type{}
for k, v := range ir.StmtKinds() {
stmtDiscToCase[k] = camelToSnake(k)
stmtDiscToGoType[k] = reflect.TypeOf(v)
}
valDiscToCase := map[string]string{}
valDiscToGoType := map[string]reflect.Type{}
for k, v := range ir.ValKinds() {
valDiscToCase[k] = camelToSnake(k)
valDiscToGoType[k] = reflect.TypeOf(v)
}
protoschemacheck.Run(t, protoschemacheck.Spec{
ProtoPath: "plan.proto",
ImportPaths: []string{"../../v1/ir"},
Messages: messages,
OpaqueMessages: []string{"Val"},
Oneofs: []protoschemacheck.OneofSpec{
{
MessageName: "Stmt",
OneofName: "kind",
DiscriminatorToCase: stmtDiscToCase,
DiscriminatorToGoType: stmtDiscToGoType,
},
{
MessageName: "Val",
OneofName: "kind",
DiscriminatorToCase: valDiscToCase,
DiscriminatorToGoType: valDiscToGoType,
},
},
})
}
func TestPlanProtoRoundTrip(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
codec := loadPlanCodec(t)
tests := []struct {
note string
module string
query string
}{
{
note: "scalar comparison",
module: `package test
p if { input.foo == 7 }`,
query: "data.test.p = true",
},
{
note: "every / scan",
module: `package test
p if { every i in input.foo { i > 0 } }`,
query: "data.test.p = true",
},
{
note: "composite construction and negation",
module: `package test
p contains x if {
some x in input.xs
not x == "skip"
}
q := {k: v | some k, v in input.m}`,
query: "data.test.p = x",
},
{
note: "with override",
module: `package test
import data.lib
p if { lib.allow with input as {"u": "alice"} }
`,
query: "data.test.p = true",
},
}
opts := roundTripCmpOpts()
for _, tc := range tests {
t.Run(tc.note, func(t *testing.T) {
plan := compileToPolicy(t, tc.module, tc.query)
bs, err := codec.Encode(plan)
if err != nil {
t.Fatalf("encode: %v", err)
}
var got ir.Policy
if err := codec.Decode(bs, &got); err != nil {
t.Fatalf("decode: %v", err)
}
if diff := cmp.Diff(plan, &got, opts...); diff != "" {
t.Fatalf("round-trip mismatch (-want +got):\n%s", diff)
}
})
}
}
// TestPlanProtoRoundTripYAMLSuite runs the round-trip codec against every
// case the planner accepts under v1/test/cases/testdata/v1.
func TestPlanProtoRoundTripYAMLSuite(t *testing.T) {
if testing.Short() {
t.Skip("too slow for testing.Short")
}
codec := loadPlanCodec(t)
corpus, err := cases.Load("../../v1/test/cases/testdata/v1")
if err != nil {
t.Fatalf("load YAML cases: %v", err)
}
if len(corpus.Cases) == 0 {
t.Fatalf("no YAML cases loaded; did the test data move?")
}
opts := roundTripCmpOpts()
for _, tc := range corpus.Cases {
t.Run(tc.Note, func(t *testing.T) {
plan, err := planYAMLCase(tc)
if err != nil {
t.Skipf("planner did not accept case (not drift): %v", err)
return
}
bs, err := codec.Encode(plan)
if err != nil {
t.Fatalf("encode: %v", err)
}
var got ir.Policy
if err := codec.Decode(bs, &got); err != nil {
t.Fatalf("decode: %v", err)
}
if diff := cmp.Diff(plan, &got, opts...); diff != "" {
t.Fatalf("round-trip mismatch (-want +got):\n%s", diff)
}
})
}
}
// roundTripCmpOpts returns the cmp.Options that ignore wire-format-irrelevant
// fields (debug-only Location internals, BuiltinFunc.Decl absent from proto)
// and equate nil with empty for slices and maps.
func roundTripCmpOpts() []cmp.Option {
return []cmp.Option{
cmpopts.IgnoreUnexported(ir.Location{}),
cmpopts.IgnoreFields(ir.BuiltinFunc{}, "Decl"),
cmpopts.EquateEmpty(),
}
}
func planYAMLCase(tc cases.TestCase) (*ir.Policy, error) {
if tc.Query == "" {
return nil, errors.New("case has no query")
}
if len(tc.Modules) == 0 {
return nil, errors.New("case has no modules")
}
moduleMap := map[string]string{}
for i, m := range tc.Modules {
moduleMap[fmt.Sprintf("module%d.rego", i)] = m
}
c, err := ast.CompileModules(moduleMap)
if err != nil {
return nil, fmt.Errorf("compile modules: %w", err)
}
mods := make([]*ast.Module, 0, len(c.Modules))
for _, m := range c.Modules {
mods = append(mods, m)
}
body, err := ast.ParseBody(tc.Query)
if err != nil {
return nil, fmt.Errorf("parse query: %w", err)
}
return planner.New().
WithQueries([]planner.QuerySet{{Name: "main", Queries: []ast.Body{body}}}).
WithModules(mods).
WithBuiltinDecls(ast.BuiltinMap).
Plan()
}
func compileToPolicy(t *testing.T, module, query string) *ir.Policy {
t.Helper()
c, err := ast.CompileModules(map[string]string{"test.rego": module})
if err != nil {
t.Fatalf("compile modules: %v", err)
}
modules := make([]*ast.Module, 0, len(c.Modules))
for _, m := range c.Modules {
modules = append(modules, m)
}
plan, err := planner.New().
WithQueries([]planner.QuerySet{{
Name: "main",
Queries: []ast.Body{ast.MustParseBody(query)},
}}).
WithModules(modules).
WithBuiltinDecls(ast.BuiltinMap).
Plan()
if err != nil {
t.Fatalf("plan: %v", err)
}
return plan
}
func loadPlanCodec(t *testing.T) *protoroundtrip.Codec {
t.Helper()
c := protocompile.Compiler{
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: []string{"../../v1/ir"},
}),
}
files, err := c.Compile(context.Background(), "plan.proto")
if err != nil {
t.Fatalf("compile plan.proto: %v", err)
}
codec := protoroundtrip.NewCodec(files[0])
codec.RegisterRoot(reflect.TypeOf(ir.Policy{}), "Policy")
codec.RegisterRoot(reflect.TypeOf(ir.Static{}), "Static")
codec.RegisterRoot(reflect.TypeOf(ir.Plans{}), "Plans")
codec.RegisterRoot(reflect.TypeOf(ir.Funcs{}), "Funcs")
codec.RegisterRoot(reflect.TypeOf(ir.BuiltinFunc{}), "BuiltinFunc")
codec.RegisterRoot(reflect.TypeOf(ir.Plan{}), "Plan")
codec.RegisterRoot(reflect.TypeOf(ir.Func{}), "Func")
codec.RegisterRoot(reflect.TypeOf(ir.Block{}), "Block")
codec.RegisterRoot(reflect.TypeOf(ir.StringConst{}), "StringConst")
codec.RegisterRoot(reflect.TypeOf(ir.Operand{}), "Operand")
codec.SkipGoFields(reflect.TypeOf(ir.BuiltinFunc{}), "Decl")
codec.SetFieldNameOverride(reflect.TypeOf(ir.Func{}), map[string]string{
"return": "result",
})
codec.SetFieldNameOverride(reflect.TypeOf(ir.CallStmt{}), map[string]string{
"func": "function",
})
codec.SetFieldNameOverride(reflect.TypeOf(ir.MakeNumberRefStmt{}), map[string]string{
"Index": "index",
})
locationT := reflect.TypeOf(ir.Location{})
for kind, body := range ir.StmtKinds() {
bodyT := reflect.TypeOf(body)
for bodyT.Kind() == reflect.Pointer {
bodyT = bodyT.Elem()
}
codec.RegisterEnvelope(bodyT, protoroundtrip.EnvelopeSpec{
Envelope: "Stmt",
Oneof: "kind",
Case: camelToSnake(kind),
PromotedEmbed: locationT,
})
codec.SkipEmbedded(bodyT, locationT)
}
for kind, val := range ir.ValKinds() {
valT := reflect.TypeOf(val)
for valT.Kind() == reflect.Pointer {
valT = valT.Elem()
}
codec.RegisterEnvelope(valT, protoroundtrip.EnvelopeSpec{
Envelope: "Val",
Oneof: "kind",
Case: kind,
})
}
return codec
}
// camelToSnake converts CamelCase to snake_case, handling acronym
// boundaries: "AssignVarStmt" → "assign_var_stmt", "HTTPSendStmt" →
// "http_send_stmt", "string_index" → "string_index" (unchanged).
func camelToSnake(s string) string {
rs := []rune(s)
var b strings.Builder
b.Grow(len(rs))
for i, r := range rs {
if i > 0 && unicode.IsUpper(r) {
prevLower := unicode.IsLower(rs[i-1])
nextLower := i+1 < len(rs) && unicode.IsLower(rs[i+1])
if prevLower || nextLower {
b.WriteByte('_')
}
}
b.WriteRune(unicode.ToLower(r))
}
return b.String()
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,595 @@
// Copyright 2026 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
// Package protoschemacheck verifies that a hand-authored .proto file
// stays consistent with the Go types it mirrors. Powers the proto
// consistency tests for v1/bundle/manifest.proto and v1/ir/plan.proto.
package protoschemacheck
import (
"context"
"errors"
"fmt"
"maps"
"reflect"
"slices"
"sort"
"strings"
"testing"
"time"
"github.com/bufbuild/protocompile"
"google.golang.org/protobuf/reflect/protoreflect"
)
// loadTimeout caps how long we'll wait for protocompile to parse the
// proto file so a pathological input fails fast.
const loadTimeout = 30 * time.Second
// Spec describes a consistency check between a .proto file and a set of
// Go types.
type Spec struct {
// ProtoPath is the .proto file relative to the test's working dir.
ProtoPath string
// ImportPaths are extra search dirs for imports; "." is added by default.
ImportPaths []string
// Messages enumerates proto messages to validate. Every message
// declared in the .proto (including nested) must appear here or in
// OpaqueMessages.
Messages []MessageSpec
// OpaqueMessages names polymorphic-union envelope messages with no
// direct Go counterpart (content is checked via Oneofs). Every entry
// must pair with an OneofSpec.
OpaqueMessages []string
// Oneofs enumerates polymorphic-union checks.
Oneofs []OneofSpec
}
// MessageSpec asserts that proto message Name matches GoType.
type MessageSpec struct {
Name string
// GoType is the Go struct compared against the proto message.
// Anonymous (embedded) struct fields are flattened recursively
// (mirroring encoding/json) unless listed in SkipEmbeddedTypes.
GoType reflect.Type
// SkipEmbeddedTypes opts out specific embedded types from flattening
// (their fields are validated on a different proto message — see
// the IR plan schema where Stmt bodies embed Location but the proto
// promotes Location's fields onto the Stmt envelope).
SkipEmbeddedTypes []reflect.Type
// FieldNameOverride remaps Go-side JSON name → proto field name
// where the default mapping doesn't apply (e.g. ir.MakeNumberRefStmt's
// Index Go field maps to proto field "index").
FieldNameOverride map[string]string
// OpaqueProtoFields names proto fields whose Go counterpart isn't
// reflectable into proto (e.g. types.Function fields modeled as
// google.protobuf.Struct). The check requires the Go field to exist
// with a structural type — opaque is "no type check feasible", not
// "no Go field expected".
OpaqueProtoFields []string
// SkipGoFields names Go fields (by Go field name, not JSON name)
// that have no proto counterpart by design — e.g. ir.BuiltinFunc.Decl
// is intentionally absent from the proto because consumers consult
// their own registry for builtin signatures.
SkipGoFields []string
}
// OneofSpec asserts that the named oneof on MessageName has exactly the
// listed cases with matching Go-side types.
type OneofSpec struct {
MessageName string
OneofName string
// DiscriminatorToCase maps each JSON-discriminator (runtime kind
// name) to the proto oneof case name.
DiscriminatorToCase map[string]string
// DiscriminatorToGoType maps each discriminator to the Go type
// implementing the union. Must have the same key set as
// DiscriminatorToCase. Struct types check by name; scalar types
// check by reflect.Kind.
DiscriminatorToGoType map[string]reflect.Type
}
// Run executes the spec, reporting every drift via t.Errorf.
func Run(t *testing.T, spec Spec) {
t.Helper()
if err := validateSpec(spec); err != nil {
t.Fatalf("invalid spec: %v", err)
}
file := loadProto(t, spec.ProtoPath, spec.ImportPaths)
declared := map[string]protoreflect.MessageDescriptor{}
collectDeclared(file.Messages(), declared)
covered := map[string]bool{}
oneofParents := map[string]bool{}
for _, o := range spec.Oneofs {
oneofParents[o.MessageName] = true
}
// Collect, per message, the names of oneofs covered by an OneofSpec.
// Fields belonging to those oneofs are validated by checkOneof and
// must NOT be reported as orphans by checkMessage's field walk.
coveredOneofs := map[string]map[string]bool{}
for _, o := range spec.Oneofs {
if coveredOneofs[o.MessageName] == nil {
coveredOneofs[o.MessageName] = map[string]bool{}
}
coveredOneofs[o.MessageName][o.OneofName] = true
}
for _, m := range spec.Messages {
covered[m.Name] = true
checkMessage(t, declared, m, coveredOneofs[m.Name])
}
for _, name := range spec.OpaqueMessages {
covered[name] = true
if _, ok := declared[name]; !ok {
t.Errorf("opaque message %q listed in spec but not present in %s", name, spec.ProtoPath)
}
if !oneofParents[name] {
t.Errorf("opaque message %q has no matching OneofSpec; OpaqueMessages is for polymorphic envelopes only — add an OneofSpec or move the message into Messages", name)
}
if hasMessageSpec(spec, name) {
t.Errorf("message %q is listed in both Messages and OpaqueMessages — pick one", name)
}
}
for name := range declared {
if !covered[name] {
t.Errorf("proto message %q is declared in %s but not covered by the spec; add a MessageSpec or list it in OpaqueMessages", name, spec.ProtoPath)
}
}
for _, o := range spec.Oneofs {
checkOneof(t, declared, o)
}
}
func validateSpec(s Spec) error {
if s.ProtoPath == "" {
return errors.New("ProtoPath is required")
}
for _, o := range s.Oneofs {
// EqualFunc with an always-true comparator collapses to a key-set
// equality check, ignoring the differing value types.
eq := maps.EqualFunc(o.DiscriminatorToCase, o.DiscriminatorToGoType,
func(string, reflect.Type) bool { return true })
if !eq {
return fmt.Errorf("oneof %s.%s: DiscriminatorToCase and DiscriminatorToGoType have different key sets",
o.MessageName, o.OneofName)
}
}
return nil
}
func hasMessageSpec(s Spec, name string) bool {
for _, m := range s.Messages {
if m.Name == name {
return true
}
}
return false
}
func collectDeclared(msgs protoreflect.MessageDescriptors, out map[string]protoreflect.MessageDescriptor) {
for i := range msgs.Len() {
m := msgs.Get(i)
// Skip synthetic map-entry messages — proto generates them
// implicitly for map<K,V> fields and they have no user-visible
// counterpart in either the proto source or the Go types.
if m.IsMapEntry() {
continue
}
out[string(m.Name())] = m
collectDeclared(m.Messages(), out)
}
}
func loadProto(t *testing.T, protoPath string, importPaths []string) protoreflect.FileDescriptor {
t.Helper()
paths := append([]string{"."}, importPaths...)
c := protocompile.Compiler{
Resolver: protocompile.WithStandardImports(&protocompile.SourceResolver{
ImportPaths: paths,
}),
}
ctx, cancel := context.WithTimeout(context.Background(), loadTimeout)
defer cancel()
files, err := c.Compile(ctx, protoPath)
if err != nil {
t.Fatalf("compile %s: %v", protoPath, err)
}
if len(files) != 1 {
t.Fatalf("compile %s: expected 1 file, got %d", protoPath, len(files))
}
return files[0]
}
func checkMessage(t *testing.T, declared map[string]protoreflect.MessageDescriptor, m MessageSpec, coveredOneofs map[string]bool) {
t.Helper()
msg, ok := declared[m.Name]
if !ok {
t.Errorf("proto message %q not declared in any input file", m.Name)
return
}
override := m.FieldNameOverride
skipEmbedded := map[reflect.Type]bool{}
for _, et := range m.SkipEmbeddedTypes {
skipEmbedded[et] = true
}
skipGo := map[string]bool{}
for _, n := range m.SkipGoFields {
skipGo[n] = true
}
type expected struct {
protoName string
goType reflect.Type
goName string
}
var want []expected
visited := map[reflect.Type]bool{}
var collect func(rt reflect.Type, qualifier string)
collect = func(rt reflect.Type, qualifier string) {
if rt.Kind() != reflect.Struct {
return
}
if visited[rt] {
return
}
visited[rt] = true
for i := range rt.NumField() {
f := rt.Field(i)
if !f.IsExported() {
continue
}
if skipGo[f.Name] {
continue
}
if f.Anonymous {
ft := f.Type
for ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct {
if skipEmbedded[ft] {
continue
}
collect(ft, qualifier)
continue
}
}
name, ok := jsonFieldName(f)
if !ok {
continue
}
if remap, has := override[name]; has {
name = remap
}
want = append(want, expected{
protoName: name,
goType: f.Type,
goName: qualifier + "." + f.Name,
})
}
}
collect(m.GoType, m.GoType.Name())
// Validate SkipGoFields entries point at real Go fields.
for _, n := range m.SkipGoFields {
if !goFieldExists(m.GoType, n) {
t.Errorf("%s: SkipGoFields entry %q does not match any Go field", m.Name, n)
}
}
opaque := map[string]bool{}
for _, n := range m.OpaqueProtoFields {
opaque[n] = true
if msg.Fields().ByName(protoreflect.Name(n)) == nil {
t.Errorf("%s: OpaqueProtoFields entry %q does not match any proto field on this message", m.Name, n)
}
}
// Validate FieldNameOverride entries point at real Go fields.
for goJSONName := range override {
if !goJSONNameExists(m.GoType, goJSONName) {
t.Errorf("%s: FieldNameOverride key %q does not match any Go field's JSON name", m.Name, goJSONName)
}
}
seen := map[string]bool{}
for _, w := range want {
seen[w.protoName] = true
pf := msg.Fields().ByName(protoreflect.Name(w.protoName))
if pf == nil {
t.Errorf("%s: Go field %s maps to proto field %q but %s has no such field; add it with the next available field number", m.Name, w.goName, w.protoName, m.Name)
continue
}
if opaque[w.protoName] {
// Opaque means "no type check feasible" — but the Go side
// must still be a structural type. A scalar Go field paired
// with an opaque proto field is almost certainly a drift bug.
switch unwrapPointer(w.goType).Kind() {
case reflect.Bool,
reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64,
reflect.String:
t.Errorf("%s.%s (Go field %s): proto field is opaque but Go type %s is scalar; opaque is for unreflectable structural types only", m.Name, w.protoName, w.goName, w.goType)
}
continue
}
if err := checkFieldType(w.goType, pf); err != nil {
t.Errorf("%s.%s (Go field %s): %v", m.Name, w.protoName, w.goName, err)
}
}
// Find proto fields that have no Go counterpart.
for i := range msg.Fields().Len() {
pf := msg.Fields().Get(i)
name := string(pf.Name())
if seen[name] {
continue
}
if opaque[name] {
continue
}
// Fields belonging to a oneof handled by an OneofSpec are
// validated there, not here. Skip them so the orphan check
// doesn't double-fire.
if oo := pf.ContainingOneof(); oo != nil && coveredOneofs[string(oo.Name())] {
continue
}
t.Errorf("%s: proto field %q (number %d) has no corresponding Go field; either remove it (and add `reserved %d`) or add a matching Go field", m.Name, name, pf.Number(), pf.Number())
}
}
func goJSONNameExists(rt reflect.Type, target string) bool {
if rt.Kind() != reflect.Struct {
return false
}
for i := range rt.NumField() {
f := rt.Field(i)
if !f.IsExported() {
continue
}
if f.Anonymous {
ft := f.Type
for ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct && goJSONNameExists(ft, target) {
return true
}
continue
}
name, ok := jsonFieldName(f)
if !ok {
continue
}
if name == target {
return true
}
}
return false
}
func goFieldExists(rt reflect.Type, target string) bool {
if rt.Kind() != reflect.Struct {
return false
}
for i := range rt.NumField() {
f := rt.Field(i)
if !f.IsExported() {
continue
}
if f.Name == target {
return true
}
if f.Anonymous {
ft := f.Type
for ft.Kind() == reflect.Pointer {
ft = ft.Elem()
}
if ft.Kind() == reflect.Struct && goFieldExists(ft, target) {
return true
}
}
}
return false
}
func checkOneof(t *testing.T, declared map[string]protoreflect.MessageDescriptor, o OneofSpec) {
t.Helper()
msg, ok := declared[o.MessageName]
if !ok {
t.Errorf("oneof check: proto message %q not declared", o.MessageName)
return
}
oneof := msg.Oneofs().ByName(protoreflect.Name(o.OneofName))
if oneof == nil {
t.Errorf("oneof check: %s has no oneof named %q", o.MessageName, o.OneofName)
return
}
caseFields := map[string]protoreflect.FieldDescriptor{}
for i := range oneof.Fields().Len() {
f := oneof.Fields().Get(i)
caseFields[string(f.Name())] = f
}
discriminators := slices.Sorted(maps.Keys(o.DiscriminatorToCase))
expectedCases := map[string]bool{}
for _, d := range discriminators {
caseName := o.DiscriminatorToCase[d]
expectedCases[caseName] = true
f, ok := caseFields[caseName]
if !ok {
t.Errorf("%s.%s: discriminator %q maps to oneof case %q, but no such case exists", o.MessageName, o.OneofName, d, caseName)
continue
}
goType := o.DiscriminatorToGoType[d]
if goType == nil {
t.Errorf("%s.%s.%s: discriminator %q has nil Go type — every discriminator must declare a concrete type so the proto kind can be checked", o.MessageName, o.OneofName, caseName, d)
continue
}
gt := unwrapPointer(goType)
gk := gt.Kind()
if gk == reflect.Struct {
if f.Kind() != protoreflect.MessageKind {
t.Errorf("%s.%s.%s: discriminator %q expected message-typed oneof case but proto field is a scalar of kind %s", o.MessageName, o.OneofName, caseName, d, f.Kind())
continue
}
want := gt.Name()
got := string(f.Message().Name())
if got != want {
t.Errorf("%s.%s.%s: discriminator %q references Go type %s but proto case wraps message %s", o.MessageName, o.OneofName, caseName, d, want, got)
}
continue
}
// Scalar Go kind — validate proto kind compatibility.
if err := checkScalarKind(gt, f); err != nil {
t.Errorf("%s.%s.%s: discriminator %q: %v", o.MessageName, o.OneofName, caseName, d, err)
}
}
// Sort orphan-case names for deterministic error ordering.
var orphanCases []string
for caseName := range caseFields {
if !expectedCases[caseName] {
orphanCases = append(orphanCases, caseName)
}
}
sort.Strings(orphanCases)
for _, caseName := range orphanCases {
f := caseFields[caseName]
t.Errorf("%s.%s: proto case %q (number %d) has no corresponding discriminator in DiscriminatorToCase; either remove it (and `reserved %d` the number) or extend the spec", o.MessageName, o.OneofName, caseName, f.Number(), f.Number())
}
}
// jsonFieldName returns the JSON name of f and whether it should be included
// (false for `json:"-"` fields).
func jsonFieldName(f reflect.StructField) (string, bool) {
tag := f.Tag.Get("json")
if tag == "-" {
return "", false
}
if tag == "" {
return f.Name, true
}
name, _, _ := strings.Cut(tag, ",")
if name == "" {
name = f.Name
}
return name, true
}
// checkFieldType verifies that the Go field type goType is compatible with
// the proto field descriptor pf.
func checkFieldType(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
goType = unwrapPointer(goType)
if pf.IsList() {
if goType.Kind() != reflect.Slice && goType.Kind() != reflect.Array {
return fmt.Errorf("proto field is repeated but Go type is %s", goType.Kind())
}
elem := unwrapPointer(goType.Elem())
return checkScalarOrMessage(elem, pf)
}
if pf.IsMap() {
if goType.Kind() != reflect.Map {
return fmt.Errorf("proto field is a map but Go type is %s", goType.Kind())
}
if goType.Key().Kind() != reflect.String {
return fmt.Errorf("proto field is a map but Go map key is %s, want string", goType.Key().Kind())
}
valField := pf.MapValue()
valType := unwrapPointer(goType.Elem())
return checkScalarOrMessage(valType, valField)
}
return checkScalarOrMessage(goType, pf)
}
func checkScalarOrMessage(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
goType = unwrapPointer(goType)
if pf.Kind() == protoreflect.MessageKind || pf.Kind() == protoreflect.GroupKind {
// Accept any Go type for message-typed proto fields. The outer
// message check verifies field-by-field shape; the
// Stmt/Val/Operand/Block plumbing relies on this looseness.
return nil
}
return checkScalarKind(goType, pf)
}
// checkScalarKind verifies a Go type is compatible with a scalar proto kind.
// For integer kinds we enforce that the Go bit-width fits in the proto
// bit-width (so Go int64 cannot be silently mapped to proto int32). The
// platform-dependent `int`/`uint` kinds are treated as 32-bit minimum and
// accepted against either width — practical for OPA's bounded indices.
func checkScalarKind(goType reflect.Type, pf protoreflect.FieldDescriptor) error {
switch pf.Kind() {
case protoreflect.BoolKind:
if goType.Kind() != reflect.Bool {
return fmt.Errorf("proto Kind=bool but Go type is %s", goType.Kind())
}
case protoreflect.StringKind:
if goType.Kind() != reflect.String {
return fmt.Errorf("proto Kind=string but Go type is %s", goType.Kind())
}
case protoreflect.BytesKind:
if !(goType.Kind() == reflect.Slice && goType.Elem().Kind() == reflect.Uint8) {
return fmt.Errorf("proto Kind=bytes but Go type is %s", goType.Kind())
}
case protoreflect.Int32Kind, protoreflect.Sint32Kind, protoreflect.Sfixed32Kind,
protoreflect.Int64Kind, protoreflect.Sint64Kind, protoreflect.Sfixed64Kind:
if !signedIntFits(goType.Kind(), pf.Kind()) {
return fmt.Errorf("proto Kind=%s but Go type is %s (Go int64 cannot be safely narrowed)", pf.Kind(), goType.Kind())
}
case protoreflect.Uint32Kind, protoreflect.Fixed32Kind,
protoreflect.Uint64Kind, protoreflect.Fixed64Kind:
if !unsignedIntFits(goType.Kind(), pf.Kind()) {
return fmt.Errorf("proto Kind=%s but Go type is %s (Go uint64 cannot be safely narrowed)", pf.Kind(), goType.Kind())
}
case protoreflect.FloatKind, protoreflect.DoubleKind:
if goType.Kind() != reflect.Float32 && goType.Kind() != reflect.Float64 {
return fmt.Errorf("proto Kind=%s but Go type is %s", pf.Kind(), goType.Kind())
}
default:
return fmt.Errorf("unsupported proto Kind=%s", pf.Kind())
}
return nil
}
// signedIntFits reports whether a Go signed integer kind fits in a proto
// signed integer kind. Go's platform-dependent `int` is treated as
// at-most 32-bit (lenient — accepted against either int32 or int64 proto
// kinds). Fixed-width kinds (int8/int16/int32/int64) are checked strictly.
func signedIntFits(goKind reflect.Kind, pfKind protoreflect.Kind) bool {
proto64 := pfKind == protoreflect.Int64Kind || pfKind == protoreflect.Sint64Kind || pfKind == protoreflect.Sfixed64Kind
switch goKind {
case reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int:
return true // fits any signed proto int kind
case reflect.Int64:
return proto64
}
return false
}
// unsignedIntFits is the unsigned counterpart of signedIntFits.
func unsignedIntFits(goKind reflect.Kind, pfKind protoreflect.Kind) bool {
proto64 := pfKind == protoreflect.Uint64Kind || pfKind == protoreflect.Fixed64Kind
switch goKind {
case reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint:
return true
case reflect.Uint64:
return proto64
}
return false
}
func unwrapPointer(t reflect.Type) reflect.Type {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t
}
+4
View File
@@ -234,6 +234,10 @@ func (a *Annotations) MarshalJSON() ([]byte, error) {
data["schemas"] = a.Schemas data["schemas"] = a.Schemas
} }
if a.Compile != nil {
data["compile"] = a.Compile
}
if len(a.Custom) > 0 { if len(a.Custom) > 0 {
data["custom"] = a.Custom data["custom"] = a.Custom
} }
+44
View File
@@ -965,6 +965,50 @@ func TestAnnotations_MarshalJSON(t *testing.T) {
} }
} }
func TestAnnotations_MarshalJSON_Compile(t *testing.T) {
// Regression: Annotations.MarshalJSON used to silently drop the
// `Compile` field even though the struct tag is `compile,omitempty`.
// Default-reflection unmarshal still reads `compile`, so the round-trip
// was asymmetric until this was fixed.
a := &Annotations{
Scope: "rule",
Compile: &CompileAnnotation{
Unknowns: []Ref{MustParseRef("input.x"), MustParseRef("input.y")},
MaskRule: MustParseRef("data.policy.mask"),
},
}
bs, err := json.Marshal(a)
if err != nil {
t.Fatalf("marshal: %v", err)
}
var got map[string]any
if err := json.Unmarshal(bs, &got); err != nil {
t.Fatalf("unmarshal: %v", err)
}
compile, ok := got["compile"].(map[string]any)
if !ok {
t.Fatalf("expected `compile` key in marshaled output, got: %s", bs)
}
if _, ok := compile["unknowns"].([]any); !ok {
t.Errorf("expected compile.unknowns to be a JSON array, got: %v", compile["unknowns"])
}
if _, ok := compile["mask_rule"].([]any); !ok {
t.Errorf("expected compile.mask_rule to be a JSON array (Ref), got: %v", compile["mask_rule"])
}
// nil Compile should not emit the key (`omitempty` semantics).
a.Compile = nil
bs, err = json.Marshal(a)
if err != nil {
t.Fatalf("marshal nil compile: %v", err)
}
if strings.Contains(string(bs), "compile") {
t.Errorf("expected nil Compile to be omitted, got: %s", bs)
}
}
func TestAnnotationsRef_MarshalJSON(t *testing.T) { func TestAnnotationsRef_MarshalJSON(t *testing.T) {
testCases := map[string]struct { testCases := map[string]struct {
+3
View File
@@ -130,6 +130,9 @@ func NewFile(name, hash, alg string) FileInfo {
// Manifest represents the manifest from a bundle. The manifest may contain // Manifest represents the manifest from a bundle. The manifest may contain
// metadata such as the bundle revision. // metadata such as the bundle revision.
//
// Schema mirror: manifest.proto + manifest_proto_test.go. Update both when
// adding/renaming/removing fields; never reuse a proto field number.
type Manifest struct { type Manifest struct {
Revision string `json:"revision"` Revision string `json:"revision"`
Roots *[]string `json:"roots,omitempty"` Roots *[]string `json:"roots,omitempty"`
+112
View File
@@ -0,0 +1,112 @@
// Copyright 2026 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
edition = "2023";
package opa.bundle.v1;
import "google/protobuf/struct.proto";
option go_package = "github.com/open-policy-agent/opa/v1/bundle/v1pb";
option java_multiple_files = true;
// Manifest mirrors `bundle.Manifest` in v1/bundle/bundle.go.
message Manifest {
// Bundle revision string.
string revision = 1;
// Root paths the bundle owns.
repeated string roots = 2;
// Wasm resolvers attached to the bundle. JSON key is `wasm`.
repeated WasmResolver wasm = 3;
// Global Rego version for the bundle. Currently 0 (RegoV0) or 1 (RegoV1).
int32 rego_version = 4;
// Per-file Rego version overrides keyed by file path.
map<string, int32> file_rego_versions = 5;
// Free-form metadata object. Modeled as `Struct` because the Go field
// is `map[string]any`.
google.protobuf.Struct metadata = 6;
}
// WasmResolver mirrors `bundle.WasmResolver` in v1/bundle/bundle.go.
message WasmResolver {
// Entrypoint policy ref this resolver targets.
string entrypoint = 1;
// Path to the wasm module within the bundle.
string module = 2;
// Rego annotations attached to the entrypoint.
repeated Annotations annotations = 3;
}
// Annotations mirrors `ast.Annotations` in v1/ast/annotations.go.
message Annotations {
string scope = 1;
string title = 2;
bool entrypoint = 3;
string description = 4;
repeated string organizations = 5;
repeated RelatedResourceAnnotation related_resources = 6;
repeated AuthorAnnotation authors = 7;
repeated SchemaAnnotation schemas = 8;
CompileAnnotation compile = 9;
// `custom` and `labels` are `map[string]any` in Go — genuinely
// free-form, so `Struct` is the right model.
google.protobuf.Struct custom = 10;
google.protobuf.Struct labels = 11;
Location location = 12;
}
// SchemaAnnotation mirrors `ast.SchemaAnnotation`. Path/Schema are
// `ast.Ref` in Go (a list of terms); the wire form is the canonical
// dotted ref string (e.g. `data.foo.bar`). Modeling the term tree
// faithfully would pull most of the AST into this schema and isn't
// worth the cost for annotations.
message SchemaAnnotation {
string path = 1;
string schema = 2;
// `*any` on the Go side — a parsed JSON Schema document or any
// JSON value. `Value` (not `Struct`) because the top level may be
// a scalar, list, or null, not just an object.
google.protobuf.Value definition = 3;
}
// CompileAnnotation mirrors `ast.CompileAnnotation`. Refs are the
// canonical dotted form; see SchemaAnnotation for the trade-off.
message CompileAnnotation {
repeated string unknowns = 1;
string mask_rule = 2;
}
// AuthorAnnotation mirrors `ast.AuthorAnnotation`.
message AuthorAnnotation {
string name = 1;
string email = 2;
}
// RelatedResourceAnnotation mirrors `ast.RelatedResourceAnnotation`.
// `Ref` is a `url.URL` in Go, serialized to its `String()` form.
message RelatedResourceAnnotation {
string ref = 1;
string description = 2;
}
// Location mirrors `ast.Location` (= `location.Location`). Only the
// File/Row/Col triple is wire-relevant; `Text`, `Offset`, and `Tabs`
// are tagged `json:"-"` and intentionally absent.
//
// Distinct from `ir.Location`, which is promoted onto the `Stmt`
// envelope in plan.proto. The two are independent Go types.
message Location {
string file = 1;
int32 row = 2;
int32 col = 3;
}
+4
View File
@@ -6,6 +6,10 @@
// //
// The IR specifies an imperative execution model for Rego policies similar to a // The IR specifies an imperative execution model for Rego policies similar to a
// query plan in traditional databases. // query plan in traditional databases.
//
// Schema mirror: plan.proto + plan_proto_test.go. Update both when
// adding/renaming/removing fields or Stmt/Val kinds; never reuse a proto
// field number or oneof case.
package ir package ir
import ( import (
+364
View File
@@ -0,0 +1,364 @@
// Copyright 2026 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
edition = "2023";
package opa.ir.v1;
option go_package = "github.com/open-policy-agent/opa/v1/ir/v1pb";
option java_multiple_files = true;
// Policy mirrors `ir.Policy` in v1/ir/ir.go.
message Policy {
Static static = 1;
Plans plans = 2;
Funcs funcs = 3;
}
// Static mirrors `ir.Static` in v1/ir/ir.go.
message Static {
repeated StringConst strings = 1;
repeated BuiltinFunc builtin_funcs = 2;
repeated StringConst files = 3;
}
// BuiltinFunc mirrors `ir.BuiltinFunc` in v1/ir/ir.go.
//
// `ir.BuiltinFunc.Decl` (the function's `types.Function` signature) is
// intentionally not modeled. Consumers that execute plans need their
// own builtin registry to dispatch host-language implementations, and
// that registry is the source of truth for signatures — bundling
// `Decl` here would be redundant and prone to drift.
message BuiltinFunc {
string name = 1;
}
// Plans mirrors `ir.Plans` in v1/ir/ir.go.
message Plans {
repeated Plan plans = 1;
}
// Funcs mirrors `ir.Funcs` in v1/ir/ir.go.
message Funcs {
repeated Func funcs = 1;
}
// Func mirrors `ir.Func` in v1/ir/ir.go.
//
// Each parameter and the return slot are local-variable indices; see
// `ir.Local` in v1/ir/ir.go.
message Func {
string name = 1;
repeated int32 params = 2;
// The local that holds the function's return value. Renamed from
// `return` (the Go field is `Func.Return Local`, JSON-tagged `return`)
// to avoid colliding with a reserved keyword in many target languages
// when this proto is fed to protoc plugins. The JSON wire form
// continues to use `return`; only the proto-side identifier differs.
int32 result = 3;
repeated Block blocks = 4;
repeated string path = 5;
}
// Plan mirrors `ir.Plan` in v1/ir/ir.go.
message Plan {
string name = 1;
repeated Block blocks = 2;
}
// Block mirrors `ir.Block` in v1/ir/ir.go.
message Block {
repeated Stmt stmts = 1;
}
// StringConst mirrors `ir.StringConst` in v1/ir/ir.go.
message StringConst {
string value = 1;
}
// Operand mirrors `ir.Operand` in v1/ir/ir.go. The `value` field is a
// polymorphic `Val` union; see the `Val` message below.
message Operand {
Val value = 1;
}
// Val mirrors the `ir.Val` interface in v1/ir/ir.go. Each oneof case
// corresponds to a concrete `Val` implementation; the case names match
// the JSON discriminator strings emitted by `*Operand.MarshalJSON`.
//
// Case-number assignments are a stability commitment. New cases must
// be added with the next unused number; existing numbers must never
// be repurposed.
message Val {
oneof kind {
bool bool = 1;
int32 local = 2;
int32 string_index = 3;
}
}
// Stmt mirrors the `ir.Stmt` interface in v1/ir/ir.go. Every Stmt carries
// the source-location triple (file, col, row) on this envelope; the body
// messages below describe only the kind-specific payload.
//
// On the Go side, `ir.Location` is embedded into every concrete Stmt
// implementation, so `encoding/json` flattens File/Col/Row into the
// emitted JSON body. The proto promotes those fields to the envelope
// because that's both more idiomatic protobuf and lets every body
// message start its own field numbering at 1.
//
// Case-number assignments (437) are a stability commitment. Field
// numbers 13 are reserved for the location triple. New cases must be
// added with the next unused number; existing numbers must never be
// repurposed.
message Stmt {
int32 file = 1;
int32 col = 2;
int32 row = 3;
oneof kind {
ArrayAppendStmt array_append_stmt = 4;
AssignIntStmt assign_int_stmt = 5;
AssignVarOnceStmt assign_var_once_stmt = 6;
AssignVarStmt assign_var_stmt = 7;
BlockStmt block_stmt = 8;
BreakStmt break_stmt = 9;
CallDynamicStmt call_dynamic_stmt = 10;
CallStmt call_stmt = 11;
DotStmt dot_stmt = 12;
EqualStmt equal_stmt = 13;
IsArrayStmt is_array_stmt = 14;
IsDefinedStmt is_defined_stmt = 15;
IsObjectStmt is_object_stmt = 16;
IsSetStmt is_set_stmt = 17;
IsUndefinedStmt is_undefined_stmt = 18;
LenStmt len_stmt = 19;
MakeArrayStmt make_array_stmt = 20;
MakeNullStmt make_null_stmt = 21;
MakeNumberIntStmt make_number_int_stmt = 22;
MakeNumberRefStmt make_number_ref_stmt = 23;
MakeObjectStmt make_object_stmt = 24;
MakeSetStmt make_set_stmt = 25;
NopStmt nop_stmt = 26;
NotEqualStmt not_equal_stmt = 27;
NotStmt not_stmt = 28;
ObjectInsertOnceStmt object_insert_once_stmt = 29;
ObjectInsertStmt object_insert_stmt = 30;
ObjectMergeStmt object_merge_stmt = 31;
ResetLocalStmt reset_local_stmt = 32;
ResultSetAddStmt result_set_add_stmt = 33;
ReturnLocalStmt return_local_stmt = 34;
ScanStmt scan_stmt = 35;
SetAddStmt set_add_stmt = 36;
WithStmt with_stmt = 37;
}
}
// Stmt body messages start their own field numbering at 1; the
// source-location triple lives on the parent `Stmt` envelope.
// ArrayAppendStmt mirrors `ir.ArrayAppendStmt` in v1/ir/ir.go.
message ArrayAppendStmt {
Operand value = 1;
int32 array = 2;
}
// AssignIntStmt mirrors `ir.AssignIntStmt` in v1/ir/ir.go.
message AssignIntStmt {
int64 value = 1;
int32 target = 2;
}
// AssignVarOnceStmt mirrors `ir.AssignVarOnceStmt` in v1/ir/ir.go.
message AssignVarOnceStmt {
Operand source = 1;
int32 target = 2;
}
// AssignVarStmt mirrors `ir.AssignVarStmt` in v1/ir/ir.go.
message AssignVarStmt {
Operand source = 1;
int32 target = 2;
}
// BlockStmt mirrors `ir.BlockStmt` in v1/ir/ir.go.
message BlockStmt {
repeated Block blocks = 1;
}
// BreakStmt mirrors `ir.BreakStmt` in v1/ir/ir.go.
message BreakStmt {
uint32 index = 1;
}
// CallDynamicStmt mirrors `ir.CallDynamicStmt` in v1/ir/ir.go.
message CallDynamicStmt {
repeated int32 args = 1;
int32 result = 2;
repeated Operand path = 3;
}
// CallStmt mirrors `ir.CallStmt` in v1/ir/ir.go.
message CallStmt {
// Renamed from `func` (Go field `CallStmt.Func`, JSON-tagged `func`)
// to avoid colliding with reserved keywords in target languages,
// mirroring the Func.return → result rename above.
string function = 1;
repeated Operand args = 2;
int32 result = 3;
}
// DotStmt mirrors `ir.DotStmt` in v1/ir/ir.go.
message DotStmt {
Operand source = 1;
Operand key = 2;
int32 target = 3;
}
// EqualStmt mirrors `ir.EqualStmt` in v1/ir/ir.go.
message EqualStmt {
Operand a = 1;
Operand b = 2;
}
// IsArrayStmt mirrors `ir.IsArrayStmt` in v1/ir/ir.go.
message IsArrayStmt {
Operand source = 1;
}
// IsDefinedStmt mirrors `ir.IsDefinedStmt` in v1/ir/ir.go.
message IsDefinedStmt {
int32 source = 1;
}
// IsObjectStmt mirrors `ir.IsObjectStmt` in v1/ir/ir.go.
message IsObjectStmt {
Operand source = 1;
}
// IsSetStmt mirrors `ir.IsSetStmt` in v1/ir/ir.go.
message IsSetStmt {
Operand source = 1;
}
// IsUndefinedStmt mirrors `ir.IsUndefinedStmt` in v1/ir/ir.go.
message IsUndefinedStmt {
int32 source = 1;
}
// LenStmt mirrors `ir.LenStmt` in v1/ir/ir.go.
message LenStmt {
Operand source = 1;
int32 target = 2;
}
// MakeArrayStmt mirrors `ir.MakeArrayStmt` in v1/ir/ir.go.
message MakeArrayStmt {
int32 capacity = 1;
int32 target = 2;
}
// MakeNullStmt mirrors `ir.MakeNullStmt` in v1/ir/ir.go.
message MakeNullStmt {
int32 target = 1;
}
// MakeNumberIntStmt mirrors `ir.MakeNumberIntStmt` in v1/ir/ir.go.
message MakeNumberIntStmt {
int64 value = 1;
int32 target = 2;
}
// MakeNumberRefStmt mirrors `ir.MakeNumberRefStmt` in v1/ir/ir.go.
//
// The Go field is named `Index` (no `json` tag). The historical JSON
// shape emitted both `index` and `Index`; the canonical key going
// forward is `index` and the deprecated `Index` alias will be removed
// in a future major release. This proto models only the canonical
// `index` field.
message MakeNumberRefStmt {
int32 index = 1;
int32 target = 2;
}
// MakeObjectStmt mirrors `ir.MakeObjectStmt` in v1/ir/ir.go.
message MakeObjectStmt {
int32 target = 1;
}
// MakeSetStmt mirrors `ir.MakeSetStmt` in v1/ir/ir.go.
message MakeSetStmt {
int32 target = 1;
}
// NopStmt mirrors `ir.NopStmt` in v1/ir/ir.go.
message NopStmt {}
// NotEqualStmt mirrors `ir.NotEqualStmt` in v1/ir/ir.go.
message NotEqualStmt {
Operand a = 1;
Operand b = 2;
}
// NotStmt mirrors `ir.NotStmt` in v1/ir/ir.go.
message NotStmt {
Block block = 1;
}
// ObjectInsertOnceStmt mirrors `ir.ObjectInsertOnceStmt` in v1/ir/ir.go.
message ObjectInsertOnceStmt {
Operand key = 1;
Operand value = 2;
int32 object = 3;
}
// ObjectInsertStmt mirrors `ir.ObjectInsertStmt` in v1/ir/ir.go.
message ObjectInsertStmt {
Operand key = 1;
Operand value = 2;
int32 object = 3;
}
// ObjectMergeStmt mirrors `ir.ObjectMergeStmt` in v1/ir/ir.go.
message ObjectMergeStmt {
int32 a = 1;
int32 b = 2;
int32 target = 3;
}
// ResetLocalStmt mirrors `ir.ResetLocalStmt` in v1/ir/ir.go.
message ResetLocalStmt {
int32 target = 1;
}
// ResultSetAddStmt mirrors `ir.ResultSetAddStmt` in v1/ir/ir.go.
message ResultSetAddStmt {
int32 value = 1;
}
// ReturnLocalStmt mirrors `ir.ReturnLocalStmt` in v1/ir/ir.go.
message ReturnLocalStmt {
int32 source = 1;
}
// ScanStmt mirrors `ir.ScanStmt` in v1/ir/ir.go.
message ScanStmt {
int32 source = 1;
int32 key = 2;
int32 value = 3;
Block block = 4;
}
// SetAddStmt mirrors `ir.SetAddStmt` in v1/ir/ir.go.
message SetAddStmt {
Operand value = 1;
int32 set = 2;
}
// WithStmt mirrors `ir.WithStmt` in v1/ir/ir.go.
message WithStmt {
int32 local = 1;
repeated int32 path = 2;
Operand value = 3;
Block block = 4;
}