Files
regorus/tests/rvm/vm/suites/coalesce_undefined_to_null.yaml
Anand Krishnamoorthi e5ac9a2734 feat(rvm): new instructions and loop semantics for Azure Policy support (#659)
The Rego VM was designed around Rego's semantics, but Azure Policy needs
a few things Rego doesn't: host-supplied context alongside input/data,
undefined-to-null coercion for missing fields, skip-undefined collection
behavior for wildcard aliases, and non-vacuous iteration over non-array
values.

This commit adds five new instructions to bridge those gaps:

  LoadContext / LoadMetadata — give programs access to host-supplied
  evaluation context and cached program metadata at runtime.

  ArrayPushDefined — like ArrayPush but silently drops undefined values,
  so wildcard alias collection (field[*].property) excludes absent
  nested properties instead of leaking undefined entries into the array.

  ReturnUndefinedIfNotTrue — early return with Undefined when a guard
  condition isn't satisfied, without tripping a VM assertion failure.
  This models "condition doesn't match" cleanly.

  CoalesceUndefinedToNull — turns Undefined into Null in-place so that
  downstream builtins see null rather than short-circuiting on undefined.

The loop engine also gains an Azure Policy mode: when the source language
is "azure_policy", an Every loop over a non-array value (scalars, null,
objects) iterates once over a virtual Null element instead of being
vacuously true.  This matches how field[*] behaves on non-array fields
in Azure Policy — the condition body runs once against Null, which
typically evaluates to false.

On the plumbing side: the VM gets a context field with set_context(),
metadata is cached as a Value on program load, and map_limit_error is
inlined into memory_check since it had only one call site.

Four new YAML test suites (~880 lines) cover the new instructions and
context/metadata loading, along with instruction parser, display, and
assembly listing support for everything added here.
2026-04-06 15:40:41 -05:00

195 lines
6.2 KiB
YAML

# CoalesceUndefinedToNull instruction test suite
#
# Used by the Azure Policy compiler to convert undefined (missing) field values
# to null before passing them to comparison operators. This prevents undefined
# propagation from short-circuiting subsequent builtins.
#
# Semantics:
# - If register == Undefined → replaces it with Null (in-place).
# - For any other value (null, bool, number, string, array, object) → no-op.
cases:
# =========================================================================
# Basic coalescing behavior
# =========================================================================
- note: undefined_becomes_null
description: Undefined register value is replaced with null
literals: []
instructions:
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: null
- note: null_unchanged
description: Null value is not modified
literals: []
instructions:
- "LoadNull { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: null
- note: true_unchanged
description: Boolean true is not modified
literals: []
instructions:
- "LoadTrue { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: true
- note: false_unchanged
description: Boolean false is not modified
literals: []
instructions:
- "LoadFalse { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: false
- note: number_unchanged
description: Number value is not modified
literals:
- 42
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: 42
- note: string_unchanged
description: String value is not modified
literals:
- "hello"
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: "hello"
- note: empty_string_unchanged
description: Empty string is not modified (not confused with undefined)
literals:
- ""
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: ""
- note: array_unchanged
description: Array value is not modified
literals: []
instructions:
- "ArrayNew { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
want_result: []
# =========================================================================
# Multiple coalesces — only undefined registers are affected
# =========================================================================
- note: selective_coalescing
description: Only undefined registers are coalesced; others remain unchanged
literals:
- 99
instructions:
- "Load { dest: 0, literal_idx: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "CoalesceUndefinedToNull { register: 1 }"
- "ArrayNew { dest: 2 }"
- "ArrayPush { arr: 2, value: 0 }"
- "ArrayPush { arr: 2, value: 1 }"
- "Return { value: 2 }"
want_result: [99, null]
# =========================================================================
# Interaction with ReturnUndefinedIfNotTrue
# =========================================================================
- note: coalesce_then_guard_null_returns_undefined
description: >
Coalescing undefined to null, then passing to ReturnUndefinedIfNotTrue
returns undefined (null is not true)
literals:
- "should not reach"
instructions:
- "CoalesceUndefinedToNull { register: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Return { value: 1 }"
want_result: "#undefined"
- note: coalesce_preserves_true_for_guard
description: >
Coalescing a true value is a no-op, so ReturnUndefinedIfNotTrue
continues execution
literals:
- "reached"
instructions:
- "LoadTrue { dest: 0 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "ReturnUndefinedIfNotTrue { condition: 0 }"
- "Load { dest: 1, literal_idx: 0 }"
- "Return { value: 1 }"
want_result: "reached"
# =========================================================================
# Interaction with HostAwait (suspendable mode)
# =========================================================================
- note: coalesce_after_host_await_undefined_response
description: >
HostAwait returns undefined, CoalesceUndefinedToNull converts it to null
literals:
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "HostAwait { dest: 0, arg: 1, id: 2 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
host_await_responses:
- id: "await-id"
value: "#undefined"
want_result: null
- note: coalesce_after_host_await_value_response
description: >
HostAwait returns a real value, CoalesceUndefinedToNull is a no-op
literals:
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "HostAwait { dest: 0, arg: 1, id: 2 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
host_await_responses:
- id: "await-id"
value: "real-value"
want_result: "real-value"
- note: suspendable_coalesce_after_host_await
description: >
In suspendable mode: HostAwait suspends and resumes with value,
CoalesceUndefinedToNull is a no-op
literals:
- "await-id"
- "arg"
instructions:
- "Load { dest: 1, literal_idx: 1 }"
- "Load { dest: 2, literal_idx: 0 }"
- "HostAwait { dest: 0, arg: 1, id: 2 }"
- "CoalesceUndefinedToNull { register: 0 }"
- "Return { value: 0 }"
host_await_responses_suspendable:
- id: "await-id"
value: 42
ignore_run_to_completion_hostawait_failure: true
want_result: 42