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 }}
rego: ${{ steps.changes.outputs.rego }}
yaml: ${{ steps.changes.outputs.yaml }}
proto: ${{ steps.changes.outputs.proto }}
steps:
- name: Check out repository code
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
@@ -46,6 +47,7 @@ jobs:
echo "docs=true" >> $GITHUB_OUTPUT
echo "rego=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
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 }}" \
"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 "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
fi
if [ ! -s changed_files.json ]; then
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
fi
@@ -78,12 +80,14 @@ jobs:
docs_result=$(jq -r '.changes.docs // false' opa_result.json)
rego_result=$(jq -r '.changes.rego // 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 "wasm=${wasm_result}" >> $GITHUB_OUTPUT
echo "docs=${docs_result}" >> $GITHUB_OUTPUT
echo "rego=${rego_result}" >> $GITHUB_OUTPUT
echo "yaml=${yaml_result}" >> $GITHUB_OUTPUT
echo "proto=${proto_result}" >> $GITHUB_OUTPUT
echo "Final outputs:"
echo " go=${go_result}"
@@ -91,6 +95,7 @@ jobs:
echo " docs=${docs_result}"
echo " rego=${rego_result}"
echo " yaml=${yaml_result}"
echo " proto=${proto_result}"
# 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.
@@ -264,6 +269,44 @@ jobs:
env:
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:
name: Github Actions Lint
runs-on: ubuntu-24.04
@@ -672,6 +715,7 @@ jobs:
go-lint,
yaml-lint,
gh-actions-lint,
proto-check,
wasm,
check-generated,
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",
]
# Paths covered by buf.yaml.
proto_change_prefixes := [
"v1/bundle/",
"v1/ir/",
]
changes.docs if {
some changed_file in input
startswith(changed_file.filename, "docs/")
@@ -81,6 +87,9 @@ changes.go if {
} else if {
some changed_file in input
changed_file.filename in go_root_files
} else if {
# .proto changes also run go-test (consistency tests live there).
changes.proto
}
changes.wasm if {
@@ -104,6 +113,15 @@ changes.yaml if {
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 {
some changed_file in input
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_proto_changelist := [{"filename": "v1/ir/plan.proto"}]
example_buf_yaml_changelist := [{"filename": "buf.yaml"}]
test_run_docs_check_expect if {
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.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
implement however they come with their own cost for both OPA developers and
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,
but please review the project's [AI Guidelines](#ai-guidelines) below before doing so to
help maintainers help you.
+2 -1
View File
@@ -6,6 +6,7 @@ go 1.25.7
replace github.com/open-policy-agent/opa => ../
require (
github.com/bufbuild/protocompile v0.14.1
github.com/go-sql-driver/mysql v1.10.0
github.com/google/go-cmp v0.7.0
github.com/lib/pq v1.12.3
@@ -13,6 +14,7 @@ require (
github.com/open-policy-agent/opa v1.8.0
github.com/rogpeppe/go-internal v1.15.0
github.com/testcontainers/testcontainers-go v0.42.0
google.golang.org/protobuf v1.36.11
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/rpc v0.0.0-20260526163538-3dc84a4a5aaa // 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/natefinch/lumberjack.v2 v2.2.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/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
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/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
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
}
if a.Compile != nil {
data["compile"] = a.Compile
}
if len(a.Custom) > 0 {
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) {
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
// 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 {
Revision string `json:"revision"`
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
// 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
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;
}