Skip to content

Evaluate dynamic Rego defaults at runtime - #863

Open
Maksym (maksym-mishchenko) wants to merge 1 commit into
microsoft:mainfrom
maksym-mishchenko:runtime-default-fallbacks
Open

Maksym (maksym-mishchenko) wants to merge 1 commit into
microsoft:mainfrom
maksym-mishchenko:runtime-default-fallbacks

Conversation

@maksym-mishchenko

Copy link
Copy Markdown
Contributor

Summary

I compile nonconstant Rego defaults into RVM fallback code instead of evaluating them during compilation. Defaults can now depend on input, data and other rules, including HostAwait suspension and resume.

  • Run the fallback only after ordinary definitions and else branches remain Undefined. Defined false, null and empty values still suppress it; surfaced strict errors do not enter it.
  • Preserve indexed-default object merging, populated base-data members and empty package namespaces without exposing default functions as policy data.
  • Make Suspendable virtual-document traversal retain the correct paths, register windows and child results across awaits, errors and fresh execution.

Compatibility

Programs requiring the new default metadata use binary version 7 and a versioned JSON envelope. Other programs retain binary version 6 and flat JSON. The new reader accepts legacy artifacts; old readers reject the new formats. Previously compiled defaults retain their historical frozen values until the policy source is recompiled.

Public RuleInfo gains default_entry_point and default_is_indexed. Constructors initialize them; downstream struct literals need the new fields. Migration details are documented in docs/rvm/vm-runtime.md.

This PR targets main independently of #862 and does not use its v2 HostAwait APIs. The only shared product-source overlap is the identical seven-line native string-result helper required to build either change on current main. Numeric package versions, releases and deployment remain outside this PR.

Verification

The final namespace-preservation tree passed RVM tests 206/206 standalone and 209/209 with the separate HostAwait change; FFI HostAwait tests passed 18/18 and 20/20 respectively. Managed tests passed 113/113 with the loaded native DLL path and hash verified against the fresh release build. The populated-base-data regression also passed through the native API in both execution modes.

Broader local cargo xtask ci-debug --frozen and embedded thumbv7m-none-eabi no_std gates passed before the last two-file namespace-preservation correction. Their receipts remain historical, not relabeled as new executions. The final tree also passed the full standard cargo xtask clippy matrix: all-targets/all-features, no-default-features and the FFI/Java/Python/WASM binding manifests.

The published commit passed normal commit and pre-push hooks without bypass, including build, formatting, Clippy, documentation tests, embedded no_std checks and OPA conformance (2875 passed, 0 failed).

Regression coverage includes namespace/function/resume-error corrections, both execution modes, strict and non-strict behavior, allocator-budget failure and reuse. Actual compatibility checks cover historical v6 artifacts on the new reader and v7 binary/JSON rejected by the old e39 reader; the namespace correction does not change those formats.

Validation limitations remain explicit:

  • Standalone cargo check --no-default-features --features rvm,no_std fails on current main and this change: 62 baseline diagnostics versus 61, with no new diagnostics. This is not a passing standalone no-arc check.
  • The NuGet packaging/sample-app route fails on wildcard package target paths. The direct managed test route passed with the exact native artifact; packaging is not certified by that result.
  • An additional minimal-feature cargo clippy --features rvm --test mod -- -D warnings invocation fails on missing_const_for_fn in the unchanged no-allocator helper in src/rvm/vm/machine.rs. This is separate from the repository's standard Clippy matrix; no pass or lint suppression is claimed for that extra configuration.

Compile valid dynamic defaults for runtime fallback while preserving indexed/default-function semantics, namespace/base-data projection, and suspendable virtual lookup.

BREAKING CHANGE: RuleInfo struct literals require default_entry_point and default_is_indexed. Runtime/indexed-default artifacts require a version-7-capable reader.

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Unresolved namespace compilation, dependency-cycle detection, and cross-mode fallback inconsistencies affect policy correctness.

4 open issues
What changed in this PR

This PR moves nonconstant Rego defaults from compile-time evaluation to runtime fallback execution in RVM, including HostAwait suspension and resume, while preserving legacy artifact compatibility.

Changes:

  • Compile runtime defaults and support indexed-default object merging.
  • Add resumable virtual-document traversal and execution-error cleanup.
  • Introduce version 7 artifact metadata, migration documentation, and Rust/C# regression coverage.
File Description
tests/​rvm/​rego/​mod.rs Adds runtime-default, namespace, resume, and reuse regressions.
tests/​rvm/​rego/​cases/​registered_host_await.yaml Tests registered HostAwait calls inside defaults.
tests/​rvm/​rego/​cases/​imports.yaml Enables imported dynamic-default function coverage.
tests/​rvm/​rego/​cases/​host_await.yaml Tests raw HostAwait fallback behavior.
tests/​rvm/​rego/​cases/​default_rules.yaml Covers dynamic defaults and suppression conditions.
tests/​rvm/​fixtures/​e39-mark-frozen-dynamic-default-v6.json Adds a historical frozen-default fixture.
tests/​rvm/​compiler.rs Tests artifact compatibility, validation, and compiler diagnostics.
src/​rvm/​vm/​virtual_data.rs Adds resumable lookups and empty-namespace preservation.
src/​rvm/​vm/​state.rs Cleans up failed executions and validates default metadata.
src/​rvm/​vm/​rules.rs Executes fallbacks and merges indexed defaults.
src/​rvm/​vm/​execution.rs Advances virtual lookups and handles strict-error precedence.
src/​rvm/​vm/​execution_model.rs Defines fallback phases and virtual-lookup frames.
src/​rvm/​tests/​vm.rs Initializes new metadata in test rules.
src/​rvm/​program/​types.rs Extends public rule metadata and constructors.
src/​rvm/​program/​serialization/​json.rs Adds versioned JSON while retaining legacy output.
src/​rvm/​program/​serialization/​binary.rs Supports version 7 and legacy version 6 layouts.
src/​rvm/​program/​rule_tree.rs Registers package namespaces without document rules.
src/​rvm/​program/​core.rs Validates default metadata and advances the format version.
src/​languages/​rego/​compiler/​rules.rs Compiles default expressions and checks dependency ancestry.
src/​languages/​rego/​compiler/​program.rs Attaches compiled-default metadata to programs.
src/​languages/​rego/​compiler/​mod.rs Tracks literal defaults and fallback entry points.
src/​languages/​rego/​compiler/​function_calls.rs Resolves qualified and imported default functions.
src/​languages/​rego/​compiler/​core.rs Recognizes default functions and their arity.
src/​interpreter.rs Shares default validation and removes compile-time evaluation support.
docs/​rvm/​vm-runtime.md Documents runtime semantics and artifact migration.
CHANGELOG.md Records default support and compatibility changes.
bindings/​ffi/​src/​rvm.rs Adds the native string-result conversion helper.
bindings/​ffi/​CHANGELOG.md Documents runtime-default support for native consumers.
bindings/​csharp/​Regorus.Tests/​RvmProgramTests.cs Tests runtime defaults and HostAwait through managed bindings.

Review effort: Balanced

Comment on lines +145 to +147
let current_entry =
WorklistEntry::new(rule_path.to_string(), self.current_call_stack.clone());
if current_entry.would_create_recursion(index) {
.default_rules
.get(rule_path)
.is_some_and(|defaults| defaults.iter().any(|(_, index)| index.is_some()));
rule_info.set_default_is_indexed(has_indexed_default);
Comment on lines +45 to +47
if self.policy.inner.rule_paths.contains(rule_path)
&& self.policy.inner.default_rules.contains_key(rule_path)
{
Comment thread src/rvm/vm/rules.rs
Comment on lines +1136 to +1139
if matches!(frame_data.phase, RuleFramePhase::ExecutingDefault) {
if let Some(result) = self.registers.get_mut(usize::from(frame_data.result_reg)) {
*result = Value::Undefined;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OPA-compatibility clarification: please resolve this mismatch by propagating the strict error, not by suppressing it in both modes.

package probe
default value := [result | result := 1 / input.divisor]

With input {"divisor":0} and strict builtin errors enabled, OPA must report an evaluation error. In the new run-to-completion fallback at src/rvm/vm/rules.rs:300-319, jump_to(default_entry_point) returns InvalidDivision, but Err(_) => {} discards it because only fatal resource errors are propagated. The result remains Undefined rather than surfacing the strict error. With strict errors disabled, the correct result is the empty comprehension [].

Please propagate strict builtin errors through fallback execution and assert an error explicitly in both VM modes. The new dynamic_default_errors_match_ordinary_expression_in_both_builtin_modes test compares against another RVM expression, so it can pass when both expressions swallow the same error; it is not an OPA oracle.

References: OPA strict builtin-error behavior and OPA propagating the default evaluation error. Source-derived finding, not an executed reproduction.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OPA-compatibility review: the core runtime-default change is worth doing. OPA explicitly allows runtime-dependent comprehensions in default values, so freezing those values during compilation is a real correctness bug. However, the indexed-parent merge behavior and named default-function parameter binding need the corrections described in the individual comments. I am also adding OPA-specific error-propagation guidance to the existing fallback-error thread.

The reported OPA conformance pass does not by itself validate these RVM changes: tests/opa.rs includes defaultkeyword, functions, and refheads in OPA_TODO_FOLDERS, so their RVM checks are skipped by default unless OPA_UNSKIP_FOLDERS overrides that list. Please add explicit OPA-oracle coverage for the affected constructs and assert the expected outcome in both execution modes; interpreter/RVM agreement alone can preserve an existing Regorus incompatibility.

This review is based on the diff and OPA documentation/compiler/evaluator source; no builds, tests, or OPA evaluations were run.

u32::try_from(self.program.instructions.len()).map_err(|_| CompilerError::General {
message: format!("default entry point for '{rule_path}' exceeds u32"),
})?;
let value_register = self.compile_rego_expr(value)?;

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Bind named default-function parameters before compiling the fallback

The fallback starts a fresh scope and compiles value, but never declares/binds the names in args. For example:

package probe
default f(x) := [y | y := x]
result := f(7)

OPA binds the default function's head arguments before evaluating its body, so result is [7]; references inside the comprehension are permitted. Here x has no parameter binding, so resolve_variable instead attempts to resolve a package rule such as data.probe.x. The fallback preparation in both VM paths also fills the register window with Undefined, discarding the actual argument values.

Please reserve and bind the default head parameters and preserve/reinitialize their actual values when entering the fallback in both execution modes. Add a regression that actually uses a named parameter inside the default comprehension; the new tests currently use ignored/unused parameters.

OPA source reference: default function argument setup and argument unification. Source-derived finding, not an executed reproduction.

Comment thread tests/rvm/rego/mod.rs
package probe
default values["constant"] := true
default values["runtime"] := [item | item := input.items[_]]
values := {"ordinary": "defined"} if input.include_ordinary

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] Do not use a conflicting complete-parent/child policy as an OPA compatibility regression

This policy combines child defaults values["constant"] / values["runtime"] with a complete parent definition values := {"ordinary": "defined"}. OPA rejects that combination at compilation: the complete parent data.probe.values conflicts with its child rules, regardless of whether input.include_ordinary is true. It does not evaluate this policy to the merged object asserted below.

Ground child defaults themselves are valid Rego. The incompatibility is treating them as generic missing-key overlays on a separately defined complete parent. The existing interpreter already accepting that form does not establish OPA compatibility; this test now locks the behavior into RVM and its serialized artifacts too.

Please use OPA-valid independent child definitions for the conformance regression and preserve per-document default selection. Either reject the complete-parent conflict or explicitly gate/document this merge as a Regorus extension rather than OPA semantics.

OPA source reference: ground parent/child conflict detection and conflict reporting. Source-derived finding, not an executed reproduction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants